Compare commits

...

62 Commits

Author SHA1 Message Date
Rustem 05d2a3ba8f requestlimit 2025-08-12 14:19:28 +05:00
Rustem bc4c00d967 new 2025-08-10 00:34:46 +05:00
Rustem 142c65cedc new 2025-08-10 00:18:13 +05:00
Rustem 6ad0fe85e6 new 2025-08-10 00:06:01 +05:00
Rustem ae3bd41178 master 2025-06-24 13:27:31 +05:00
Rustem dc4bc7f88b master 2025-06-16 17:47:43 +05:00
Rustem f66ef5c75c master 2025-06-16 17:46:06 +05:00
Rustem 2d5c650310 master 2025-06-16 17:45:40 +05:00
Rustem 92f2ca3229 master 2025-06-16 17:44:22 +05:00
Rustem db933f8aca master 2025-06-14 22:16:22 +05:00
Rustem 56921dd6d4 master 2025-06-14 21:32:01 +05:00
Rustem 9bf02e2940 master 2025-06-14 15:43:08 +05:00
Rustem 772af756b5 master 2025-06-14 14:38:24 +05:00
Rustem e0f426ce54 master 2025-06-12 20:46:56 +05:00
Rustem 6c5b7f56df master 2025-06-12 20:44:34 +05:00
Rustem 8736f248dc master 2025-06-12 20:43:20 +05:00
Rustem 430e910afb master 2025-06-12 20:41:28 +05:00
Rustem 5ca3b2655f master 2025-06-08 01:50:32 +05:00
Rustem 52d41a1346 master 2025-06-03 00:32:52 +05:00
Rustem d9057193b8 master 2025-06-03 00:32:10 +05:00
Rustem 3eea63b147 master 2025-06-03 00:30:33 +05:00
Rustem c18348a43d master 2025-06-02 10:22:45 +05:00
Rustem 7f72804ce0 master 2025-05-28 16:38:20 +05:00
Rustem ef9750f729 master 2025-05-28 16:37:37 +05:00
Rustem ec737e2a49 master 2025-05-28 16:27:33 +05:00
Rustem 737605d066 master 2025-05-26 11:43:35 +05:00
Rustem 4eb7421506 master 2025-05-26 11:42:47 +05:00
Rustem de78b985ea master 2025-05-26 11:36:07 +05:00
Rustem 928d6e9ad0 master 2025-05-26 11:19:59 +05:00
Rustem 04594bf24c master 2025-05-26 10:25:59 +05:00
Rustem 6a09e53e0b master 2025-05-25 21:57:43 +05:00
Rustem 54d5abfa76 master 2025-05-25 21:56:40 +05:00
Rustem dfb547b3de master 2025-05-25 12:41:48 +05:00
Rustem 2ba4146f5b master 2025-05-24 19:35:25 +05:00
Rustem ea036c8913 master 2025-05-24 19:34:05 +05:00
Rustem 83d62045bd master 2025-05-24 19:33:12 +05:00
Rustem fab3679f53 master 2025-05-24 19:09:42 +05:00
Rustem 23b2fdb22c master 2025-05-24 19:07:57 +05:00
Rustem 75e4922ec5 master 2025-05-24 18:59:43 +05:00
Rustem e2454b8112 master 2025-05-24 18:52:48 +05:00
Rustem 0feee086f8 master 2025-05-24 18:52:35 +05:00
Rustem b8054bd149 master 2025-05-24 18:50:05 +05:00
Rustem 4fd2ecc85c master 2025-05-24 15:34:27 +05:00
Rustem fff0d79335 master 2025-05-24 15:29:07 +05:00
Rustem 54b4271312 master 2025-05-24 13:00:09 +05:00
Rustem 97b31e4a3b master 2025-05-24 12:48:21 +05:00
Rustem 56be064c19 master 2025-05-24 12:46:26 +05:00
Rustem ec1a2cc27c master 2025-05-24 12:42:16 +05:00
Rustem 8e318a041b master 2025-05-24 09:48:06 +05:00
Rustem 2d5e5a609f master 2025-05-24 09:38:23 +05:00
Rustem 0b87f9f290 master 2025-05-23 22:00:36 +05:00
Rustem c18dc4f682 deposit 2025-05-23 21:51:48 +05:00
Rustem b0ea436307 deposit 2025-05-23 21:46:22 +05:00
Rustem a46dc1ec01 deposit 2025-05-23 21:43:45 +05:00
nimtaurel dce5c1e30f Merge pull request 'nim_style' (#16) from nim_style into master
Reviewed-on: #16
2025-05-20 11:48:03 +00:00
nimtaurel 5b33da4b8f Merge pull request 'nim_style' (#15) from nim_style into master
Reviewed-on: #15
2025-05-19 13:50:52 +00:00
nimtaurel 816de5f5ac Merge pull request 'nim_style 3' (#14) from nim_style into master
Reviewed-on: #14
2025-05-19 13:36:38 +00:00
nimtaurel bdc40dfbdc Merge pull request 'nim_style 9' (#13) from nim_style into master
Reviewed-on: #13
2025-05-15 13:24:54 +00:00
Rustem 7e56f7da90 orders 2025-05-02 14:50:03 +05:00
Rustem 80aae069d0 orders 2025-05-02 14:49:02 +05:00
nimtaurel f32b389fa3 Merge pull request 'nim_style 9' (#12) from nim_style into master
Reviewed-on: #12
2025-04-29 09:01:23 +00:00
nimtaurel faa33860ae Merge pull request 'nim_style 8' (#11) from nim_style into master
Reviewed-on: #11
2025-04-28 13:54:50 +00:00
30 changed files with 1738 additions and 541 deletions

View File

@ -24,6 +24,7 @@ class PipiCarInstallCommands extends InstallCommand
$this->packages = [
'main', // Главная страница
'address', // Главная страница
'auto_brands', // Бренд авто
'auto_bodywork', // Кузов авто
'auto_colors', // Цвета авто
@ -37,7 +38,8 @@ class PipiCarInstallCommands extends InstallCommand
'auto', // авто
'applications', //Заявки
'pipi_users', // Добавление логики для пользователей
'auto_calendar' // Добавление логики для пользователей
'auto_calendar', // Добавление логики для пользователей
'faq' // FAQ
];
}

View File

@ -5,8 +5,11 @@ namespace App\Http\Controllers;
use A7kz\Platform\Models\UniModel;
use A7kz\Platform\Modules\Platform\Acl\Facades\Acl;
use A7kz\Platform\Modules\Platform\Segment\Facades\Segment;
use App\Http\Notifications\TelegramNotification;
use App\Models\User;
use App\Modules\applications\Enum\ApplicationStatus;
use App\Modules\auto\Enums\AutoStatusEnums;
use App\Service\DepositService;
use Carbon\CarbonPeriod;
use Exception;
use Illuminate\Http\JsonResponse;
@ -58,73 +61,101 @@ class MobileApiController extends Controller
return response()->json($data);
}
/**
* @throws Exception
*/
public function sendApplication(Request $request): JsonResponse {
$data = $request->all();
$data['started_at'] = Carbon::parse($data['started_at'])?->format('Y-m-d H:i:s');
$data['ended_at'] = Carbon::parse($data['ended_at'])?->format('Y-m-d H:i:s');
$authToken = null;
$data['car_id'] = null;
if ($request->header('Authorization')) {
$user = auth()->guard('api')->user();
$data['user_id'] = $user?->id;
$authToken = $request->header('Authorization');
} else {
$user = UniModel::model('core_users')
->where('email', $data['email'])
->first();
if ($user) {
$data['user_id'] = $user->id;
$tokenResult = $user->createToken('auth_token');
$authToken = $tokenResult->accessToken;
try {
$data = $request->all();
$data['started_at'] = Carbon::createFromFormat('d-m-Y H:i', $data['started_at']);
$data['ended_at'] = Carbon::createFromFormat('d-m-Y H:i', $data['ended_at']);
$authToken = null;
$data['car_id'] = null;
if ($request->header('Authorization')) {
$user = auth()->guard('api')->user();
$data['user_id'] = $user?->id;
$authToken = $request->header('Authorization');
} else {
$data['user_id'] = null;
$user = User::where('email', $data['email'])->first();
if ($user) {
$data['user_id'] = $user->id;
$tokenResult = $user->createToken('auth_token');
$authToken = $tokenResult->plainTextToken;
} else {
$data['user_id'] = null;
}
}
}
$response = [
'status' => 'OK',
'message' => 'Заявка создана',
];
$response = [
'status' => 'OK',
'message' => 'Заявка создана',
];
$car = $this->getAvailableCar($data['started_at'], $data['ended_at'], $data['mark_id'], $data['color_code']);
if (!isset($car)) {
return response()->json('Нет свободных машин');
}
$data['car_id'] = $car->id;
if ($data['user_id']) {
$period = CarbonPeriod::create($data['started_at'], $data['ended_at']);
$dates = array_map(fn($date) => $date->toDateString(), iterator_to_array($period));
foreach ($dates as $date) {
UniModel::model('pipi_auto_calendar')->create([
'auto_id' => $data['car_id'],
'date' => $date,
'status' => AutoStatusEnums::Rent->name
]);
$car = $this->getAvailableCar($data['started_at'], $data['ended_at'], $data['mark_id'], $data['color_code']);
$status = ApplicationStatus::pending->value;
if (!isset($car)) {
return response()->json('Нет свободных машин');
}
$response['auth_token'] = $authToken;
$response['message'] = 'Заявка создана, и вы были автоматически авторизованы';
} elseif (!isset($data['user_id'])) {
$response['message'] = 'Заявка создана, с вами свяжется наш оператор';
$data['car_id'] = $car->id;
if ($data['user_id']) {
$period = CarbonPeriod::create($data['started_at'], $data['ended_at']);
$dates = array_map(fn($date) => $date->toDateString(), iterator_to_array($period));
foreach ($dates as $date) {
UniModel::model('pipi_auto_calendar')->create([
'auto_id' => $data['car_id'],
'date' => $date,
'status' => AutoStatusEnums::Rent->name
]);
}
$status = ApplicationStatus::approved->value;
$response['auth_token'] = $authToken;
$response['message'] = 'Заявка создана, и вы были автоматически авторизованы';
} elseif (!isset($data['user_id'])) {
$response['message'] = 'Заявка создана, с вами свяжется наш оператор';
}
$address_end = UniModel::model('pipi_address')->where('name', $data['address_end'])->first();
$address_start = UniModel::model('pipi_address')->where('name', $data['address_start'])->first();
$service = new DepositService;
$sums = $service->calculateSummary($data['mark_id'], $data['started_at'], $data['ended_at']);
$sum = $sums['discounted_sum'];
if ($data['deposit'] != $sums['deposit_base']) {
$sum += $data['deposit'];
}
UniModel::model('pipi_applications')->create([
'rent_day' => $data['rent_day'],
'started_at' => $data['started_at'],
'ended_at' => $data['ended_at'],
'user_id' => $data['user_id'],
'phone' => $user?->phone ?? $data['phone'] ?? '' ,
'car_id' => $data['car_id'],
'user_name' => $user?->name ?? $data['name'] ?? null,
'user_surname' => $user?->name ?? $data['surname'] ?? null,
'user_email' => $user?->email ?? $data['email'] ?? null,
'address_end' => $address_end?->id,
'address_start' => $address_start?->id,
'deposit' => $data['deposit'] ?? null,
'status' => $status,
'sum' => $sum
]);
$notification = [];
$notification['Адрес получения'] = $data['address_start'];
$notification['Адрес возврата'] = $data['address_end'];
$notification['Дата и время начала'] = $data['started_at'];
$notification['Дата и время возврата'] = $data['ended_at'];
$notification['Телефон'] = $data['phone'] ?? $user?->phone ?? null;
$notification['Имя'] = $data['name'] ?? $user?->name ?? null;
$notification['Почта'] = $data['email'] ?? $user?->email ?? null;
$notification['Сообщение'] = __('Новая заявка');
$this->sendNotification($notification);
return response()->json($response);
} catch (Exception $e) {
return response()->json($e->getMessage());
}
UniModel::model('pipi_applications')->create([
'rent_day' => $data['rent_day'],
'started_at' => $data['started_at'],
'ended_at' => $data['ended_at'],
'user_id' => $data['user_id'],
'phone' => $data['phone'],
'car_id' => $data['car_id'],
'user_name' => $data['name'] ?? null,
'user_surname' => $data['surname'] ?? null,
'user_email' => $data['email'] ?? null,
'status' => ApplicationStatus::pending->value
]);
return response()->json($response);
}
public function getApplications(): JsonResponse
@ -139,20 +170,33 @@ class MobileApiController extends Controller
]);
}
$applications = [];
$application = UniModel::model('pipi_applications')->where('user_id', $user->id)->get();
foreach ($application as $app) {
$status = $request->query('status');
$data = [];
$query = UniModel::model('pipi_applications')->where('user_id', $user->id);
if (isset($status)) {
$query->where('status', $status);
} else {
$query->whereIn('status', [ApplicationStatus::approved->value, ApplicationStatus::review->value]);
}
$applications = $query->orderBy('id', 'desc')->get();
foreach ($applications as $app) {
$car = UniModel::model('pipi_auto')->find($app->car_id);
$model = UniModel::model('pipi_brand_models')->find($car->model_id);
$photo = UniModel::model('core_files')->find($model->photo_id);
$photo = url('api/files/file/' . ltrim($photo?->path, '/'));
$class_id = UniModel::model('pipi_auto_classes')->where('id', $model->class_id)->first();
$bodywork_id = UniModel::model('pipi_auto_bodywork')->where('id', $model->bodywork_id)->first();
$applications[] = [
$address = UniModel::model('pipi_address')->where('id', $app->address_end)->first();
$data[] = [
'id' => $app->id,
'rent_day' => $app->rent_day,
'started_at' => $app->started_at,
'ended_at' => $app->ended_at,
'status' => ApplicationStatus::from($app->status)->getName(),
'sum' => $app->sum ?? 0,
'address_end' => $address->name ?? '',
'car' => [
'name' => $model->name,
'year' => $model->year,
@ -168,10 +212,10 @@ class MobileApiController extends Controller
]
];
}
if (empty($applications)) {
$applications = (object) $applications;
if (empty($data)) {
$data = (object) $data;
}
return response()->json($applications);
return response()->json($data);
}
public function login(Request $request)
@ -198,16 +242,18 @@ class MobileApiController extends Controller
}
}
public function checkAvailableCar($started_at = null, $ended_at = null, $modelId = null): bool {
if (!$started_at || !$ended_at || !$modelId) {
public function checkAvailableCar($started_at = null, $ended_at = null, $modelId = null, $color = null): JsonResponse
{
if (!$started_at || !$ended_at || !$modelId || !$color) {
$request = request();
$started_at = $started_at ?? $request->input('started_at');
$ended_at = $ended_at ?? $request->input('ended_at');
$modelId = $modelId ?? $request->input('model_id');
$color = Unimodel::model('pipi_auto_colors')->where('code', $request->input('color'))->first()->id;
}
if (!$started_at || !$ended_at || !$modelId) {
return false;
if (!$started_at || !$ended_at || !$modelId || !$color) {
return response()->json(['available' => false]);
}
$started_at = Carbon::parse($started_at)->format('Y-m-d');
@ -215,6 +261,7 @@ class MobileApiController extends Controller
$cars = UniModel::model('pipi_auto')
->where('is_inactive', '=', false)
->where('model_id', $modelId)
->where('color_id', $color)
->get();
$busyCars = UniModel::model('pipi_auto_calendar')
->whereBetween('date', [$started_at, $ended_at])
@ -223,11 +270,9 @@ class MobileApiController extends Controller
$availableCars = $cars->reject(fn($car) => in_array($car->id, $busyCars));
return $availableCars->isNotEmpty();
return response()->json(['available' => $availableCars->isNotEmpty()]);
}
public function getAvailableMarksList(Request $request): JsonResponse
{
$started_at = $request->query('started_at');
@ -240,11 +285,11 @@ class MobileApiController extends Controller
$availableMarks = [];
if ($class) {
$class_id = UniModel::model('pipi_auto_classes')->where('name', $class)->first()->id;
$class_id = UniModel::model('pipi_auto_classes')->where('name', $class)->first()?->id;
$marks = $marks->where('class_id', $class_id);
}
if ($bodywork) {
$bodywork_id = UniModel::model('pipi_auto_bodywork')->where('name', $bodywork)->first()->id;
$bodywork_id = UniModel::model('pipi_auto_bodywork')->where('name', $bodywork)->first()?->id;
$marks = $marks->where('bodywork_id', $bodywork_id);
}
@ -253,13 +298,13 @@ class MobileApiController extends Controller
continue;
}
if (!$this->checkAvailableCar($started_at, $ended_at, $mark->id)) {
$tariffs = UniModel::model('pipi_auto_tariffs')->where('model_id', $mark->id)->get();
if ($tariffs->isEmpty()) {
continue;
}
$cars = UniModel::model('pipi_auto')->where('model_id', $mark->id)->get();
if ($cars->isEmpty()) {
continue;
}
@ -289,7 +334,7 @@ class MobileApiController extends Controller
$carsByColor = $cars->groupBy('color_id');
foreach ($carsByColor as $carColor => $carsOfColor) {
$carColor = UniModel::model('pipi_auto_colors')->find($carColor)->code;
$carColor = UniModel::model('pipi_auto_colors')->find($carColor);
$colorPath = $path;
if ($carsOfColor->first()->photo_id) {
@ -297,14 +342,14 @@ class MobileApiController extends Controller
$colorPath = url('api/files/file/' . ltrim($photo->path, '/'));
}
$key = $mark->name . '-' . $mark->year . '-' . $carColor;
$key = $mark->name . '-' . $mark->year . '-' . $carColor->code;
$availableMarks[$key] = [
'id' => $mark->id,
'brand' => $brand?->name,
'mark' => $mark->name,
'year' => $mark->year,
'color' => $carColor,
'color' => $carColor->code,
'configuration' => $equipment?->name,
'people' => $mark?->people ?? '5',
'actuator' => $mark?->actuator ?? 'Передний',
@ -317,6 +362,7 @@ class MobileApiController extends Controller
'deposit' => $tariffs_new[0]['deposit'] ?? 30000,
'conditioner' => $mark?->conditioner,
'photo' => $colorPath,
'available' => $this->checkAvailableCar($started_at, $ended_at, $mark->id, $carColor->id)->getData()->available,
'tariffs' => $tariffs_new,
];
}
@ -336,6 +382,7 @@ class MobileApiController extends Controller
$cars = UniModel::model('pipi_auto')
->where('color_id', $color)
->where('model_id', $modelId)
->whereNull('regular_user')
->get();
$busyCars = UniModel::model('pipi_auto_calendar')
->whereBetween('date', [$started_at, $ended_at])
@ -359,45 +406,14 @@ class MobileApiController extends Controller
$start = Carbon::createFromFormat('d-m-Y H:i', $started_at);
$end = Carbon::createFromFormat('d-m-Y H:i', $ended_at);
$days = $start->diffInDays($end);
try {
$service = new DepositService();
$summary = $service->calculateSummary($mark_id, $start, $end);
// Получаем тарифы
$tariffs = UniModel::model('pipi_auto_tariffs')
->where('model_id', $mark_id)
->get();
if (!isset($tariffs)) {
return response()->json(__('Отсутсвуют данные по машине'));
return response()->json($summary);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
$discountRate = null;
$basePrice = null;
foreach ($tariffs as $range) {
if ($range->day_range_start == 1 && $range->day_range_end == 2) {
$basePrice = $range->base_rate;
}
if ($days >= $range->day_range_start && $days <= $range->day_range_end) {
$discountRate = $range->base_rate;
}
}
// Если не нашли подходящий тариф, но дней больше 30 — применяем скидку
if (is_null($discountRate) && $days > 30 && count($tariffs) > 0) {
$discountRate = round($tariffs[0]->base_rate * 0.6);
}
// Считаем суммы
$baseSum = $basePrice ? $basePrice * $days : null;
$discountedSum = $discountRate ? $discountRate * $days : null;
return response()->json([
'days' => $days,
'base_price_per_day' => $basePrice,
'discount_price_per_day' => $discountRate,
'base_sum' => $baseSum,
'discounted_sum' => $discountedSum,
]);
}
public function closeOrder(Request $request): JsonResponse
@ -412,7 +428,7 @@ class MobileApiController extends Controller
]);
}
if (count($request->file('photos')) != 5) {
if (count($request->file('photos')) < 5) {
return response()->json([
'success' => false,
'message' => __('Неверное количество фотографий')
@ -423,8 +439,13 @@ class MobileApiController extends Controller
$files[] = $this->uploadFile($photo);
}
$application->photos = $files;
$application->status = ApplicationStatus::review->value;
$application->save();
$notification = [];
$notification['ID заявки'] = $application->id;
$notification['Сообщение'] = __('Получена заявка на закрытие');
$this->sendNotification($notification);
return response()->json([
'success' => true,
'message' => __('Фото получены ожидайте закрытия заявки после проверки оператором'),
@ -456,4 +477,62 @@ class MobileApiController extends Controller
$folder2 = substr($storage_file_name, 2, 3);
return $folder1 . '/' . $folder2 . '/' . $storage_file_name . '.' . $ext;
}
public function getAddress(): JsonResponse
{
$addresses = UniModel::model('pipi_address')->get();
foreach ($addresses as $address) {
$data[$address->id] = $address->name;
}
if (empty($data)) {
$data = (object) [];
}
return response()->json($data);
}
public function getFAQCategory(): JsonResponse
{
$faqs = UniModel::model('pipi_faq')->distinct('category')->get();
$data = [];
foreach ($faqs as $faq) {
$data[] = $faq->category;
}
return response()->json($data);
}
public function getFAQSubCategory(Request $request): JsonResponse
{
$category = $request->query('category');
$query = UniModel::model('pipi_faq');
if (isset($category)) {
$query->where('category', $category);
}
$faqs = $query->distinct('subcategory')->get();
$data = [];
foreach ($faqs as $faq) {
$data[] = $faq->subcategory;
}
return response()->json($data);
}
public function getFAQText(Request $request): JsonResponse
{
$subcategory = $request->query('subcategory');
$query = UniModel::model('pipi_faq')->where('subcategory', $subcategory)->first();
$text = $query?->text;
return response()->json($text ?? '');
}
private function sendNotification(array $data): void
{
foreach ($data as $key => $value) {
$text[$key] = $key . ': ' . $value;
}
Notification::send([$text], new TelegramNotification($text));
}
}

View File

@ -32,19 +32,26 @@ class TelegramNotification extends Notification
public function toTelegram($notifiable)
{
print_r($notifiable);
return TelegramMessage::create()
->to(env('TELEGRAM_CHAT_ID'))
->content($notifiable['car'])
->line('')
->line($notifiable['base_price'])
->line($notifiable['period'])
->line($notifiable['pickup_location'])
->line($notifiable['return_location'])
->line($notifiable['customer_type'])
->line($notifiable['customer_full_name'])
->line($notifiable['customer_email'])
->line($notifiable['customer_phone']);
$message = TelegramMessage::create()
->to(config('services.telegram-bot-api.id'))
->content($notifiable['Сообщение']);
$excludedFields = [
'Сообщение',
'id',
'created_at',
'updated_at',
'token'
];
foreach ($notifiable as $field => $value) {
if (in_array($field, $excludedFields) || empty($value)) {
continue;
}
$message->line($value);
}
return $message;
}
/**

View File

@ -18,13 +18,8 @@ class Seed
Nav::add(new NavItem('pipicar', '/app/main', 1, 'Главная', ["admin","director","coworker","observer","moderator"], 'star'));
Nav::add(new NavItem('pipicar-nav-auto', null, 1, 'Автомобили', ["admin"], 'car-front'));
Nav::add(new NavItem('auto', '/app/pipicar.auto', 1, 'Машины', ["admin"], 'car-front'), 'pipicar-nav-auto');
Nav::add(new NavItem('marks', '/app/pipicar.auto_brands', 1, 'Марки', ["admin"], 'car-front'), 'pipicar-nav-auto');
Nav::add(new NavItem('classes', '/app/pipicar.auto_classes', 1, 'Классы', ["admin"], 'car-front'), 'pipicar-nav-auto');
Nav::add(new NavItem('colors', '/app/pipicar.auto_colors', 1, 'Цвета', ["admin"], 'car-front'), 'pipicar-nav-auto');
Nav::add(new NavItem('tariffs', '/app/pipicar.auto_tariffs', 1, 'Тарифы', ["admin"], 'car-front'), 'pipicar-nav-auto');
Nav::add(new NavItem('types', '/app/pipicar.auto_types', 1, 'Типы', ["admin"], 'car-front'), 'pipicar-nav-auto');
Nav::add(new NavItem('models', '/app/pipicar.brand_models', 1, 'Модели', ["admin"], 'car-front'), 'pipicar-nav-auto');
Nav::add(new NavItem('models', '/app/pipicar.auto_orders', 1, 'Заявки', ["admin"], 'car-front'), 'pipicar-nav-auto');
Nav::add(new NavItem('orders', '/app/pipicar.auto_orders', 1, 'Заявки', ["admin"], 'car-front'), 'pipicar-nav-auto');
}
private static function nav(): void
{}

View File

@ -57,7 +57,7 @@ class RouteServiceProvider extends ServiceProvider
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
return Limit::perMinute(1000)->by(optional($request->user())->id ?: $request->ip());
});
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace App\Service;
use A7kz\Platform\Models\UniModel;
use Illuminate\Support\Carbon;
class DepositService
{
public function calcWithoutDeposit($deposit, $day): float {
$percentRates = [
3.00, 3.00, 3.00, 4.05, 5.10, 6.15, 7.20, 8.25, 9.30,
10.35, 11.40, 12.45, 13.00, 13.00, 13.00, 13.00, 13.00, 13.00, 13.00,
13.00, 13.00, 13.00, 13.00, 13.00, 13.00, 13.00, 13.00, 13.00, 13.00, 13.00,
13.44, 13.88, 14.32, 14.76, 15.20
];
$ndsRate = 0.12;
$lastKnownRate = 15.20;
$index = $day - 1;
$percent = $percentRates[$index] ?? $lastKnownRate;
if ($percent === null) {
return 0.0;
}
$sum = $deposit * ($percent / 100) * (1 + $ndsRate);
return round($sum, 2);
}
/**
* @throws \Exception
*/
public function calculateSummary(int $markId, $start, $end): array
{
$fullDays = $start->copy()->diffInDays($end); // 7
$totalHours = $start->copy()->diffInHours($end); // 191 часов
$hoursRemainder = $totalHours - ($fullDays * 24); // остаток: 191 - 7*24 = 191 - 168 = 23
$threshold = 9; // если остаток по часам больше этого порога, то добавим день
$days = $fullDays + ($hoursRemainder > $threshold ? 1 : 0);
$tariffs = UniModel::model('pipi_auto_tariffs')
->where('model_id', $markId)
->get();
if ($tariffs->isEmpty()) {
throw new \Exception(__('Отсутствуют данные по машине'));
}
$basePrice = null;
$discountRate = null;
$deposit = null;
foreach ($tariffs as $range) {
if ($range->day_range_start == 1 && $range->day_range_end == 2) {
$basePrice = $range->base_rate;
}
if (isset($range->deposit)) {
$deposit = $range->deposit;
}
if ($days >= $range->day_range_start && $days <= $range->day_range_end) {
$discountRate = $range->base_rate;
}
}
// Если не нашли скидку, но дней > 30
if (is_null($discountRate) && $days > 30 && $basePrice) {
$discountRate = round($basePrice * 0.6);
}
$baseSum = $basePrice ? $basePrice * $days : null;
$discountedSum = $discountRate ? $discountRate * $days : null;
return [
'days' => $days,
'base_price_per_day' => $basePrice,
'discount_price_per_day' => $discountRate,
'base_sum' => $baseSum,
'discounted_sum' => $discountedSum,
'deposit_base' => $deposit,
'without_deposit' => $this->calcWithoutDeposit($deposit, $days),
];
}
}

View File

@ -12,11 +12,12 @@
"license": "MIT",
"require": {
"php": "^8.1",
"a7kz/platform": "*",
"guzzlehttp/guzzle": "^7.2",
"laravel-notification-channels/telegram": "^5.0",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.3",
"laravel/tinker": "^2.8",
"a7kz/platform": "*"
"laravel/tinker": "^2.8"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",

672
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,11 @@ return [
|
*/
'telegram-bot-api' => [
'token' => env('TELEGRAM_BOT_TOKEN'),
'id' => env('TELEGRAM_CHAT_ID'),
],
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),

View File

@ -13,7 +13,7 @@ $approvedApplications = UniModel::model('pipi_applications', Acl::connection())
// Заявки с ожидающим статусом
$pendingApplications = UniModel::model('pipi_applications', Acl::connection())
->where('user_id', auth()->user()->id)
->where('status', ApplicationStatus::reserved)
->where('status', ApplicationStatus::approved)
->get();
?>

View File

@ -0,0 +1,9 @@
{
"admin": [
"default",
"add",
"show",
"edit",
"delete"
]
}

104
modules/address/app.json Normal file
View File

@ -0,0 +1,104 @@
{
"module": "pipicar",
"name": "pipicar.address",
"type": "crud",
"title": "Адреса",
"withHeader": false,
"data": {
"table": "pipi_address",
"pk": "id",
"limit": 25,
"segment": true,
"timestamp": false,
"fields": {
"id": {
"type": "pk"
},
"name": {
"type": "string"
}
}
},
"ui": {
"grid": {
"title": "Адреса",
"component": "App.components.Grid",
"cols": [
{
"name": "name",
"caption": "Адрес"
}
],
"action": {
"head": [
"add"
],
"row": [
"edit",
"delete"
]
},
"filter": {
"template": "app.base.crud.filter",
"rows": [
{
"cols": [
{
"size": 6,
"input": {
"name": "name",
"label": "Название цвета"
}
}
]
}
]
}
},
"forms": {
"add": {
"title": "Адрес",
"template": "app.base.crud.form",
"component": "App.components.Show",
"form": {
"submits": "struct:crud.form.edit.submits",
"rows": [
{
"cols": [
{
"size": 6,
"input": {
"name": "name",
"label": "Название"
}
}
]
}
]
}
},
"edit": {
"title": "Адрес",
"template": "app.base.crud.form",
"component": "App.components.Show",
"form": {
"rows": [
{
"cols": [
{
"size": 6,
"input": {
"name": "name",
"label": "Название"
}
}
]
}
],
"submits": "struct:crud.form.edit.submits"
}
}
}
},
"actions": "struct:crud.actions"
}

View File

@ -0,0 +1,23 @@
<?php
use A7kz\Platform\Modules\Platform\Segment\Facades\Segment;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
$segments = Segment::listActive();
foreach ($segments as $segment) {
Schema::connection($segment->connector)->create('pipi_address', static function (Blueprint $table) {
$table->id();
$table->string('name')->nullable()->comment('Наименование');
$table->timestamps();
$table->softDeletes();
});
}
}
};

View File

@ -0,0 +1,38 @@
<?php
use A7kz\Platform\Models\UniModel;
use A7kz\Platform\Modules\Platform\Core\Facades\Core;
use A7kz\Platform\Modules\Platform\Segment\Facades\Segment;
use Carbon\Carbon;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use \A7kz\Platform\Commands\InstallScript;
return new class extends \A7kz\Platform\Commands\InstallScript {
public function install($module_name, $module_version)
{
}
public function update($module_name, $module_version): void
{
$this->upgrade();
}
private function upgrade(): void
{
$segments = Segment::listActive();
foreach ($segments as $segment) {
if (!Schema::connection($segment->connector)->hasTable('pipi_address')) {
Schema::connection($segment->connector)->create('pipi_address', static function (Blueprint $table) {
$table->id();
$table->string('name')->nullable()->comment('Наименование');
$table->timestamps();
$table->softDeletes();
});
}
}
}
};

View File

@ -7,6 +7,19 @@ enum ApplicationStatus: string
case pending = 'pending';
case approved = 'approved';
case rejected = 'rejected';
case reserved = 'reserved'; //статус брони
case completed = 'completed';
case review = 'review';
function getName(): string
{
return match ($this) {
self::pending => 'В обработке',
self::approved => 'Бронь',
self::rejected => 'Отклонена',
self::completed => 'Завершена',
self::review => 'На рассмотрении',
};
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Modules\applications\Logic;
use A7kz\Platform\Models\UniModel;
use A7kz\Platform\Modules\Platform\Acl\Facades\Acl;
use A7kz\Platform\Modules\Platform\Core\Services\Base\Logic;
use App\Http\Notifications\TelegramNotification;
use App\Models\User;
use App\Modules\applications\Enum\ApplicationStatus;
use App\Modules\auto\Enums\AutoStatusEnums;
use Carbon\CarbonPeriod;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
class Approve extends Logic
{
public function run()
{
$user = new User();
if (!isset($this->row->user_id)) {
$checkUser = UniModel::model('core_users')->where('email', $this->row->user_email)->first();
if (isset($checkUser)) {
$this->row->user_id = $checkUser->id;
$this->row->save();
} else {
$password = Str::random(8);
$user->forceFill([
'name' => $this->row->user_name . ' ' . $this->row->surname_name,
'username' => $this->row->user_email,
'email' => $this->row->user_email,
'phone' => $this->row->user_phone,
'password' => Hash::make($password),
'active' => true
]);
$user->save();
$this->row->user_id = $user->id;
$this->row->save();
$userRole = UniModel::model('core_roles')
->where('alias', 'user')
->first();
if (!empty($userRole)) {
$userRoleModel = UniModel::model('core_user_roles');
$userRoleModel->forceFill([
'user_id' => $user->id,
'role_id' => $userRole->id
]);
$userRoleModel->save();
}
$company = UniModel::model(config('platform.company.tables.company'))
->firstOrCreate([
'name' => $this->row->user_name . ' ' . $this->row->surname_name,
'biniin' => $this->row->biniin ?? '',
'fullname' => $this->row->user_name . ' ' . $this->row->surname_name,
'segment' => 'sol'
]);
$ucr = UniModel::model(config('platform.company.tables.company_user_role'))
->firstOrCreate([
'user_id' => $user->id,
'role_id' => 7,
'company_id' => $company->id
]);
if (!$ucr) {
UniModel::model(config('platform.company.tables.company_user_role'))
->create([
'user_id' => $user->id,
'role_id' => 6,
'company_id' => $company->id
]);
}
UniModel::model(config('platform.company.tables.company_user'))
->firstOrCreate([
'company_id' => $company->id,
'user_id' => $user->id,
'default' => true,
]);
$text = [
'Сообщение' => __('Пользователь создан'),
'Логин' => 'Логин: ' . $user->email,
'Пароль' => 'Пароль: ' . $password
];
Notification::send([$text], new TelegramNotification($text));
}
}
$this->fillCalendar($this->row);
$this->row->status = ApplicationStatus::approved->value;
$this->row->save();
return $this->response();
}
private function fillCalendar($row): void
{
$period = CarbonPeriod::create($row->started_at, $row->ended_at);
$dates = array_map(fn($date) => $date->toDateString(), iterator_to_array($period));
foreach ($dates as $date) {
UniModel::model('pipi_auto_calendar')->create([
'auto_id' => $row->car_id,
'date' => $date,
'status' => AutoStatusEnums::Rent->name
]);
}
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Modules\applications\Logic;
use A7kz\Platform\Models\UniModel;
use A7kz\Platform\Modules\Platform\Acl\Facades\Acl;
use A7kz\Platform\Modules\Platform\Core\Services\Base\Logic;
use App\Models\User;
use App\Modules\applications\Enum\ApplicationStatus;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
class Cancel extends Logic
{
public function run()
{
if (Carbon::now() > $this->row->ended_at) {
$this->message = 'Вы не можете завершить заявку после даты окончания аренды';
return $this->response();
}
$car = $this->row->car_id;
if (isset($car)) {
UniModel::model('pipi_auto_calendar')
->where('date', '>=', Carbon::now()->toDateString())
->where('date', '<=', $this->row->ended_at)
->where('auto_id', $car)->delete();
}
$this->row->status = ApplicationStatus::rejected->value;
$this->row->ended_at = Carbon::now();
$this->row->save();
return $this->response();
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Modules\applications\Logic;
use A7kz\Platform\Models\UniModel;
use A7kz\Platform\Modules\Platform\Acl\Facades\Acl;
use A7kz\Platform\Modules\Platform\Core\Services\Base\Logic;
use App\Models\User;
use App\Modules\applications\Enum\ApplicationStatus;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
class Complete extends Logic
{
public function run()
{
$this->row->status = ApplicationStatus::completed->value;
$this->row->save();
return $this->response();
}
}

View File

@ -35,9 +35,6 @@
"phone": {
"type": "string"
},
"status": {
"type": "string"
},
"user_name": {
"type": "string"
},
@ -47,6 +44,12 @@
"user_email": {
"type": "string"
},
"sum": {
"type": "int"
},
"deposit": {
"type": "int"
},
"started_at": {
"type": "date",
"validation": "required|date"
@ -58,6 +61,25 @@
"photos": {
"type": "json",
"validation": "nullable"
},
"address_start": {
"type": "foreign",
"table": "pipi_address",
"foreign": "id",
"display": [
"name"
],
"validation": "required|integer"
},
"address_end": {
"type": "foreign",
"table": "pipi_address",
"alias": "address_end",
"foreign": "id",
"display": [
"name"
],
"validation": "required|integer"
}
}
},
@ -70,19 +92,17 @@
{ "name": "car_id", "caption": "Машина" },
{ "name": "user_id", "caption": "Пользователь" },
{ "name": "phone", "caption": "Телефон" },
{ "name": "status", "caption": "Статус" },
{ "name": "user_name", "caption": "Имя" },
{ "name": "user_surname", "caption": "Фамилия" },
{ "name": "user_email", "caption": "Email" },
{ "name": "started_at", "caption": "Начало" },
{ "name": "ended_at", "caption": "Окончания" }
{ "name": "ended_at", "caption": "Дата окончания" }
],
"action": {
"head": [
"add"
],
"row": [
"edit",
"delete"
]
},
@ -119,8 +139,7 @@
},
{
"cols": [
{ "size": 6, "input": { "name": "phone", "label": "Телефон" } },
{ "size": 6, "input": { "name": "status", "label": "Статус" } }
{ "size": 6, "input": { "name": "phone", "label": "Телефон" } }
]
},
{
@ -137,7 +156,15 @@
},
{
"cols": [
{ "size": 6, "input": { "name": "ended_at", "label": "Окончания" } },
{ "size": 3, "input": { "name": "address_start", "label": "Адрес получения" } },
{ "size": 3, "input": { "name": "address_end", "label": "Конечный адрес" } },
{ "size": 3, "input": { "name": "sum", "label": "Сумма" } },
{ "size": 3, "input": { "name": "deposit", "label": "Депозит" } }
]
},
{
"cols": [
{ "size": 6, "input": { "name": "ended_at", "label": "Дата окончания" } },
{ "size": 6, "input": { "name": "photos", "label": "Фотографии", "type": "files"} }
]
}
@ -158,30 +185,103 @@
},
{
"cols": [
{ "size": 6, "input": { "name": "phone", "label": "Телефон" } },
{ "size": 6, "input": { "name": "status", "label": "Статус" } }
{ "size": 4, "input": { "name": "phone", "label": "Телефон" } },
{ "size": 4, "input": { "name": "user_name", "label": "Имя" } },
{ "size": 4, "input": { "name": "user_surname", "label": "Фамилия" } }
]
},
{
"cols": [
{ "size": 6, "input": { "name": "user_name", "label": "Имя" } },
{ "size": 6, "input": { "name": "user_surname", "label": "Фамилия" } }
{ "size": 4, "input": { "name": "user_email", "label": "Email" } },
{ "size": 4, "input": { "name": "started_at", "label": "Начало" } },
{ "size": 4, "input": { "name": "ended_at", "label": "Дата окончания" } }
]
},
{
"cols": [
{ "size": 6, "input": { "name": "user_email", "label": "Email" } },
{ "size": 6, "input": { "name": "started_at", "label": "Начало" } }
{ "size": 3, "input": { "name": "address_start", "label": "Адрес получения" } },
{ "size": 3, "input": { "name": "address_end", "label": "Конечный адрес" } },
{ "size": 3, "input": { "name": "sum", "label": "Сумма" } },
{ "size": 3, "input": { "name": "deposit", "label": "Депозит" } }
]
},
{
"cols": [
{ "size": 6, "input": { "name": "ended_at", "label": "Окончания" } },
{ "size": 6, "input": { "name": "photos", "label": "Фотографии", "type": "files"} }
]
}
],
"submits": "struct:crud.form.edit.submits"
"submits": [
"struct:crud.form.submit.save",
"struct:crud.form.submit.close"
]
}
},
"show": {
"title": "Заявки",
"template": "app.base.crud.form",
"component": "App.components.Show",
"form": {
"rows": [
{
"cols": [
{ "size": 6, "input": { "name": "car_id", "label": "Машина" } },
{ "size": 6, "input": { "name": "user_id", "label": "Пользователь" } }
]
},
{
"cols": [
{ "size": 4, "input": { "name": "phone", "label": "Телефон" } },
{ "size": 4, "input": { "name": "user_name", "label": "Имя" } },
{ "size": 4, "input": { "name": "user_surname", "label": "Фамилия" } }
]
},
{
"cols": [
{ "size": 4, "input": { "name": "user_email", "label": "Email" } },
{ "size": 4, "input": { "name": "started_at", "label": "Начало" } },
{ "size": 4, "input": { "name": "ended_at", "label": "Дата окончания" } }
]
},
{
"cols": [
{ "size": 3, "input": { "name": "address_start", "label": "Адрес получения" } },
{ "size": 3, "input": { "name": "address_end", "label": "Конечный адрес" } },
{ "size": 3, "input": { "name": "sum", "label": "Сумма" } },
{ "size": 3, "input": { "name": "deposit", "label": "Депозит" } }
]
},
{
"cols": [
{ "size": 6, "input": { "name": "photos", "label": "Фотографии", "type": "files"} }
]
}
],
"submits": [
{
"type": "logic",
"name": "App.Modules.applications.Logic.Approve",
"label": "Подтвердить заявку",
"btn": "btn btn-success",
"condition": "status,==,pending"
},
{
"type": "logic",
"name": "App.Modules.applications.Logic.Cancel",
"label": "Отменить заявку",
"btn": "btn btn-danger",
"condition": "status,==,approved"
},
{
"type": "logic",
"name": "App.Modules.applications.Logic.Complete",
"label": "Завершить заявку",
"btn": "btn btn-success",
"condition": "status,==,review"
},
"struct:crud.form.submit.save",
"struct:crud.form.submit.close"
]
}
}
}

View File

@ -48,6 +48,25 @@ return new class extends \A7kz\Platform\Commands\InstallScript {
$table->json('photos')->nullable();
});
}
if (!Schema::connection($segment->connector)->hasColumn('pipi_applications', 'address_start')) {
Schema::connection($segment->connector)->table('pipi_applications', static function (Blueprint $table) {
$table->unsignedBigInteger('address_end')->nullable();
$table->unsignedBigInteger('address_start')->nullable();
});
}
if (!Schema::connection($segment->connector)->hasColumn('pipi_applications', 'sum')) {
Schema::connection($segment->connector)->table('pipi_applications', static function (Blueprint $table) {
$table->integer('sum')->nullable();
});
}
if (!Schema::connection($segment->connector)->hasColumn('pipi_applications', 'deposit')) {
Schema::connection($segment->connector)->table('pipi_applications', static function (Blueprint $table) {
$table->integer('deposit')->nullable();
});
}
}
}
};

View File

@ -153,6 +153,14 @@
"photo_id": {
"type": "int",
"validation": "nullable|int"
},
"regular_user": {
"type": "foreign",
"table": "core_users",
"foreign": "id",
"display": ["username"],
"nullable": true,
"comment": "Постоянный клиент"
}
}
},
@ -301,8 +309,8 @@
{
"size": 4,
"input": {
"name": "owner_id",
"label": "Владелец"
"name": "regular_user",
"label": "Постоянный клиент"
}
},
{
@ -330,21 +338,24 @@
"size": 4,
"input": {
"name": "code",
"label": "Код"
"label": "Код",
"readonly": true
}
},
{
"size": 4,
"input": {
"name": "name",
"label": "Наименование"
"label": "Наименование",
"readonly": true
}
},
{
"size": 4,
"input": {
"name": "state_number",
"label": "Госномер"
"label": "Госномер",
"readonly": true
}
}
]
@ -355,21 +366,24 @@
"size": 4,
"input": {
"name": "brand_id",
"label": "Марка"
"label": "Марка",
"readonly": true
}
},
{
"size": 4,
"input": {
"name": "model_id",
"label": "Модель"
"label": "Модель",
"readonly": true
}
},
{
"size": 4,
"input": {
"name": "color_id",
"label": "Цвет"
"label": "Цвет",
"readonly": true
}
}
]
@ -380,21 +394,23 @@
"size": 4,
"input": {
"name": "manufacture_year",
"label": "Год производства"
"label": "Год производства",
"readonly": true
}
},
{
"size": 4,
"input": {
"name": "estimated_cost",
"label": "Оценочная стоимость"
"label": "Оценочная стоимость",
"readonly": true
}
},
{
"size": 4,
"input": {
"name": "owner_id",
"label": "Владелец"
"name": "regular_user",
"label": "Постоянный клиент"
}
},
{

View File

@ -145,6 +145,12 @@ return new class extends \A7kz\Platform\Commands\InstallScript {
$table->unsignedBigInteger('photo_id')->nullable();
});
}
if (!Schema::connection($segment->connector)->hasColumn('pipi_auto', 'regular_user')) {
Schema::connection($segment->connector)->table('pipi_auto', static function (Blueprint $table) {
$table->unsignedBigInteger('regular_user')->nullable();
});
}
}
}

9
modules/faq/access.json Normal file
View File

@ -0,0 +1,9 @@
{
"admin": [
"default",
"add",
"show",
"edit",
"delete"
]
}

149
modules/faq/app.json Normal file
View File

@ -0,0 +1,149 @@
{
"module": "pipicar",
"name": "pipicar.faq",
"type": "crud",
"title": "FAQ",
"withHeader": false,
"data": {
"table": "pipi_faq",
"pk": "id",
"limit": 25,
"segment": true,
"timestamp": false,
"fields": {
"id": {
"type": "pk"
},
"text": {
"type": "text"
},
"category": {
"type": "string"
},
"subcategory": {
"type": "string"
}
}
},
"ui": {
"grid": {
"title": "FAQ",
"component": "App.components.Grid",
"cols": [
{
"name": "category",
"caption": "Категория"
},
{
"name": "subcategory",
"caption": "Подкатегория"
}
],
"action": {
"head": [
"add"
],
"row": [
"edit",
"delete"
]
},
"filter": {
"template": "app.base.crud.filter",
"rows": [
{
"cols": [
{
"size": 6,
"input": {
"name": "category",
"label": "Категория"
}
},
{
"size": 6,
"input": {
"name": "subcategory",
"label": "Подкатегория"
}
}
]
}
]
}
},
"forms": {
"add": {
"title": "FAQ",
"template": "app.base.crud.form",
"component": "App.components.Show",
"form": {
"submits": "struct:crud.form.edit.submits",
"rows": [
{
"cols": [
{
"size": 6,
"input": {
"name": "category",
"label": "Категория"
}
},
{
"size": 6,
"input": {
"name": "subcategory",
"label": "Подкатегория"
}
},
{
"size": 12,
"input": {
"name": "text",
"label": "Текст"
}
}
]
}
]
}
},
"edit": {
"title": "Редактирование типа",
"template": "app.base.crud.form",
"component": "App.components.Show",
"form": {
"rows": [
{
"cols": [
{
"size": 6,
"input": {
"name": "category",
"label": "Категория"
}
},
{
"size": 6,
"input": {
"name": "subcategory",
"label": "Подкатегория"
}
},
{
"size": 12,
"input": {
"name": "text",
"label": "Текст"
}
}
]
}
],
"submits": "struct:crud.form.edit.submits"
}
}
}
},
"actions": "struct:crud.actions"
}

25
modules/faq/migrate.php Normal file
View File

@ -0,0 +1,25 @@
<?php
use A7kz\Platform\Modules\Platform\Segment\Facades\Segment;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
$segments = Segment::listActive();
foreach ($segments as $segment) {
Schema::connection($segment->connector)->create('pipi_faq', static function (Blueprint $table) {
$table->id();
$table->longText('text')->nullable()->comment('Текст');
$table->string('category')->nullable()->comment('Категория');
$table->string('subcategory')->nullable()->comment('Подкатегория');
$table->timestamps();
$table->softDeletes();
});
}
}
};

40
modules/faq/script.php Normal file
View File

@ -0,0 +1,40 @@
<?php
use A7kz\Platform\Models\UniModel;
use A7kz\Platform\Modules\Platform\Core\Facades\Core;
use A7kz\Platform\Modules\Platform\Segment\Facades\Segment;
use Carbon\Carbon;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use \A7kz\Platform\Commands\InstallScript;
return new class extends \A7kz\Platform\Commands\InstallScript {
public function install($module_name, $module_version)
{
}
public function update($module_name, $module_version): void
{
$this->upgrade();
}
private function upgrade(): void
{
$segments = Segment::listActive();
foreach ($segments as $segment) {
if (!Schema::connection($segment->connector)->hasTable('pipi_faq')) {
Schema::connection($segment->connector)->create('pipi_faq', static function (Blueprint $table) {
$table->id();
$table->longText('text')->nullable()->comment('Текст');
$table->string('category')->nullable()->comment('Категория');
$table->string('subcategory')->nullable()->comment('Подкатегория');
$table->timestamps();
$table->softDeletes();
});
}
}
}
};

View File

@ -9,6 +9,7 @@
namespace App\Modules\main\Components;
use A7kz\Platform\Models\UniModel;
use A7kz\Platform\Modules\Platform\Core\Services\Base\Component;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Carbon;
@ -21,21 +22,11 @@ class Main extends Component
parent::__construct($params, $module);
$this->template = 'pipicar::main.views.main';
$class_filters = Request::input('class_filters') ?? null;
$started_at = Request::input('started_at') ?? null;
$ended_at = Request::input('ended_at') ?? null;
$bodywork_filters = Request::input('bodywork_filters') ?? null;
$request = request();
$request = new \Illuminate\Http\Request([
'started_at' => $started_at ?? Carbon::now()->toDateString(),
'ended_at' => $ended_at ?? Carbon::now()->addDays(3)->toDateString(),
'class' => $class_filters ?? null,
'bodywork' => $bodywork_filters ?? null,
]);
$response = $this->getAuto($request);
$response = (new \App\Http\Controllers\MobileApiController)->getAvailableMarksList($request);
$this->params['cars'] = collect($response->getData(true))->filter(function ($item) {
$this->params['cars'] = collect($response)->filter(function ($item) {
return !empty($item['tariffs']);
})->values();
@ -50,4 +41,142 @@ class Main extends Component
// return !empty($item['tariffs']);
// })->values();
}
private function getAuto($request)
{
$started_at = $request->query('started_at');
$ended_at = $request->query('ended_at');
$class = $request->query('class');
$bodywork = $request->query('bodywork');
$marks = UniModel::model('pipi_brand_models')->get();
$availableMarks = [];
if ($class) {
$class_id = UniModel::model('pipi_auto_classes')->where('name', $class)->first()?->id;
$marks = $marks->where('class_id', $class_id);
}
if ($bodywork) {
$bodywork_id = UniModel::model('pipi_auto_bodywork')->where('name', $bodywork)->first()?->id;
$marks = $marks->where('bodywork_id', $bodywork_id);
}
foreach ($marks as $mark) {
if ($mark->name === 'Ввод остатков') {
continue;
}
$tariffs = UniModel::model('pipi_auto_tariffs')->where('model_id', $mark->id)->get();
if ($tariffs->isEmpty()) {
continue;
}
$cars = UniModel::model('pipi_auto')->where('model_id', $mark->id)->get();
if ($cars->isEmpty()) {
continue;
}
$brand = UniModel::model('pipi_auto_brands')->find($mark->brand_id);
$tariffs = UniModel::model('pipi_auto_tariffs')->where('model_id', $mark->id)->get();
$equipment = UniModel::model('pipi_auto_equipment')->where('id', $mark->equipment_id)->first();
$class = UniModel::model('pipi_auto_classes')->find($mark->class_id);
$bodywork = UniModel::model('pipi_auto_bodywork')->find($mark->bodywork_id);
$photo = UniModel::model('core_files')->find($mark?->photo_id);
$path = null;
if ($photo) {
$path = url('api/files/file/' . ltrim($photo->path, '/'));
}
$tariffs_new = [];
foreach ($tariffs as $tariff) {
$tariffs_new[] = [
'name' => $tariff?->name,
'price' => $tariff?->base_rate,
'min' => $tariff?->day_range_start,
'max' => $tariff?->day_range_end,
'deposit' => $tariff?->deposit,
];
}
$carsByColor = $cars->groupBy('color_id');
foreach ($carsByColor as $carColor => $carsOfColor) {
$carColor = UniModel::model('pipi_auto_colors')->find($carColor);
$colorPath = $path;
if ($carsOfColor->first()->photo_id) {
$photo = UniModel::model('core_files')->find($carsOfColor->first()->photo_id);
$colorPath = url('api/files/file/' . ltrim($photo->path, '/'));
}
$key = $mark->name . '-' . $mark->year . '-' . $carColor->code;
$availableMarks[$key] = [
'id' => $mark->id,
'brand' => $brand?->name,
'mark' => $mark->name,
'year' => $mark->year,
'color' => $carColor->code,
'configuration' => $equipment?->name,
'people' => $mark?->people ?? '5',
'actuator' => $mark?->actuator ?? 'Передний',
'fuel_type' => $mark?->fuel_type ?? 'АКПП',
'hp' => $mark?->hp ?? '1.6',
'engine_capacity' => $mark?->engine_capacity ?? '1591',
'fuel_tank' => $mark?->fuel_tank ?? '50',
'class' => $class->name ?? 'Эконом',
'bodywork' => $bodywork->name ?? 'Седан',
'deposit' => $tariffs_new[0]['deposit'] ?? 30000,
'conditioner' => $mark?->conditioner,
'photo' => $colorPath,
'free' => $this->checkAvailableCar($started_at, $ended_at, $mark->id, $carColor),
'tariffs' => $tariffs_new,
];
}
}
if (empty($availableMarks)) {
$availableMarks = (object) [];
}
return $availableMarks;
}
private function checkAvailableCar($started_at = null, $ended_at = null, $modelId = null, $color = null): bool {
if (!$started_at || !$ended_at || !$modelId) {
$request = request();
$started_at = $started_at ?? $request->input('started_at');
$ended_at = $ended_at ?? $request->input('ended_at');
$modelId = $modelId ?? $request->input('model_id');
}
if (is_null($started_at) or is_null($ended_at)) {
$started_at = Carbon::now()->format('Y-m-d');
$ended_at = Carbon::now()->addDays(3)->format('Y-m-d');
}
$started_at = Carbon::parse($started_at)->format('Y-m-d');
$ended_at = Carbon::parse($ended_at)->format('Y-m-d');
$cars = UniModel::model('pipi_auto')
->where('is_inactive', '=', false)
->where('model_id', $modelId)
->where('color_id', $color->id)
->get();
$busyCars = UniModel::model('pipi_auto_calendar')
->whereBetween('date', [$started_at, $ended_at])
->pluck('auto_id')
->toArray();
$availableCars = $cars->reject(fn($car) => in_array($car->id, $busyCars));
return $availableCars->isNotEmpty();
}
public function action_rent()
{
$request = Request::input();
dd($request);
}
}

View File

@ -1,16 +1,18 @@
{
"admin": [
"default",
"add",
"show",
"edit",
"delete"
],
"director": [
"default",
"add",
"show",
"edit",
"delete"
]
}
"admin": [
"default",
"add",
"show",
"edit",
"delete",
"rent"
],
"user": [
"default",
"add",
"show",
"edit",
"delete",
"rent"
]
}

View File

@ -1,7 +1,10 @@
@php $request = \Illuminate\Support\Facades\Request::input();
$address = \A7kz\Platform\Models\UniModel::model('pipi_address')->get();
@endphp
<div class="container text-center main-page">
<div class="main-page-header">
<p class="top-title">Доступные автомобили</p>
<p class="title">Выберите лучший автомобиль</p>
<p class="top-title">@lang('Доступные автомобили')</p>
<p class="title">@lang('Выберите лучший автомобиль')</p>
</div>
<div class="container my-4">
<div class="row">
@ -115,11 +118,10 @@
<div class="row custom-row w-100">
@php $carsValues = array_values($cars->toArray()); @endphp
@foreach($carsValues as $car)
{{-- @dump($car)--}}
<div class="col-12 col-md-6 mb-4">
<div class="car-card flex-grow-1">
<img class="img-fluid" src="{{ $car['photo'] ?? asset('img/default.png') }}" alt="Car Image">
<div class="title">{{ $car['brand'] . ' ' . $car['mark'] . ' - ' . $car['year'] }}</div>
<div class="title">{{ $car['brand'] . ' ' . $car['mark'] . ' - ' . $car['year']. ' ' . \A7kz\Platform\Models\UniModel::model('pipi_auto_colors')->where('code', $car['color'])->first()?->name }}</div>
<div class="card-car-chars">
<div class="add-li"><i class="material-symbols-outlined">ac_unit</i><p>&nbsp;Кондиционер</p></div>
<div class="add-li"><i class="material-symbols-outlined">auto_transmission</i><p>&nbsp;{{ $car['fuel_type'] }}</p></div>
@ -140,24 +142,20 @@
@endif
</p>
</div>
<a href="#" class="btn btn-primary open-rent-modal"
data-bs-toggle="modal"
data-bs-target="#rentModal"
data-car='@json($car)'
data-photo="{{ $car['photo'] ?? asset('img/default.png') }}"
{{-- data-brand="{{ $car['brand'] }}"--}}
{{-- data-mark="{{ $car['mark'] }}"--}}
{{-- data-year="{{ $car['year'] }}"--}}
{{-- data-fuel="{{ $car['fuel_type'] }}"--}}
{{-- data-people="{{ $car['people'] }}"--}}
{{-- data-tariffWithMin1 ="{{ $tariffWithMin1['price'] ?? '' }}"--}}
{{-- data-tariffWithMin3="{{ $tariffWithMin3['price'] ?? '' }}"--}}
{{-- data-tariffWithMin6="{{ $tariffWithMin6['price'] ?? '' }}"--}}
{{-- data-tariffWithMin15="{{ $tariffWithMin15['price'] ?? '' }}"--}}
>
@lang('Арендовать')
</a>
@if($car['free'])
<a href="#" class="btn btn-primary open-rent-modal"
data-bs-toggle="modal"
data-bs-target="#rentModal"
data-car='@json($car)'
data-photo="{{ $car['photo'] ?? asset('img/default.png') }}"
>
@lang('Арендовать')
</a>
@else
<span class="badge bg-danger text-wrap" style="width: 150px;">
@lang('Машина занята выберете другую дату')
</span>
@endif
</div>
</div>
</div>
@ -173,7 +171,8 @@
<div class="modal-content">
<div class="modal-body">
<div class="row justify-content-center align-items-start">
<form method="post" class="col-12 col-md-7">
<form method="POST" class="col-12 col-md-7" action="{{ $app->getPath() . '/rent' }}">
@csrf
<input hidden="" name="mark_id" id="mark_id" value="">
<input hidden="" name="color" id="color" value="">
<div class="car-card d-flex flex-grow-1 flex-row mb-4">
@ -195,7 +194,7 @@
<div class="row justify-content-center align-items-end">
<div class="col-12 col-md-6">
<label for="days" class="form-label">Срок аренды, дней <b>*</b></label>
<input type="number" name="days" class="form-control" id="days" value="7" required="" min="1" step="1">
<input type="number" name="days" class="form-control" id="days" value="3" required="" min="1" step="1">
</div>
<div class="col-12 col-md-6">
<div class="form-text">
@ -224,10 +223,14 @@
<div class="card-body">
<div class="mb-3">
<label class="form-label">Место и время получения <b>*</b></label>
<input placeholder="Введите адрес получения" type="text" class="form-control" name="pick-up-address" id="pick-up-address" required="">
<select required class="form-select" name="pick-up-address" id="pick-up-address">
@foreach($address as $addres)
<option value="{{ $addres->id }}">{{ $addres->name }}</option>
@endforeach
</select>
<div class="row mt-3">
<div class="col-6">
<input type="date" class="form-control" id="pick-up-date" name="pick-up-date" required="" min="2025-05-02">
<input type="date" class="form-control" id="pick-up-date" name="pick-up-date" required="" min="{{now()->format('Y-m-d')}}">
</div>
<div class="col-6">
<input type="time" class="form-control" name="pick-up-time" id="pick-up-time" value="09:00" required="">
@ -237,7 +240,11 @@
<hr>
<div class="mb-3">
<label class="form-label">Место и время возврата <b>*</b></label>
<input placeholder="Введите адрес возврата" type="text" class="form-control" name="return-address" id="return-address" required="">
<select required class="form-select" id="return-address" name="return-address">
@foreach($address as $addres)
<option value="{{ $addres->id }}">{{ $addres->name }}</option>
@endforeach
</select>
<div class="row mt-3">
<div class="col-6">
<input type="date" class="form-control" id="return-date" name="return-date" disabled="" required="">
@ -250,7 +257,11 @@
</div>
</div>
<div class="mb-4 lst-btn">
<button type="submit" class="btn btn-primary btn-lg">Забронировать сейчас</button>
<button type="submit" class="btn btn-primary btn-lg" id="book-button" style="display: none;">
{{ __('Забронировать сейчас') }}</button>
<div id="not-available-message" class="alert alert-danger" style="display: none;">
{{ __('Машина на эти даты не доступна') }}
</div>
<p class="add-text2">Бесплатная отмена в любое время</p>
</div>
</form>
@ -353,22 +364,34 @@
const returnDateInput = document.getElementById('return-date');
const daysInput = document.getElementById('days');
const today = new Date();
const yyyy = today.getFullYear();
const mm = String(today.getMonth() + 1).padStart(2, '0');
const dd = String(today.getDate()).padStart(2, '0');
const todayStr = `${yyyy}-${mm}-${dd}`;
pickUpDateInput.value = todayStr;
pickUpDateInput.setAttribute('min', todayStr);
const urlParams = new URLSearchParams(window.location.search);
const pickUpDateParam = urlParams.get('started_at');
const returnDateParam = urlParams.get('ended_at');
if (returnDateParam) {
const [day, month, year] = returnDateParam.split('.');
returnDateInput.value = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
}
daysInput.addEventListener('input', updateReturnDate);
pickUpDateInput.addEventListener('change', updateReturnDate);
const today = new Date();
let initialPickUpDate = today.toISOString().split('T')[0];
if (pickUpDateParam) {
const [day, month, year] = pickUpDateParam.split('.');
initialPickUpDate = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
}
pickUpDateInput.value = initialPickUpDate;
daysInput.addEventListener('input', () => updateReturnDate());
pickUpDateInput.addEventListener('change', () => updateReturnDate());
document.querySelectorAll('.open-rent-modal').forEach(button => {
button.addEventListener('click', function () {
const car = JSON.parse(this.getAttribute('data-car'));
modal.setAttribute('data-car', JSON.stringify(car));
daysInput.addEventListener('input', () => checkAvailableCar(car));
pickUpDateInput.addEventListener('change', () => checkAvailableCar(car));
document.getElementById('modalCarImage').src = this.getAttribute('data-photo');
document.getElementById('modalCarTitle').innerText = `${car.brand} ${car.mark} - ${car.year}`;
document.getElementById('modalFuel').innerHTML = `&nbsp;${car.fuel_type}`;
@ -383,11 +406,27 @@
document.getElementById('tariffWithMin16').innerHTML = `&nbsp;${getTariffPrice(16)} ₸`;
document.getElementById('tariffWithMin30').innerHTML = `&nbsp;${(getTariffPrice(1) * 0.6).toFixed(0)} ₸`;
updateReturnDate();
updateReturnDate(true);
checkAvailableCar(car);
});
});
function updateReturnDate() {
function updateReturnDate(isInitialLoad = false) {
if (isInitialLoad && pickUpDateParam && returnDateParam) {
const [day1, month1, year1] = pickUpDateParam.split('.');
const [day2, month2, year2] = returnDateParam.split('.');
const pickUpDate = new Date(`${year1}-${month1}-${day1}`);
const returnDate = new Date(`${year2}-${month2}-${day2}`);
const timeDiff = returnDate - pickUpDate;
const rentalDays = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
if (!isNaN(rentalDays) && rentalDays > 0) {
daysInput.value = rentalDays;
}
}
const pickUpDate = new Date(pickUpDateInput.value);
const rentalDays = parseInt(daysInput.value, 10);
@ -402,6 +441,54 @@
calculateOther(rentalDays);
}
function checkAvailableCar(car) {
const started_at = document.getElementById('pick-up-date').value;
const ended_at = document.getElementById('return-date').value;
const bookButton = document.getElementById('book-button');
const notAvailableMessage = document.getElementById('not-available-message');
bookButton.style.display = 'none';
notAvailableMessage.style.display = 'none';
if (!started_at || !ended_at) {
console.log('Даты не заполнены');
return;
}
fetch('{{ url('/api/mobile/checkAvailableCar') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
},
body: JSON.stringify({
'started_at': started_at,
'ended_at': ended_at,
'model_id': car.id,
'color': car.color
})
})
.then(async response => {
return response.json();
})
.then(result => {
let isAvailable = result.available;
if (isAvailable) {
bookButton.style.display = 'block';
} else {
notAvailableMessage.style.display = 'block';
notAvailableMessage.textContent = 'Машина на эти даты не доступна';
}
})
.catch(error => {
console.error('Ошибка:', error);
notAvailableMessage.style.display = 'block';
notAvailableMessage.textContent = 'Ошибка при проверке доступности';
});
}
function calculateOther(days) {
if (!days || isNaN(days) || days < 1) {
updatePrices(0, 0, 0, 0);

View File

@ -26,6 +26,10 @@ Route::prefix('1c')->group(function () {
Route::prefix('mobile')->group(function () {
Route::get('getMarks', [MobileApiController::class, 'getMarks']);
Route::get('getFAQCategory', [MobileApiController::class, 'getFAQCategory']);
Route::get('getFAQSubCategory', [MobileApiController::class, 'getFAQSubCategory']);
Route::get('getFAQText', [MobileApiController::class, 'getFAQText']);
Route::get('getAddress', [MobileApiController::class, 'getAddress']);
Route::get('getAvailableMarksList', [MobileApiController::class, 'getAvailableMarksList']);
Route::post('checkAvailableCar', [MobileApiController::class, 'checkAvailableCar']);
Route::post('sendApplication', [MobileApiController::class, 'sendApplication']);