notices and tg-bot module created

This commit is contained in:
Thekindbull 2025-05-23 19:37:45 +08:00
parent c2501e8a29
commit 57b1c744b9
40 changed files with 1276 additions and 10 deletions

View File

@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Deal\Deal;
use App\Notifications\UniqueContact;
class NotificationProbeController extends Controller
{
public function index()
{
auth()->user()->notify(new UniqueContact(Deal::find(4)));
echo auth()->user()->unreadNotifications->count() . '<br>';
die(auth()->user()->notifications);
}
}

View File

@ -1,8 +1,11 @@
@php($title = 'Жилые комплексы')
@php($title = 'Жилой комплекс «' . $complex->name . '»')
@php($backUrl = route('admin.complexes'))
@extends('layouts.admin')
@section('content')
<div class="fs-5 bg-light p-0 m-0 border border-1 rounded-4 p-3">
<h4 class="fw-bold my-3">{{ $complex->name }}</h4>
<h4 class="fw-bold my-3">
Редактирование
</h4>
<form action="{{ route('admin.complexes.update', ['complex' => $complex]) }}" method="post"
enctype="multipart/form-data">
@csrf
@ -28,6 +31,8 @@
</div>
<button type="submit" class="btn btn-primary">Сохранить</button>
<a class="btn btn-secondary" href="{{ route('admin.complexes') }}">Отмена</a>
</form>
</div>
@endsection

View File

@ -0,0 +1,14 @@
<?php
return [
'telegram_bot_token' => '8108664710:AAG4sNtP02P0a6ElSHpCypsYV77EZCADwSo',
'telegram_bot_identifier' => 'testnaviomiro38_bot',
'telegram_bot_id' => 1,
'telegram_type' => 'Telegram',
'vk_bot_id' => 2,
'vk_type' => 'Vk',
'vk_group_token' => 'vk1.a.s1l2J2Nri9FukE8m-eIAjq1fgBMXO_O5WR6T-cCoDjetrY9XrVggY2JyhGDv5pcTeJQuX16OxUrcCaTLagjIyp1w1A6CXKstsCPcnIxDm0GATK15360lhQ5SFjT0ciYh809wf22cQbFssHkkQj6shGzhVvJC7rdpjXNmdMyPs2geiaxktWjEVM7-2oIUrcOrs-zeGxKeucJ7_ofstL4g0A',
'vk_group_id' => 227931308,
'vk_group_confirmation' => '1bb6fd31'
];

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('messenger_bots', function (Blueprint $table) {
$table->id();
$table->string('type');
$table->timestamps();
});
// Добавляем запись с type "Telegram"
DB::table('messenger_bots')->insertOrIgnore([
'type' => 'Telegram',
'created_at' => now(),
'updated_at' => now(),
]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('messenger_bots');
}
};

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Добавляем запись с type "Vk"
DB::table('messenger_bots')->insertOrIgnore([
'type' => 'Vk',
'created_at' => now(),
'updated_at' => now(),
]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::table('messenger_bots')->where('type', 'Vk')->delete();
}
};

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_bots', function (Blueprint $table) {
$table->id();
$table->foreignId('bot_id')->constrained(
table: 'messenger_bots',
indexName: 'messenger_bots_bot_id_foreign_key'
)->onDelete('cascade');
$table->foreignId('user_id')->constrained(
table: 'users',
indexName: 'users_user_id_foreign_key'
)->onDelete('cascade');
$table->string('user_bot_id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_bots');
}
};

View File

@ -0,0 +1,28 @@
<?php
namespace Modules\Bot\Http\Controllers;
use App\Modules\Bot\Models\UserBot;
use Illuminate\Support\Facades\Log;
class BotNotificationSender {
public static function routeMessage(int $userId, string $message): void
{
$userBots = UserBot::where('user_id', $userId)->with('bot')->get();
foreach ($userBots as $userBot) {
switch ($userBot->bot->type) {
case config('bot.telegram_type'):
TelegramBot::sendMessage($userBot->user_bot_id, $message);
break;
case config('bot.vk_type'):
VkBot::sendMessage($userBot->user_bot_id, $message);
break;
default:
Log::warning("Unknown bot type: {$userBot->bot->type}");
break;
}
}
}
}

View File

@ -0,0 +1,161 @@
<?php
namespace Modules\Bot\Http\Controllers;
use App\Modules\Bot\Http\Interfaces\MessengerBotInterface;
use App\Models\User;
use App\Modules\Bot\Models\UserBot;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class TelegramBot implements MessengerBotInterface
{
private $botIdentifier;
private $botToken;
public function __construct()
{
$this->botIdentifier = config('bot.telegram_bot_identifier');
$this->botToken = config('bot.telegram_bot_token');
}
public function handleWebhook(Request $request)
{
$update = $request->all();
if (isset($update['message'])) {
$this->handleMessage($update['message']);
} elseif (isset($update['my_chat_member'])) {
$this->handleChatMemberUpdate($update['my_chat_member']);
}
}
private function handleMessage($message)
{
if (isset($message['text'])) {
$text = $message['text'];
$chatId = $message['chat']['id'];
$command = strtolower(explode(' ', $text)[0]);
switch ($command) {
case '/start':
$this->handleStart($text, $chatId);
break;
case '/stop':
$this->handleUnsubscribe($chatId);
break;
}
}
}
private function handleChatMemberUpdate($chatMember)
{
$chatId = $chatMember['chat']['id'];
$newStatus = $chatMember['new_chat_member']['status'];
if ($newStatus === 'kicked' || $newStatus === 'left') {
$this->stopBot($chatId);
}
}
private function handleStart($text, $chatId)
{
$parts = explode(' ', $text);
if (count($parts) > 1) {
$encryptedUserId = $parts[1];
try {
$userId = base64_decode($encryptedUserId);
$user = User::find($userId);
if ($user) {
UserBot::updateOrCreate(
['user_id' => $userId, 'bot_id' => config('bot.telegram_bot_id')],
['user_bot_id' => $chatId]
);
$this->sendMessage($chatId, "Здравствуйте, {$user->name}! Вы успешно подписались на уведомления.");
} else {
$this->sendMessage($chatId, "Извините, не удалось найти вашу учетную запись.");
}
} catch (\Exception $e) {
Log::error('Error decrypting user ID', ['error' => $e->getMessage()]);
$this->sendMessage($chatId, "Произошла ошибка при обработке вашего запроса.");
}
} else {
$this->sendMessage($chatId, "Добро пожаловать! Для подписки на уведомления, пожалуйста, используйте ссылку с нашего сайта.");
}
}
private function stopBot($chatId)
{
try {
$userBot = UserBot::where('user_bot_id', $chatId)
->where('bot_id', config('bot.telegram_bot_id'))
->first();
if ($userBot) {
$userBot->delete();
Log::info('User unsubscribed', ['chatId' => $chatId]);
} else {
Log::info('Unsubscribe attempt for non-subscribed user', ['chatId' => $chatId]);
}
} catch (\Exception $e) {
Log::error('Error during unsubscribe process', ['error' => $e->getMessage(), 'chatId' => $chatId]);
}
}
private function handleUnsubscribe($chatId)
{
try {
$userBot = UserBot::where('user_bot_id', $chatId)
->where('bot_id', config('bot.telegram_bot_id'))
->first();
if ($userBot) {
$userBot->delete();
$this->sendMessage($chatId, "Вы успешно отписались от уведомлений. Если захотите подписаться снова, воспользуйтесь ссылкой на нашем сайте.");
Log::info('User unsubscribed', ['chatId' => $chatId]);
} else {
$this->sendMessage($chatId, "Вы не были подписаны на уведомления.");
Log::info('Unsubscribe attempt for non-subscribed user', ['chatId' => $chatId]);
}
} catch (\Exception $e) {
Log::error('Error during unsubscribe process', ['error' => $e->getMessage(), 'chatId' => $chatId]);
$this->sendMessage($chatId, "Произошла ошибка при обработке вашего запроса. Пожалуйста, попробуйте позже.");
}
}
public function subscribe() :mixed
{
$userId = Auth::id();
$encryptedUserId = base64_encode($userId);
$telegramUrl = "https://t.me/$this->botIdentifier?start=$encryptedUserId";
return redirect($telegramUrl);
}
public static function sendMessage(string $userBotId, string $message): void
{
$botToken = config('bot.telegram_bot_token');
Http::withoutVerifying()
->post("https://api.telegram.org/bot{$botToken}/sendMessage", [
'chat_id' => $userBotId,
'text' => $message,
'parse_mode' => 'HTML'
]);
}
public function setHook()
{
$response = Http::withoutVerifying()
->post("https://api.telegram.org/bot{$this->botToken}/setWebhook", [
'url' => 'https://naviom.iro38.ru/api/telegramWebhook',
'allowed_updates' => ['message', 'my_chat_member']
])->json();
dd($response);
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace Modules\Bot\Http\Controllers;
use App\Models\User;
use App\Modules\Bot\Http\Interfaces\MessengerBotInterface;
use App\Modules\Bot\Models\UserBot;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class VkBot implements MessengerBotInterface
{
private int $botId;
private int $groupId;
private string $groupConfirmation;
public function __construct()
{
$this->botId = config('bot.vk_bot_id');
$this->groupId = config('bot.vk_group_id');
$this->groupConfirmation = config('bot.vk_group_confirmation');
}
public static function sendMessage(string $userBotId, string $message): void
{
Log::info('Попытка отправить сообщение');
$params['access_token'] = config('bot.vk_group_token');
$params['v'] = '5.199';
$params['random_id'] = random_int(1, PHP_INT_MAX);
$params['user_id'] = $userBotId;
$params['message'] = $message;
$response = Http::withoutVerifying()->withQueryParameters($params)->post("https://api.vk.com/method/messages.send");
}
public function allowMessages(Request $request)
{
if ($request->vkUserId and $request->naviomUserId and User::find($request->naviomUserId)) {
UserBot::updateOrCreate(
['user_id' => $request->naviomUserId, 'bot_id' => $this->botId],
['user_bot_id' => $request->vkUserId]
);
$user = User::find($request->naviomUserId);
$this->sendMessage($request->vkUserId, "Здравствуйте, {$user->name}! Вы успешно подписались на уведомления.");
}
return response()->json(['success' => true]);
}
public function handleVkCallback(Request $request)
{
switch ($request->type) {
case 'confirmation':
return $this->handleConfirmation($request->group_id);
case 'message_deny':
return $this->handleDenyMessage($request->object['user_id']);
}
}
private function handleConfirmation(int $groupId): string
{
if ($groupId == $this->groupId) {
return $this->groupConfirmation;
}
}
private function handleDenyMessage(int $vkUserId): string
{
$subscription = UserBot::where('user_bot_id', $vkUserId)->where('bot_id', $this->botId)->first();
if ($subscription) {
$subscription->delete();
}
return 'ok';
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Modules\Bot\Http\Interfaces;
interface MessengerBotInterface {
public static function sendMessage(string $userBotId, string $message): void;
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Modules\Bot\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Bot extends Model
{
use HasFactory;
protected $fillable = ['name'];
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Modules\Bot\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class MessengerBot extends Model
{
use HasFactory;
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Modules\Bot\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UserBot extends Model
{
use HasFactory;
protected $fillable = [
'bot_id',
'user_id',
'user_bot_id'
];
public function bot()
{
return $this->belongsTo(MessengerBot::class);
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Modules\Bot\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Blade;
use Livewire\Livewire;
class ModuleServiceProvider extends ServiceProvider
{
protected String $moduleName = 'Bot';
public function register()
{
$this->app->register(RouteServiceProvider::class);
}
public function boot()
{
$this->registerViews();
$this->registerLivewireViews();
$this->registerMigrations();
$this->registerConfig();
$this->registerComponent();
$this->registerLivewire();
}
protected function registerViews()
{
$moduleViewsPath = __DIR__.'/../Views';
$this->loadViewsFrom(
$moduleViewsPath, strtolower($this->moduleName)
);
}
protected function registerLivewireViews()
{
$moduleViewsPath = __DIR__.'/../Views/livewire';
$this->loadViewsFrom(
$moduleViewsPath, strtolower($this->moduleName)
);
}
protected function registerMigrations()
{
$this->loadMigrationsFrom(
app_path('Modules/'.$this->moduleName.'/Database/Migrations')
);
}
protected function registerConfig()
{
$path = app_path('Modules/'.$this->moduleName.'/Config/config.php');
$this->mergeConfigFrom(
$path, strtolower($this->moduleName)
);
}
protected function registerLivewire()
{
//Livewire::component('<name>', \Modules\<NAME>\Http\Livewire\<NAME>::class);
}
protected function registerComponent()
{
//Blade::component('<name>', \Modules\<NAME>\Http\Components\<NAME>::class);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Modules\Bot\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
public function map()
{
$this->registerWebRoutes();
}
protected function registerWebRoutes()
{
//Add Web Routes with web Guard
Route::middleware('web')
//Set Default Controllers Namespace
->namespace('Modules\\Bot\\Http\\Controllers')
->group(app_path('Modules/Bot/Routes/web.php'));
}
}

View File

@ -0,0 +1,20 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Bot\Http\Controllers\TelegramBot;
use Modules\Bot\Http\Controllers\VkBot;
Route::middleware(['auth'])->group(function() {
Route::get('/telegram/subscribe',[TelegramBot::class, 'subscribe'])->name('telegram.subscribe');
Route::get('/telegramHook', [TelegramBot::class, 'setHook'])->name('setHook');
Route::post('/vk/allowMessages',[VkBot::class, 'allowMessages'])->name('vk.allowMessages');
Route::middleware(['hasAccess'])->group(function() {
/** Routes that need to be protected - Маршруты которые нужно защитить */
});
});

View File

@ -0,0 +1,4 @@
@extends('layouts.app')
@section('content')
<h1> Example views </h1>
@endsection

View File

@ -0,0 +1,5 @@
<?php
return [
];

View File

@ -0,0 +1,5 @@
<?php
return [
'new Recomendation' => 'NewRecomendationNotice',
];

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
/*Schema::create('notifications', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained(
table: 'users', indexName: 'notifications_user_id_foreign_key'
)->onDelete('cascade');
$table->string('action');
$table->string('object_name');
$table->unsignedInteger('object_id');
$table->date('read_at')->nullable();
$table->date('sended_to_email_at')->nullable();
$table->date('disabled_at')->nullable();
$table->timestamps();
});
Schema::table('notifications', function (Blueprint $table) {
$table->unique(['user_id', 'action', 'object_name', 'object_id']);
});*/
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//Schema::dropIfExists('notifications');
}
};

View File

@ -0,0 +1,25 @@
<?php
namespace Modules\Notice\Http\Controllers;
use Modules\Notice\Models\Notice;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\Notice\Models\UnreadMessagesMailer;
class NoticeController extends Controller
{
public function index()
{
return view('notice::index');
}
public function run()
{
$mailer = new UnreadMessagesMailer();
$mailer->run();
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Modules\Notice\Http\Livewire;
use Livewire\Component;
use Illuminate\Notifications\DatabaseNotification;
class UserNotices extends Component
{
public function read(DatabaseNotification $notice)
{
$notice->markAsRead();
}
public function render()
{
$currentDate = date("Y-m-d");
$startDate = date("Y-m-d", strtotime($currentDate . " -2 months"));
return view('notice::user-notifications', [
'notifications' => auth()->user()->notifications
]);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Modules\Notice\Http\Livewire;
use Livewire\Component;
use Modules\Notice\Models\Notice;
use Modules\Chat\Models\ChatMessageRead;
class UserNoticesButton extends Component
{
public function render()
{
$currentDate = date("Y-m-d");
$startDate = date("Y-m-d", strtotime($currentDate . " -2 months"));
return view('notice::user-notices-button', [
'unreadedCount' =>
auth()->user()->unreadNotifications
->count()
]);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Modules\Notice\Models;
use Modules\Notice\Models\Notice;
use App\Models\Meeting\WayMeeting;
class NewWayMeetingNotice
{
public static function getParameters(Notice $notice)
{
$wayMeeting = WayMeeting::find($notice->object_id);
if (!$wayMeeting)
return false;
return [
'id' => $wayMeeting->id,
'name' => $wayMeeting->meeting->name,
'action' => route('way.index', ['way' => $wayMeeting->way->id]),
];
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Modules\Notice\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Notice extends Model
{
protected $table = 'notices';
protected $fillable = [
'user_id',
'action',
'object_name',
'object_id',
'read_at'
];
public function toString()
{
$noticeName = "$this->action $this->object_name";
$noticeClass = __DIR__ . '/' . config('notice.types.' . $noticeName);
if (file_exists($noticeClass . '.php')) {
$noticeClass = 'Modules\\Notice\\Models\\' . config('notice.types.' . $noticeName);
if ($parameters = $noticeClass::getParameters($this)) {
return __("notification.$this->action $this->object_name", $parameters);
};
};
return FALSE;
//
}
public function created_at()
{
return date_format(date_create($this->created_at), env('DATETIME_FORMAT'));
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace Modules\Notice\Models;
use Illuminate\Support\Carbon;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Notification;
use App\Notifications\NoticesMailerNotification;
use Modules\Notice\Models\Notice;
use Modules\Bot\Http\Controllers\BotNotificationSender;
use App\Models\User;
use Illuminate\Support\Facades\Log;
class NoticesMailer
{
use Notifiable;
private $user;
private $notices;
private $messages;
public function __construct()
{
$date = Carbon::now();
//$date->subDays(1);
$date = $date->toDateTimeString();
$latest_notice = Notice::where('created_at', '<=', $date)
->whereNull('sended_to_email_at')
->whereNull('read_at')
->orderBy('created_at', 'desc');
if ($latest_notice = $latest_notice->first()) {
$this->user = User::findOrFail($latest_notice->user_id);
$this->notices = Notice::where('user_id', $this->user->id)
->whereNull('sended_to_email_at')
->whereNull('read_at')
->orderBy('object_name')
->orderBy('created_at')->get();
};
return;
}
public function run()
{
if (!$this->user) {
Log::error('Has no notices for mailing');
return;
};
if ($this->notices->count() == 0) {
return false;
};
$notification = new NoticesMailerNotification($this->notices);
Notification::route('mail', $this->user->email)
->notify($notification);
$botMessage = $notification->buildBotMessage();
BotNotificationSender::routeMessage($this->user->id, $botMessage);
foreach ($this->notices as $notice) {
$notice->sended_to_email_at = now();
$notice->save();
};
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Modules\Notice\Models;
use Illuminate\Support\Carbon;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Notification;
use App\Notifications\NewChatMessageNoticesMailerNotification;
use Modules\Notice\Models\Notice;
use Modules\Chat\Models\Chat;
use Modules\Chat\Models\ChatMessage;
use Modules\Chat\Models\ChatMessageRead;
use App\Models\User;
use Modules\Bot\Http\Controllers\BotNotificationSender;
class UnreadMessagesMailer
{
use Notifiable;
private $user;
private $notices;
private $senders;
public function __construct()
{
$newChatNotice = Notice::whereNull('sended_to_email_at')
->whereNull('read_at')
->where('object_name', 'Chat')
->orderBy('created_at');
if ($newChatNotice = $newChatNotice->first()) {
$this->user = User::findOrFail($newChatNotice->user_id);
$this->notices = Notice::where('user_id', $this->user->id)
->whereNull('sended_to_email_at')
->whereNull('read_at')
->where('object_name', 'Chat')
->orderBy('created_at')->get();
//$this->senders = ChatMessageRead::where('receiver_id', $this->user->id)->where('message.sender_id');
};
return;
}
public function run()
{
if (!$this->user) {
//Log::error('Has no notices for mailing');
return;
};
if ($this->notices->count() == 0) {
return false;
};
$notification = new NewChatMessageNoticesMailerNotification();
Notification::route('mail', $this->user->email)
->notify($notification);
$botMessage = $notification->buildBotMessage();
BotNotificationSender::routeMessage($this->user->id, $botMessage);
foreach ($this->notices as $notice) {
$notice->sended_to_email_at = now();
$notice->save();
};
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace Modules\Notice\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Blade;
use Livewire\Livewire;
class ModuleServiceProvider extends ServiceProvider
{
protected string $moduleName = 'Notice';
public function register()
{
$this->app->register(RouteServiceProvider::class);
}
public function boot()
{
$this->registerViews();
$this->registerLivewireViews();
$this->registerMigrations();
$this->registerConfig();
$this->registerComponent();
$this->registerLivewire();
}
protected function registerViews()
{
$moduleViewsPath = __DIR__ . '/../Views';
$this->loadViewsFrom(
$moduleViewsPath,
strtolower($this->moduleName)
);
}
protected function registerLivewireViews()
{
$moduleViewsPath = __DIR__ . '/../Views/livewire';
$this->loadViewsFrom(
$moduleViewsPath,
strtolower($this->moduleName)
);
}
protected function registerMigrations()
{
$this->loadMigrationsFrom(
app_path('Modules/' . $this->moduleName . '/Database/Migrations')
);
}
protected function registerConfig()
{
$path = app_path('Modules/' . $this->moduleName . '/Config/config.php');
$this->mergeConfigFrom(
$path,
strtolower($this->moduleName)
);
$path = app_path('Modules/' . $this->moduleName . '/Config/notices.php');
$this->mergeConfigFrom(
$path,
'notice.types'
);
}
protected function registerLivewire()
{
Livewire::component('notices.user-notices', \Modules\Notice\Http\Livewire\UserNotices::class);
Livewire::component('notices.user-notices-button', \Modules\Notice\Http\Livewire\UserNoticesButton::class);
}
protected function registerComponent()
{
//Blade::component('<name>', \Modules\<NAME>\Http\Components\<NAME>::class);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Modules\Notice\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
public function map()
{
$this->registerWebRoutes();
}
protected function registerWebRoutes()
{
//Add Web Routes with web Guard
Route::middleware('web')
//Set Default Controllers Namespace
->namespace('Modules\\Notice\\Http\\Controllers')
->group(app_path('Modules/Notice/Routes/web.php'));
}
}

View File

@ -0,0 +1,14 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Notice\Http\Controllers\NoticeController;
Route::middleware(['auth'])->group(function() {
Route::get('/notice', [NoticeController::class, 'index']);
Route::get('/notice/run', [NoticeController::class, 'run']);
Route::middleware(['hasAccess'])->group(function() {
/** Routes that need to be protected - Маршруты которые нужно защитить */
});
});

View File

@ -0,0 +1,16 @@
<div>
<div class="offcanvas offcanvas-end rounded-start offcanvas-animated-xl" tabindex="-1" id="notificationsOffcanvas"
aria-labelledby="offcanvasExampleLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvasExampleLabel">Уведомления</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div class="list-group list-group-flush scrollarea">
@livewireStyles
<livewire:notices.user-notices />
@livewireScripts
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,29 @@
<div wire:poll.10000ms>
<div class="d-block">
<a type="button" class="rounded-circle p-2 lh-1 link-secondary bg-primary-soft-hover text-decoration-none p-1"
data-bs-toggle="offcanvas" href="#notificationsOffcanvas">
<div class="position-relative">
<i class="bi bi-bell fs-4"></i>
@if ($unreadedCount > 0)
<span class="notices-badge animation-blink"></span>
@endif
</div>
</a>
</div>
{{-- <div class="d-lg-none d-block">
<a class="nav-link text-center" data-bs-toggle="offcanvas" href="#notificationsOffcanvas">
<div class="position-relative">
<svg xmlns="http://www.w3.org/2000/svg" width="1.5em" height="1.5em" fill="currentColor" class="bi bi-bell" viewBox="0 0 16 16">
<path
d="M8 16a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2M8 1.918l-.797.161A4 4 0 0 0 4 6c0 .628-.134 2.197-.459 3.742-.16.767-.376 1.566-.663 2.258h10.244c-.287-.692-.502-1.49-.663-2.258C12.134 8.197 12 6.628 12 6a4 4 0 0 0-3.203-3.92zM14.22 12c.223.447.481.801.78 1H1c.299-.199.557-.553.78-1C2.68 10.2 3 6.88 3 6c0-2.42 1.72-4.44 4.005-4.901a1 1 0 1 1 1.99 0A5 5 0 0 1 13 6c0 .88.32 4.2 1.22 6"/>
</svg>
<div class="position-absolute start-45 bottom-100">
@if ($unreadedCount > 0 || $unreadedMessages > 0)
<span class="notices-badge animation-blink"></span>
@endif
</div>
</div>
<span class="small d-block text-small-mobile">Уведомления</span>
</a>
</div> --}}
</div>

View File

@ -0,0 +1,38 @@
<div wire:poll.10000ms>
@if ($notifications->count() == 0)
<div class="d-flex justify-content-center align-items-center">
<div class="d-grid gap-1 p-3">
<i class="bi bi-inbox display-5 text-center"></i>
<span class="fs-6 fw-semibold">{{ __('notification.has no notifications') }}</span>
</div>
</div>
@else
<ul class="list-group gap-1">
@foreach ($notifications as $notification)
@if (array_key_exists('text', $notification->data))
<li href="#"
class="list-group-item list-group-item-action rounded {{ !$notification->read_at ? 'list-group-item-warning' : 'list-group-item-light' }}"
aria-current="true" wire:click="read({{ $notification }})">
<div class="small">
<p class="m-0">
{{ $notification->data['text'] }}
</p>
<p class="m-0 d-flex flex-wrap justify-content-end align-items-center">
<span class="badge fw-light text-secondary">{{ $notification->created_at }}</span>
@if ($notification->read_at)
<span class=" fs-5 text-success">
<i class="bi bi-check-all"></i>
</span>
@else
<span class=" fs-5 text-secondary">
<i class="bi bi-check"></i>
</span>
@endif
</p>
</div>
</li>
@endif
@endforeach
</ul>
@endif
</div>

View File

@ -0,0 +1,62 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\Models\Deal\Deal;
use App\Models\Deal\DealStatus;
class UniqueContact extends Notification
{
use Queueable;
private $deal;
/**
* Create a new notification instance.
*/
public function __construct(Deal $deal)
{
if ($deal->status != DealStatus::UNIQUE)
{
//throw new \Exception('Notification sending: deal is not unique');
}
$this->deal = $deal;
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['database'];
}
/**
* Get the mail representation of the notification.
*/
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->line('The introduction to the notification.')
->action('Notification Action', url('/'))
->line('Thank you for using our application!');
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
'deal' => $this->deal->id,
'text' => __('notifications.' . get_class($this), ['contact' => $this->deal->user->name])
];
}
}

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('notifications');
}
};

18
lang/ru/notifications.php Normal file
View File

@ -0,0 +1,18 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'App\Notifications\UniqueContact' => 'Контакт :contact был проверен, уникальность подтверждена',
];

View File

@ -66,4 +66,45 @@ .active>.page-link {
.page-item:not(.active)>.page-link:hover {
color: #e6662a !important;
border-color: #ccc
}
/*NOTOFICATIONS*/
.notices-badge {
position: absolute;
top: 0;
right: 0;
left: 15px;
transform: translate(50%, -50%);
background-color: #d63939 !important
}
.notices-badge:empty {
display: inline-block;
width: .5rem;
height: .5rem;
min-width: 0;
min-height: auto;
padding: 0;
border-radius: 100rem;
vertical-align: baseline
}
.animation-blink {
-webkit-animation: blink 2s infinite;
animation: blink 2s infinite
}
@keyframes blink {
0% {
opacity: 0
}
50% {
opacity: 1
}
to {
opacity: 0
}
}

View File

@ -27,11 +27,21 @@
<img src={{ url('/images/logo.png') }} alt="Logo" width="70">
</a>
</div>
<div class="col px-0 px-md-4 text-start text-truncate fw-light fs-4 text-secondary text-uppercase"
id="pageTitle">
@isset($title)
{{ $title }}
@endisset
<div class="col px-0 px-md-4 text-start" id="pageTitle">
<a class="icon-link icon-link-hover text-truncate fw-lighter fs-4 text-secondary text-uppercase text-decoration-none align-middle"
href="@isset($backUrl) {{ $backUrl }} @endisset"
style="--bs-icon-link-transform: translate3d(-.125rem, 0, 0);">
@isset($backUrl)
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-caret-left" viewBox="0 0 16 16">
<path
d="M10 12.796V3.204L4.519 8zm-.659.753-5.48-4.796a1 1 0 0 1 0-1.506l5.48-4.796A1 1 0 0 1 11 3.204v9.592a1 1 0 0 1-1.659.753" />
</svg>
@endisset
@isset($title)
{{ $title }}
@endisset
</a>
</div>
<div class="col-auto col-sm-2">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"

View File

@ -15,7 +15,7 @@
<link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
@vite(['resources/sass/app.scss', 'resources/js/app.js', 'resources/css/app.css', 'resources/css/docs.css'])
</head>
<body>
@ -47,7 +47,10 @@
</ul>
<!-- Right Side Of Navbar -->
<ul class="navbar-nav ms-auto">
<ul class="navbar-nav align-items-center ms-auto">
<div class="nav-item ">
@livewire('notices.user-notices-button')
</div>
<li class="nav-item dropdown">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button"
data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
@ -118,6 +121,8 @@
</div>
</div>
</div>
@include('notice::index')
@vite(['resources/sass/app.scss', 'resources/js/app.js', 'resources/css/app.css', 'resources/css/docs.css'])
</body>
</html>

View File

@ -59,4 +59,6 @@
Route::get('profile', function ()
{
return view(view: 'user.profile');
});
});
Route::get('/notices', [App\Http\Controllers\NotificationProbeController::class, 'index']);