Compare commits

...

47 Commits

Author SHA1 Message Date
kusowl
b2132f8c02 refactor: chat time, grouping and toast
- show message time
- group messages by date
- show toast when message sent is failed
2026-02-18 11:38:03 +05:30
kusowl
44b13a5ebf fix: disply chats in sidebar, minor ui issues 2026-02-17 11:49:49 +05:30
kusowl
964d7a8936 feature: user can chat to each other 2026-02-16 12:30:57 +05:30
kusowl
12726c93b5 chore: formatting via pint 2026-02-16 12:30:07 +05:30
kusowl
82d61e757f fix: schema so that created_at uses current timestamp 2026-02-16 12:28:06 +05:30
kusowl
59b74a6905 wip: live chat
1.1 User can request for message from explore page
1.2User cannot message broker if not following
1.3 prepare action which handles creating inbox between user and broker if not exists
2026-02-13 12:03:23 +05:30
kusowl
a853b58f48 wip live chat
- add individual user chat view
- add routes
- wip: action to check and get inbox
2026-02-12 12:37:59 +05:30
kusowl
587893511c wip: live support chat
- add migration
- setup models
- generate phpdoc via helper
2026-02-11 12:27:58 +05:30
kusowl
976ea01c89 wip: add laravel reverb and echo
fix env
install the dependecies
2026-02-10 19:15:50 +05:30
kusowl
2f566cc4d8 feature(Live Support): add chat ui
- add chat ui for live support
- fix misc bugs
2026-02-09 19:07:16 +05:30
kusowl
0d0818baf3 feature(send push notification): send push notification to broker's followers 2026-02-09 12:15:11 +05:30
kusowl
82715973dc WIP: push notification when admin approves a deal 2026-02-06 19:01:53 +05:30
kusowl
5cae04884a feature(users can follow a broker)
- add schema and endpoints to make follows relationship with customer
and broker

- show and update states of follow button on ui
2026-02-05 18:19:52 +05:30
kusowl
a06fac4fef feature(users can comment on deals) 2026-02-04 16:50:20 +05:30
kusowl
aa7e2f245f misx bugfixes:
- fix users cannot reset password due session expiration
- add a new mail when users contacts via Contact form
- add list page for all deals in broker dashboard
- fixed links in home page
2026-02-03 16:06:26 +05:30
kusowl
2fecd52d7e feature(send notification for report status): send mail to broker and user when admin resolves/rejects the reports 2026-02-02 19:33:25 +05:30
kusowl
d7c06c38a6 feature(deals by category pie chart):
- add a pie chart that shows deals by category
- change UI to make it more clear
2026-02-02 17:22:01 +05:30
kusowl
1edfd7b9d4 feature(active users graph)
- users active is recorded when users logges in
- add active broker and active customer multi axis line chart
- add filter option of 30 days and 7 days
2026-02-02 16:25:12 +05:30
kusowl
aa8ad6b84b feature(resend otp): users can resend OTP in OTP verification page 2026-01-30 13:37:02 +05:30
kusowl
d03aa31f30 chore: minor UI changes in password reset pages 2026-01-29 18:57:09 +05:30
kusowl
0b631f6049 feature(password reset): send otp via SMS
- use twilio to send otp via test sms
2026-01-29 18:50:53 +05:30
kusowl
039f8f5568 feature(password reset): guests can reset theri passwords via email 2026-01-29 17:32:02 +05:30
kusowl
38d429e5d5 bugfix: improvemnets in UI, misc bugfixes
add tooltip to sidebar buttons
remove profile for admin
fix mobile menu not opening in home page
fix deal image input modal size in mobile view
make image scrollable in input modal
fix explore page filters are not clickable when recent search is maxed
out
change UI for the recent searches
add seeder for categories
improve deal card ui in broker dashboard
2026-01-29 14:02:39 +05:30
kusowl
450c8b4dce chore: add ide helper code
- add ide-helper doc block generator for ide intelisense
2026-01-29 10:37:40 +05:30
kusowl
a8be44553e Merge branch 'feature/admin-panel' 2026-01-29 09:48:25 +05:30
kusowl
0789c21100 feature(deal approval): admin can approve the deals
- admin can see the deal details in modal
- admin can approve or delete the deals
2026-01-28 18:21:48 +05:30
kusowl
2590ebc0ab Merge feature/admin-panel into main 2026-01-28 17:01:34 +05:30
kusowl
6ede0d4548 feature(manage-reported-deals): admin can approve or reject reports and remove content 2026-01-28 16:56:25 +05:30
kusowl
298d73791f feature(manage-reported-deals): admin can approve or reject reports and remove content 2026-01-28 16:42:59 +05:30
kusowl
c087126080 feature(admin-panel): manage broker section
- admin can edit, approve or reject broker registration
- admin can edit, delete or impersonate as broker
2026-01-28 13:44:05 +05:30
kusowl
aa3056e1d1 feature(admin-panel): admin can stop impersonating
- add a alert which shows which user admin is impersonating
- add a logout icon, which stops impersonating and restore admin
dashboard
2026-01-28 10:09:15 +05:30
kusowl
c1ff760282 feature(impersonate as user): add impersonate link in user list 2026-01-27 19:05:24 +05:30
kusowl
a4f644ad20 feature(impersonate as user): Admin can impersonate as others users 2026-01-27 18:54:08 +05:30
kusowl
690a50408d feature(admin-panel): add manage customer page
- list customer
- edit customer details
- delete customer
2026-01-27 17:18:50 +05:30
kusowl
91a11c8f56 feature(admin-panel): add admin dashboard 2026-01-27 17:15:04 +05:30
kusowl
4fd98957cb feature(guest access to explore page)
- guest users can access explore page
- deal interactions are seen to customer and guest users
- guest users redirected to login page for interactions
2026-01-27 13:49:42 +05:30
kusowl
1442856fb4 feature(delete-search-history): users can delere their recent search histories 2026-01-27 12:52:21 +05:30
kusowl
62c306986c fix: empty recent searches, user cannot report deals, category filter is not working with search 2026-01-27 12:05:14 +05:30
kusowl
94ef8f360d feature (favorite and reported deals):
- add favorites and reported tabs in user profile pages
- add remove favorites
- customers can view a deal directly from profiles section and deal modal is shown in explore page
- fix formatting by pint
2026-01-23 16:14:04 +05:30
kusowl
a5f2f43fb1 add shimmer animation in deal modal 2026-01-23 09:45:07 +05:30
kusowl
45196cba56 Merge branch 'feature/broker-dashboard'
# Conflicts:
#	app/Models/User.php
2026-01-22 19:14:15 +05:30
kusowl
4f34c5b595 feature(customer profile): add show and edit page for customer profile 2026-01-22 19:06:59 +05:30
kusowl
193913dfad fix: controller namespaces
- change 'auth' to Auth
- fix use statements to correctly
2026-01-22 17:38:50 +05:30
kusowl
4060866b2d Merge branch 'refs/heads/main' into bugfix/dealhub 2026-01-22 17:34:38 +05:30
kusowl
1d9f3c5c04 refactor: group controllers 2026-01-22 16:41:54 +05:30
kusowl
df4e82a160 refactor: group controllers 2026-01-22 16:41:09 +05:30
kusowl
9e7fda4ea2 feature (stats in broker dashboard):
show individual brokers total likes, view and click per deal andin stats
2026-01-22 16:36:22 +05:30
235 changed files with 39258 additions and 761 deletions

View File

@ -1,4 +1,4 @@
APP_NAME=Laravel
APP_NAME=DealHub
APP_ENV=local
APP_KEY=
APP_DEBUG=true
@ -63,3 +63,28 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
TWILIO_SID=
TWILIO_AUTH_TOKEN=
TWILIO_NUMBER=
# OTP valid time in minutes
OTP_LIFESPAN=10
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
# Same as the VAPID_PUBLIC_KEY
VITE_VAPID_PUBLIC_KEY="${VAPID_PUBLIC_KEY}"
REVERB_APP_ID=
REVERB_APP_KEY=
REVERB_APP_SECRET=
REVERB_HOST=
REVERB_PORT=
REVERB_SCHEME=
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"

2371
.phpstorm.meta.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -79,11 +79,22 @@ ## ⚙️ Installation
```bash
git clone https://git.sentientgeeks.us/joydip.manna/dealhub.git
cd dealhub
# Install dependencies
composer install
npm install
# Set VAPID for webpush
php artisan webpush:vapid
# Install Reverb
php artisan install:broadcasting
npm run dev
php artisan migrate
php artisan serve
# Start Reverb Server ( Another terminal )
php artisan reverb:start
```
---

4
TODO.md Normal file
View File

@ -0,0 +1,4 @@
- [ ] Live Support.
- [ ] Add an Audit Log table to save all actions.
- [ ] Link foreign keys properly with related tables in the database.
- [ ] Add debouncing to the search feature and create proper database indexes to improve search performance.

28340
_ide_helper.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,31 @@
<?php
namespace App\Actions;
use App\Models\Inbox;
use App\Models\User;
use DB;
final readonly class CreateOrGetInboxAction
{
/**
* @throws \Throwable
*/
public function execute(User $recipient, User $sender): Inbox
{
$existingInbox = Inbox::whereHas('users', fn ($q) => $q->where('users.id', $sender->id))
->whereHas('users', fn ($q) => $q->where('users.id', $recipient->id))
->first();
if ($existingInbox) {
return $existingInbox;
}
return DB::transaction(function () use ($sender, $recipient) {
$inbox = Inbox::create();
$inbox->users()->attach([$sender->id, $recipient->id]);
return $inbox;
}, 2);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Actions;
use App\Enums\UserTypes;
use App\Models\Deal;
use App\Models\User;
final readonly class GetAdminStatsAction
{
/**
* @return array<string, int>
*/
public function execute(): array
{
return [
'listings' => Deal::count(),
'customers' => User::where('role', UserTypes::User->value)->count(),
'brokers' => User::where('role', UserTypes::Broker->value)->count(),
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Actions;
use App\Enums\InteractionType;
use App\Models\User;
final readonly class GetBrokerStatsAction
{
/**
* @return array<string, int>
*/
public function execute(User $user): array
{
return [
'listings' => $user->deals()->count(),
'likes' => $user->dealsInteractions()->where('type', InteractionType::Like)->count(),
'views' => $user->dealsInteractions()->where('type', InteractionType::View)->sum('count'),
'clicks' => $user->dealsInteractions()->where('type', InteractionType::Redirection)->sum('count'),
];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Actions;
use App\Enums\InteractionType;
use App\Models\Deal;
use App\Models\User;
use Illuminate\Support\Collection;
final readonly class GetUserFavoritesAction
{
public function execute(User $user): Collection
{
return $user->interactedDeals()
->where('interactions.type', InteractionType::Favorite)
->tap(fn ($q) => (new Deal)->withActiveDeals($q))
->select('deals.id', 'deals.title')
->get();
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Actions;
use App\Models\User;
use Illuminate\Support\Collection;
final readonly class GetUserReportedDealsAction
{
public function execute(User $user): Collection
{
return $user->reports()
->select('reports.id')
->with('deals:id,title')
->get();
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Actions\PasswordReset;
use App\Exceptions\UserNotFoundException;
use App\Models\User;
use App\Services\OTPService;
final readonly class ResendOTPAction
{
public function __construct(
private OTPService $otpService,
private SendOTPToUserAction $otpToUserAction
) {}
/**
* @throws \Throwable
*/
public function execute(): void
{
$user = \Session::get('otp_user_id') ? User::find(\Session::get('otp_user_id')) : null;
throw_if(! $user, new UserNotFoundException('User not found'));
$otp = $this->otpService->generate($user);
$this->otpToUserAction->execute(['user' => $user, 'otp' => $otp]);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Actions\PasswordReset;
use App\Exceptions\UserNotFoundException;
use App\Models\User;
use App\Services\OTPService;
final readonly class SendOTPAction
{
public function __construct(
private OTPService $otpService,
private SendOTPToUserAction $otpToUserAction
) {}
/**
* @throws \Throwable
*/
public function execute(array $data): void
{
$user = User::where('email', $data['email'])->first();
throw_if(! $user, new UserNotFoundException('User not found'));
\Session::put('otp_user_id', $user->id);
$otp = $this->otpService->generate($user);
$this->otpToUserAction->execute(['user' => $user, 'otp' => $otp]);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Actions\PasswordReset;
use App\Actions\SendPasswordResetMailAction;
use App\Services\TwilioService;
use Twilio\Exceptions\TwilioException;
final readonly class SendOTPToUserAction
{
public function __construct(
private SendPasswordResetMailAction $mailAction,
private TwilioService $twilioService
) {}
/**
* @param array<string, mixed> $data
*
* @throws \Throwable
*/
public function execute(array $data): void
{
['user' => $user,'otp' => $otp] = $data;
$this->mailAction->execute($user->email, $otp);
if ($user?->type->phone !== null) {
try {
$this->twilioService->sendSms($user->type->phone, "Your OTP is $otp");
} catch (TwilioException $e) {
\Log::error('SMS send failed', [$e->getMessage()]);
}
}
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Actions\PasswordReset;
use App\Exceptions\UserNotFoundException;
use App\Models\User;
use App\Services\OTPService;
final readonly class VerifyOTPAction
{
public function __construct(
private OTPService $otpService,
) {}
/**
* @throws \Throwable
*/
public function execute(array $data): bool
{
$user = \Session::get('otp_user_id') ? User::find(\Session::get('otp_user_id')) : null;
throw_if(! $user, new UserNotFoundException('User not found'));
return $this->otpService->verify($user, $data['otp']);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Actions;
use App\Models\PageVisit;
use App\Models\User;
final readonly class RecordUserPageVisitAction
{
public function execute(?User $user, string $page): void
{
PageVisit::create(['user_id' => $user?->id, 'page' => $page, 'user_type' => $user?->role ?? null, 'created_at' => now()]);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Actions;
use App\Models\Broker;
use App\Models\Customer;
use App\Models\Deal;
use App\Notifications\NewDealNotification;
final readonly class SendDealCreatedNotificationCustomerAction
{
public function execute(Deal $deal): void
{
/**
* @var Broker $broker
*/
$broker = $deal->broker->type;
$followers = $broker->followers()->with('user')->get();
$followers->map(function (Customer $follower) use ($deal) {
$user = $follower->user;
\Log::info("Sending notification to {$follower->user->name}", [$deal, $follower->user]);
$user->notifyNow(new NewDealNotification($deal));
});
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Actions;
use App\Events\MessageSent;
use App\Exceptions\MessageNotSendException;
use App\Models\User;
use DB;
final readonly class SendMessageAction
{
public function __construct(private CreateOrGetInboxAction $inboxAction) {}
/**
* @throws \Throwable
*/
public function execute(User $sender, User $recipient, array $data): void
{
try {
// find the inbox between the two users
DB::beginTransaction();
$inbox = $this->inboxAction->execute($recipient, $sender);
// update the inbox with the last message and last user as current user
$inbox->last_message = $data['message'];
$inbox->last_user_id = $sender->id;
$inbox->save();
// create a new message in the inbox
$message = $inbox->messages()->create([
'message' => $data['message'],
'user_id' => $sender->id,
]);
// Send the message to all other users in the inbox
broadcast(new MessageSent($message))->toOthers();
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
throw new MessageNotSendException('Message not sent.', previous: $e);
}
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Actions;
use App\Mail\PasswordResetMail;
final readonly class SendPasswordResetMailAction
{
/**
* @throws \Throwable
*/
public function execute(string $email, string $otp): void
{
try {
$message = \Mail::to($email)->send(new PasswordResetMail($otp));
\Log::info('Mail sent successfully', ['message' => $message]);
} catch (\Throwable $e) {
\Log::info('Mail send failed', ['message' => $e->getMessage()]);
}
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Actions;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Throwable;
final readonly class UpdateBrokerAction
{
/**
* @throws Throwable
*/
public function execute(array $data, User $profile): void
{
/**
* Separate the user fields from the broker fields
*/
$userFields = ['name', 'email'];
$data = collect($data);
$profileData = $data->only($userFields)->toArray();
$userData = $data->except($userFields)->toArray();
DB::transaction(function () use ($profileData, $profile, $userData) {
$profile->update($profileData);
$user = $profile->type;
$user->update($userData);
});
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Actions;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Throwable;
final readonly class UpdateCustomerAction
{
/**
* @throws Throwable
*/
public function execute(array $data, User $profile): void
{
/**
* Separate the user fields from the broker fields
*/
$userFields = ['name', 'email'];
$data = collect($data);
$profileData = $data->only($userFields)->toArray();
$userData = $data->except($userFields)->toArray();
DB::transaction(function () use ($profileData, $profile, $userData) {
$profile->update($profileData);
$user = $profile->type;
$user->update($userData);
});
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Events;
use App\Models\Message;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class MessageSent implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Message $message)
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
$users = $this->message->inbox->users->pluck('id')->toArray();
sort($users);
return [
new PrivateChannel("chat.{$users[0]}.{$users[1]}"),
];
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Exceptions;
use Exception;
class MessageNotSendException extends Exception {}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Exceptions;
use Exception;
class UserNotFoundException extends Exception {}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Actions\GetAdminStatsAction;
use App\Http\Controllers\Controller;
class AdminDashboardController extends Controller
{
public function __invoke(GetAdminStatsAction $action)
{
return view('dashboards.admin.index')
->with('stats', $action->execute());
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Actions\UpdateBrokerAction;
use App\Enums\UserStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreBrokerProfileRequest;
use App\Models\Broker;
use Illuminate\Support\Facades\Log;
class BrokerController extends Controller
{
public function index()
{
return view('dashboards.admin.brokers.index')
->with('activeBrokers', Broker::select(['id', 'location'])
->whereRelation('user', 'status', UserStatus::Active->value)
->with('user:id,name,email,role_id,role_type')
->get()
)
->with('pendingBrokers', Broker::select(['id', 'location'])
->whereRelation('user', 'status', UserStatus::Pending->value)
->with('user:id,name,email,role_id,role_type')
->get()
);
}
public function edit(Broker $broker)
{
return view('dashboards.admin.brokers.edit')
->with('profile', $broker->user)
->with('backLink', route('admin.brokers.index'))
->with('actionLink', route('admin.brokers.update', $broker));
}
public function update(StoreBrokerProfileRequest $request, Broker $broker, UpdateBrokerAction $action)
{
try {
$action->execute($request->validated(), $broker->user);
return to_route('admin.brokers.index')
->with('success', 'Profile updated successfully.');
} catch (\Throwable $e) {
Log::error('Broker Profile Update Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->withInput()->with('error', 'Something went wrong.');
}
}
public function destroy(Broker $broker)
{
try {
\DB::transaction(function () use ($broker) {
$broker->user->delete();
$broker->delete();
});
return back()->with('success', 'Broker deleted successfully.');
} catch (\Throwable $e) {
Log::error('Broker Delete Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->with('error', 'Something went wrong.');
}
}
public function approve(Broker $broker)
{
try {
$broker->user->update(['status' => UserStatus::Active->value]);
return to_route('admin.brokers.index')->with('success', 'Broker approved successfully.');
} catch (\Throwable $e) {
Log::error('Broker Approval Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Actions\UpdateCustomerAction;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreCustomerProfileRequest;
use App\Models\Customer;
use Illuminate\Support\Facades\Log;
class CustomerController extends Controller
{
public function index()
{
return view('dashboards.admin.customers.index')
->with('customers', Customer::select(['id', 'location'])
->with('user:id,name,email,role_id,role_type')
->get()
);
}
public function edit(Customer $customer)
{
return view('dashboards.admin.customers.edit')
->with('profile', $customer->user)
->with('backLink', route('admin.customers.index'))
->with('actionLink', route('admin.customers.update', $customer));
}
public function update(StoreCustomerProfileRequest $request, Customer $customer, UpdateCustomerAction $action)
{
try {
$action->execute($request->validated(), $customer->user);
return to_route('admin.customers.index')
->with('success', 'Profile updated successfully.');
} catch (\Throwable $e) {
Log::error('Customer Profile Update Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->withInput()->with('error', 'Something went wrong.');
}
}
public function destroy(Customer $customer)
{
try {
\DB::transaction(function () use ($customer) {
$customer->user->delete();
$customer->delete();
});
return back()->with('success', 'Customer deleted successfully.');
} catch (\Throwable $e) {
Log::error('Customer Delete Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Actions\SendDealCreatedNotificationCustomerAction;
use App\Http\Controllers\Controller;
use App\Models\Deal;
use Illuminate\Support\Facades\Log;
class DealController extends Controller
{
public function index()
{
return view('dashboards.admin.deals.index')
->with('pendingDeals', $this->pendingDeals())
->with('activeDeals', $this->activeDeals());
}
public function approve(Deal $deal, SendDealCreatedNotificationCustomerAction $notificationCustomerAction)
{
try {
\DB::transaction(function () use ($deal) {
$deal->active = true;
$deal->save();
});
$notificationCustomerAction->execute($deal);
return back()->with('success', 'Deal activated successfully.');
} catch (\Throwable $e) {
Log::error('Deal activation Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->with('error', 'Something went wrong.');
}
}
public function reject(Deal $deal)
{
try {
$deal->delete();
return back()->with('success', 'Deal deleted successfully.');
} catch (\Throwable $e) {
Log::error('Deal deletion Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->with('error', 'Something went wrong.');
}
}
private function pendingDeals()
{
return Deal::where('active', false)->get();
}
private function activeDeals()
{
return Deal::where('active', true)->get();
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Enums\ReportStatus;
use App\Http\Controllers\Controller;
use App\Models\Report;
use App\Notifications\ReportRejectedNotificationToUser;
use App\Notifications\ReportResolvedNotificationToBroker;
use Illuminate\Support\Facades\DB;
class ReportController extends Controller
{
public function index()
{
$reports = Report::query()
->with(['user:id,name', 'deals:id,title'])
->tap(fn ($q) => (new Report)->orderByStatus($q,
[ReportStatus::Pending, ReportStatus::Rejected, ReportStatus::Resolved]))
->get();
return view('dashboards.admin.reports.index')
->with('reports', $reports);
}
public function resolve(Report $report)
{
try {
$report->status = ReportStatus::Resolved;
$report->save();
$report->user->notify(new ReportRejectedNotificationToUser($report->deals()->first()->title, false));
$report->deals()->first()->broker->notify(new ReportResolvedNotificationToBroker($report->deals()->first()->title,
false));
return back()->with('success', 'Report resolved successfully.');
} catch (\Throwable $e) {
\Log::error('Error resolving report', [$report->id, $e->getMessage()]);
return back()->with('error', 'Something went wrong.');
}
}
public function reject(Report $report)
{
try {
$report->status = ReportStatus::Rejected;
$report->save();
$report->user->notify(new ReportRejectedNotificationToUser($report->deals()->first()->title));
return back()->with('success', 'Report Rejected successfully.');
} catch (\Throwable $e) {
\Log::error('Error rejecting report', [$report->id, $e->getMessage()]);
return back()->with('error', 'Something went wrong.');
}
}
/**
* Make the attached deal inactive and mark the report resolved.
*/
public function removeContent(Report $report)
{
try {
DB::transaction(function () use ($report) {
$report->user->notify(new ReportRejectedNotificationToUser($report->deals()->first()->title, true));
$report->deals()->first()->broker->notify(new ReportResolvedNotificationToBroker($report->deals()->first()->title,
true));
$deal = $report->deals()->first();
$deal->active = false;
$report->status = ReportStatus::Resolved;
$deal->save();
$report->save();
});
return back()->with('error', 'Deal has been deactivated.');
} catch (\Throwable $e) {
\Log::error('Error removing content of report', [$report->id, $e->getMessage(), $e->getTraceAsString()]);
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -1,9 +1,11 @@
<?php
namespace App\Http\Controllers;
namespace App\Http\Controllers\Auth;
use App\Actions\RecordUserPageVisitAction;
use App\Enums\UserStatus;
use App\Enums\UserTypes;
use App\Http\Controllers\Controller;
use App\Http\Requests\AuthenticateUserRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -15,7 +17,7 @@ public function create()
return view('auth.login');
}
public function store(AuthenticateUserRequest $request)
public function store(AuthenticateUserRequest $request, RecordUserPageVisitAction $action)
{
$data = $request->validated();
if (Auth::attempt($data, $data['remember_me'] ?? false)) {
@ -34,6 +36,12 @@ public function store(AuthenticateUserRequest $request)
UserTypes::Broker->value, UserTypes::User->value => 'explore',
};
try {
$action->execute($user, $route);
} catch (\Throwable $e) {
\Log::error('Error recording user page visit', [$e->getMessage()]);
}
return to_route($route);
} else {
return back()

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Auth;
class ImpersonatedUserController extends Controller
{
public function store(User $user)
{
Session()->put('impersonate', Auth::id());
Session()->put('impersonate_name', $user->name);
Auth::login($user);
return to_route('explore');
}
public function destroy()
{
$adminId = Session()->get('impersonate');
Auth::loginUsingId($adminId);
Session()->forget('impersonate');
Session()->forget('impersonate_name');
return to_route('admin.dashboard');
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Actions\PasswordReset\ResendOTPAction;
use App\Actions\PasswordReset\SendOTPAction;
use App\Actions\PasswordReset\VerifyOTPAction;
use App\Exceptions\UserNotFoundException;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Validation\Rules\Password;
class PasswordResetController extends Controller
{
public function show()
{
return view('auth.passwords.reset');
}
public function sendCode(Request $request, SendOTPAction $action)
{
$data = $request->validate([
'email' => 'required|email',
]);
try {
$action->execute($data);
return to_route('password.reset.show.verify')
->with('success', 'Password reset code is sent');
} catch (UserNotFoundException $e) {
return to_route('password.reset.show.verify')->with('success', 'Password reset code is sent');
}
}
public function showVerify()
{
return view('auth.passwords.verify')
->with('expiryMinutes', 3);
}
public function verify(Request $request, VerifyOTPAction $otpAction)
{
$data = $request->validate(['otp' => 'required|string:min:5:max:6']);
try {
$isVerified = $otpAction->execute($data);
if (! $isVerified) {
return back()->with('error', 'Invalid OTP');
}
return to_route('password.reset.show.update')->with('success', 'OTP Verified');
} catch (UserNotFoundException $e) {
return back()->with('error', 'Session Expired');
}
}
public function showUpdate()
{
return view('auth.passwords.update');
}
public function update(Request $request)
{
$data = $request->validate([
'password' => 'required', 'confirmed', Password::min(8)->letters()->mixedCase()->numbers()->symbols(),
]);
$user = User::find(Session::get('otp_user_id'));
if (! $user) {
return back()->with('error', 'Session Expired');
}
$user->update(['password' => $data['password']]);
\Session::forget('otp_user_id');
return to_route('login.create')->with('success', 'Password updated successfully');
}
public function resend(ResendOTPAction $otpAction)
{
try {
$otpAction->execute();
return to_route('password.reset.show.verify')
->with('success', 'Password reset code is sent');
} catch (UserNotFoundException $e) {
return to_route('password.reset.show.verify')->with('success', 'Password reset code is sent');
}
}
}

View File

@ -1,12 +1,13 @@
<?php
namespace App\Http\Controllers;
namespace App\Http\Controllers\Auth;
use App\Enums\UserStatus;
use App\Enums\UserTypes;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreRegisterdUser;
use App\Models\Broker;
use App\Models\User;
use App\Models\Customer;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@ -22,21 +23,29 @@ public function store(StoreRegisterdUser $request)
$data = $request->validated();
try {
DB::transaction(function () use ($data) {
if ($data['role'] === UserTypes::Broker->value) {
switch ($data['role']) {
case UserTypes::Broker->value:
$data['status'] = UserStatus::Pending->value;
// Create Broker first, then the User linked to it
// Create Broker first, then link the user
$broker = Broker::create();
$broker->user()->create($data);
} else {
break;
case UserTypes::User->value:
$data['status'] = UserStatus::Active->value;
User::create($data);
$customer = Customer::create();
$customer->user()->create($data);
break;
}
});
return to_route('login.create')
->with('userRegistered', 'User registered successfully.');
->with('success', 'User registered successfully.');
} catch (\Throwable $e) {
Log::error('Registration Failed: '.$e->getMessage());

View File

@ -2,15 +2,17 @@
namespace App\Http\Controllers\Broker;
use App\Actions\GetBrokerStatsAction;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
class BrokerDashboardController extends Controller
{
public function index()
public function index(GetBrokerStatsAction $getBrokerStatsAction)
{
return view('dashboards.broker.index')
->with('deals', $this->deals());
->with('deals', $this->deals())
->with('stats', $getBrokerStatsAction->execute(Auth::user()));
}
protected function deals()
@ -28,7 +30,9 @@ protected function deals()
'deal_category_id',
])
->with('category:id,name')
->latest()
->paginate();
->WithLikePerDeal()
->WithRedirectionPerDeal()
->withViewPerDeal()
->latest()->take(3)->get();
}
}

View File

@ -1,14 +1,15 @@
<?php
namespace App\Http\Controllers;
namespace App\Http\Controllers\Broker;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreBrokerDeal;
use App\Models\Deal;
use App\Models\DealCategory;
use App\Services\FileService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class BrokerDealController extends Controller
@ -18,7 +19,8 @@ class BrokerDealController extends Controller
*/
public function index()
{
//
return view('dashboards.broker.deals.index')
->with('deals', $this->deals());
}
/**
@ -49,7 +51,7 @@ public function store(StoreBrokerDeal $request, FileService $fileService)
Deal::create($data);
Deal::reguard();
return to_route('broker.dashboard')->with('success', 'Deal has been created.');
return to_route('broker.deals.index')->with('success', 'Deal has been created.');
}
/**
@ -89,10 +91,10 @@ public function update(StoreBrokerDeal $request, Deal $deal, FileService $fileSe
} catch (\Throwable $exception) {
Log::error($exception->getMessage(), $exception->getTrace());
return to_route('broker.dashboard')->with('error', 'Something gone wrong.');
return back()->with('error', 'Something gone wrong.');
}
return to_route('broker.dashboard')->with('success', 'Deal has been updated.');
return back()->with('success', 'Deal has been updated.');
}
/**
@ -112,9 +114,31 @@ public function destroy(Deal $deal, FileService $fileService)
} catch (\Throwable $exception) {
Log::error($exception->getMessage(), $exception->getTrace());
return to_route('broker.dashboard')->with('error', 'Something gone wrong.');
return back()->with('error', 'Something gone wrong.');
}
return to_route('broker.dashboard')->with('success', 'Deal has been deleted.');
return back()->with('success', 'Deal has been deleted.');
}
protected function deals()
{
return Auth::user()
->deals()
->select([
'id',
'title',
'description',
'image',
'active',
'slug',
'link',
'deal_category_id',
])
->with('category:id,name')
->WithLikePerDeal()
->WithRedirectionPerDeal()
->withViewPerDeal()
->latest()
->paginate(15);
}
}

View File

@ -6,16 +6,16 @@
use App\Http\Requests\StoreBrokerProfileRequest;
use App\Models\Broker;
use App\Models\User;
use App\Services\ProfileInitialsService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class BrokerProfileController extends Controller
{
/**
* Display the specified resource.
*/
public function show(User $profile)
public function show(User $profile, ProfileInitialsService $service)
{
// Get the broker profile
$broker = $profile->type;
@ -25,14 +25,7 @@ public function show(User $profile)
abort(403, 'This user is not a broker.');
}
/**
* Create the initials from a full name (e.g. John Doe, Alex Mark, jane clerk)
* to display on profile page (e.g. JD, AM, JC).
*/
$initials = Str::of($profile->name)
->explode(' ')
->map(fn ($word) => Str::substr(ucfirst($word), 0, 1))
->join('');
$initials = $service->create($profile->name);
return view('dashboards.broker.profile.show')
->with('name', $profile->name)

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers;
use App\Actions\CreateOrGetInboxAction;
use App\Actions\SendMessageAction;
use App\Exceptions\MessageNotSendException;
use App\Http\Requests\SendMessageRequest;
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
use Log;
use Throwable;
class ChatController extends Controller
{
//
public function index(#[CurrentUser] User $user)
{
return view('dashboards.user.chat')
->with('inboxes', $user->inboxes);
}
public function show(#[CurrentUser] User $sender, User $recipient, CreateOrGetInboxAction $action)
{
try {
$inbox = $action->execute($recipient, $sender);
} catch (Throwable $e) {
Log::error('Inbox instantiation Failed: ', [$e->getMessage()]);
abort(500);
}
return view('dashboards.user.chat')
->with('recipient', $recipient)
->with('inboxes', $sender->inboxes)
->with('messages', $inbox->messages()->latest()->get());
}
/**
* @throws Throwable
*/
public function store(
#[CurrentUser] User $sender,
User $recipient,
SendMessageRequest $request,
SendMessageAction $action
) {
try {
$action->execute($sender, $recipient, $request->validated());
return response()->json(['message' => 'Message sent successfully.']);
} catch (MessageNotSendException $e) {
Log::error('Message send failed', [$e->getMessage()]);
return response()->json(['message' => 'Message sent failed.'], 500);
}
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\CommentRequest;
use App\Models\Comment;
use App\Models\Deal;
class CommentController extends Controller
{
public function index(Deal $deal)
{
$comments = $deal->comments()->with('user')->latest()->get();
$html = view('components.dashboard.user.deal-comment.index', compact('comments'))->render();
return response()->json(['html' => $html]);
}
public function store(Deal $deal, CommentRequest $request)
{
$data = $request->validated();
$data['user_id'] = $request->user()->id;
$data['deal_id'] = $deal->id;
Comment::create($data);
return response()->json(['message' => 'Comment created successfully.'], 201);
}
public function show(Comment $comment) {}
public function update(CommentRequest $request, Comment $comment)
{
$comment->update($request->validated());
}
public function destroy(Comment $comment)
{
$comment->delete();
return response()->json();
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ContactRequest;
use App\Models\Admin;
use App\Notifications\NewContactNotification;
class ContactController extends Controller
{
public function __invoke(ContactRequest $request)
{
try {
$data = $request->validated();
$admin = Admin::first();
$admin->user->notify(new NewContactNotification($data['name'], $data['email'], $data['message']));
return back()->with('success', 'Your message has been sent successfully.');
} catch (\Throwable $e) {
\Log::error('Error sending contact message', [$e->getMessage()]);
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -31,6 +31,7 @@ public function __invoke(
protected function deals(FormRequest $request, Builder $query, AddRecentSearchAction $action): LengthAwarePaginator
{
$query->tap(fn ($q) => (new Deal)->withActiveDeals($q));
// Add a search query
if ($request->has('search') && $request->get('search') !== null) {
$query->tap(fn ($q) => (new Deal)->search($q, $request->search));
@ -46,7 +47,6 @@ protected function deals(FormRequest $request, Builder $query, AddRecentSearchAc
}
// Add sorting filters
if ($request->has('sortBy')) {
$query = match (ExplorePageFilters::tryFrom($request->sortBy)) {
ExplorePageFilters::Like => $query->orderBy('total_likes', 'desc'),
ExplorePageFilters::Click => $query->orderBy('total_redirection', 'desc'),
@ -54,7 +54,6 @@ protected function deals(FormRequest $request, Builder $query, AddRecentSearchAc
'((COALESCE(total_likes, 0) * 70.0) / 100.0) + ((COALESCE(total_redirection, 0) * 30.0) / 100.0) DESC'
)
};
}
return $query->latest()->paginate();
}
@ -68,11 +67,12 @@ protected function deals(FormRequest $request, Builder $query, AddRecentSearchAc
protected function profileLink(): string
{
$user = Auth::user();
if ($user->role === UserTypes::Broker->value) {
return route('broker.profile.show', $user);
}
return '';
return match ($user->role ?? null) {
UserTypes::Broker->value => route('broker.profile.show', $user),
UserTypes::User->value => route('customer.profile.show', $user),
default => route('login.create')
};
}
protected function categories(): Collection
@ -82,6 +82,10 @@ protected function categories(): Collection
protected function recentSearches(): Collection
{
return Auth::user()->recentSearches()->latest()->pluck('query');
if (! Auth::check()) {
return collect();
}
return Auth::user()->recentSearches()->latest()->select(['id', 'query'])->get();
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers;
use App\Models\Broker;
use App\Models\Follow;
class FollowController extends Controller
{
public function __invoke(Broker $broker)
{
$follow = $this->checkFollow($broker);
if ($follow === null) {
return $this->store($broker);
} else {
return $this->destroy($follow);
}
}
public function store(Broker $broker)
{
Follow::create([
'broker_id' => $broker->id,
'customer_id' => auth()->id(),
]);
return response()->json(['message' => 'Followed successfully.']);
}
public function destroy(Follow $follow)
{
$follow->delete();
return response()->json(['message' => 'Unfollowed successfully.']);
}
protected function checkFollow(Broker $broker)
{
return Follow::where('broker_id', $broker->id)
->where('customer_id', auth()->id())
->first();
}
}

View File

@ -1,10 +1,12 @@
<?php
namespace App\Http\Controllers;
namespace App\Http\Controllers\Interaction;
use App\Enums\InteractionType;
use App\Http\Controllers\Controller;
use App\Models\Deal;
use App\Models\Interaction;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
@ -14,9 +16,8 @@ class InteractionController extends Controller
* Interact to a deal by Like or Favorite state
*
* @param InteractionType $type [InteractionType::Like, InteractionType::Favorite]
* @return \Illuminate\Http\JsonResponse
*/
public function togglesState(Deal $deal, InteractionType $type)
public function togglesState(Deal $deal, InteractionType $type, Request $request)
{
if (! in_array($type, [InteractionType::Like, InteractionType::Favorite])) {
return response()->json(['error' => 'This interaction is not supported'], 400);
@ -46,8 +47,11 @@ public function togglesState(Deal $deal, InteractionType $type)
$message = ucfirst($type->value).' added to deal';
}
if ($request->expectsJson()) {
return response()->json(['message' => $message]);
} else {
return back()->with('success', $message);
}
} catch (\Throwable $e) {
Log::error('Error when liked a deal',
@ -59,9 +63,12 @@ public function togglesState(Deal $deal, InteractionType $type)
'trace' => $e->getTraceAsString(),
]
);
if ($request->expectsJson()) {
return response()->json(['error' => 'Something went wrong.'], 500);
}
return back()->with('error', 'Something went wrong.');
}
}
public function redirect(Deal $deal)
@ -99,7 +106,6 @@ public function redirect(Deal $deal)
public function view(Deal $deal)
{
ds('hi');
try {
$interaction = $deal->interactions()->firstOrCreate([
'type' => InteractionType::View,
@ -109,7 +115,6 @@ public function view(Deal $deal)
if (! $interaction->wasRecentlyCreated) {
$interaction->increment('count');
}
ds($interaction);
} catch (\Throwable $e) {
Log::error('Error when view a deal',

View File

@ -1,7 +1,8 @@
<?php
namespace App\Http\Controllers;
namespace App\Http\Controllers\Interaction;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreReportRequest;
use App\Models\Deal;
use App\Models\Report;
@ -29,9 +30,9 @@ public function store(StoreReportRequest $request, Deal $deal)
$data['user_id'] = Auth::id();
// Check if the user already reported the deal
$alreadyReported = $deal->reports()->where('user_id', Auth::id())->first();
$alreadyReported = $deal->reports()->where('user_id', Auth::id())->exists();
if ($alreadyReported) {
return response()->json(['message' => 'You already reported this report'], 405);
return response()->json(['message' => 'You had already reported this deal'], 200);
}
try {
@ -41,7 +42,7 @@ public function store(StoreReportRequest $request, Deal $deal)
Report::reguard();
});
return response()->json(['message' => 'Report created'], 201);
return response()->json(['message' => 'Report submitted. Thank you for keeping DealHub safe'], 201);
} catch (\Throwable $exception) {
Log::error('Error creating report', [
@ -77,6 +78,21 @@ public function update(Request $request, Report $report)
*/
public function destroy(Report $report)
{
//
try {
DB::transaction(function () use ($report) {
$report->deals()->detach();
$report->delete();
});
return back()->with('success', 'Report deleted successfully.');
} catch (\Throwable $e) {
Log::error('Error deleting report', [
'user_id' => Auth::id(),
'report_id' => $report->id,
'error' => $e->getMessage(),
]);
}
return back()->with('error', 'Something went wrong.');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\PushSubscriptionRequest;
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\JsonResponse;
class PushSubscriptionController extends Controller
{
public function __invoke(#[CurrentUser] User $user, PushSubscriptionRequest $request): JsonResponse
{
$data = $request->validated();
$user->updatePushSubscription($data['endpoint'], $data['keys']['p256dh'], $data['keys']['auth']);
return response()->json(['message' => 'Push subscription updated successfully.']);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Http\Controllers;
use App\Models\RecentSearch;
class RecentSearchController extends Controller
{
public function __invoke(RecentSearch $recentSearch)
{
$recentSearch->delete();
return response()->json(['message' => 'Search deleted successfully.']);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers;
use App\Enums\UserTypes;
use App\Http\Resources\ActiveUsersStatsCollection;
use App\Http\Resources\DealsCountByCategoryCollection;
use App\Models\DealCategory;
use App\Queries\PageVisitStatsQuery;
use Illuminate\Http\Request;
class StatsController extends Controller
{
public function getActiveUsers(Request $request, PageVisitStatsQuery $baseQuery)
{
$startDay = $request->from ?? now()->subDays(30);
$endDay = $request->to ?? now();
$activeCustomers = $baseQuery->builder(UserTypes::User, $startDay, $endDay)->get();
$activeBrokers = $baseQuery->builder(UserTypes::Broker, $startDay, $endDay)->get();
return response()->json([
'activeCustomers' => new ActiveUsersStatsCollection($activeCustomers),
'activeBrokers' => new ActiveUsersStatsCollection($activeBrokers),
]);
}
public function getDealsByCategory()
{
return new DealsCountByCategoryCollection(
DealCategory::select(['id', 'name'])
->withCount('deals')
->get()
);
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\User;
use App\Actions\GetUserFavoritesAction;
use App\Actions\GetUserReportedDealsAction;
use App\Actions\UpdateCustomerAction;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreCustomerProfileRequest;
use App\Models\User;
use App\Services\ProfileInitialsService;
use Illuminate\Support\Facades\Log;
class UserProfileController extends Controller
{
/**
* Display the specified resource.
*/
public function show(
User $profile,
ProfileInitialsService $service,
GetUserFavoritesAction $favoritesAction,
GetUserReportedDealsAction $reportedDealsAction
) {
// Get the user profile
$user = $profile->type;
$initials = $service->create($profile->name);
return view('dashboards.user.profile.show')
->with('user', $profile)
->with('name', $profile->name)
->with('joinDate', $profile->created_at->format('F Y'))
->with('email', $profile->email)
->with('initials', $initials)
->with('location', $user->location)
->with('bio', $user->bio)
->with('phone', $user->phone)
->with('favorites', $favoritesAction->execute($profile))
->with('reported', $reportedDealsAction->execute($profile));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(User $profile)
{
return view('dashboards.user.profile.edit')
->with('profile', $profile)
->with('pageTitle', 'Edit Profile')
->with('title', 'Edit Your Profile')
->with('description', 'Update your profile information.')
->with('backLink', route('customer.profile.show', $profile))
->with('actionLink', route('customer.profile.update', $profile));
}
/**
* Update the specified resource in storage.
*/
public function update(StoreCustomerProfileRequest $request, User $profile, UpdateCustomerAction $action)
{
try {
$action->execute($request->validated(), $profile);
return to_route('customer.profile.show', $profile)
->with('success', 'Profile updated successfully.');
} catch (\Throwable $e) {
Log::error('Customer Profile Update Failed: '.$e->getMessage(), $e->getTrace());
return back()->withInput()->with('error', 'Something went wrong.');
}
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserFollowedBroker
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$sender = \Auth::user();
if ($sender->isBroker()) {
return $next($request);
}
$recipientUser = $request->route('recipient');
if ($recipientUser->isBroker()) {
$recipient = $recipientUser->type;
$isFollowing = $recipient->followers->contains($sender->id);
if ($isFollowing) {
return $next($request);
}
abort(403, 'You are not following this broker.');
}
abort('404', 'Broker not found.');
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CommentRequest extends FormRequest
{
public function rules(): array
{
return [
'text' => ['required', 'string', 'max:255'],
];
}
public function authorize(): bool
{
return true;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ContactRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|min:3|max:255',
'email' => 'required|email|max:255',
'message' => 'required|string|min:10|max:255',
];
}
protected function getRedirectUrl(): string
{
return parent::getRedirectUrl().'#contact';
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class PushSubscriptionRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'endpoint' => 'required|string|max:500',
'keys.auth' => 'required|string|max:255',
'keys.p256dh' => 'required|string|max:255',
];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SendMessageRequest extends FormRequest
{
public function rules(): array
{
return [
'message' => 'required|string',
];
}
public function authorize(): bool
{
return true;
}
}

View File

@ -2,9 +2,11 @@
namespace App\Http\Requests;
use AllowDynamicProperties;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
#[AllowDynamicProperties]
class StoreBrokerProfileRequest extends FormRequest
{
/**
@ -12,7 +14,17 @@ class StoreBrokerProfileRequest extends FormRequest
*/
public function authorize(): bool
{
return $this->user()->isBroker();
// If this request is by a broker profile, then only allow the owner to update it.
if (isset($this->profile)) {
$this->user = $this->profile;
return $this->user()->id === $this->profile->id;
}
// If this request is by an admin, then allow them to update any profile.
$this->user = $this->broker->user;
return $this->user()->isAdmin();
}
/**
@ -25,7 +37,7 @@ public function rules(): array
return [
'name' => 'required|string|min:3|max:255',
'bio' => 'required|string|min:10|max:255',
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($this->user()->id)],
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($this->user->id)],
'phone' => 'required|string|min:10|max:255',
'location' => 'required|string|min:3|max:255',
];

View File

@ -0,0 +1,45 @@
<?php
namespace App\Http\Requests;
use AllowDynamicProperties;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
#[AllowDynamicProperties]
class StoreCustomerProfileRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
// If this request is by a customer profile, then only allow the owner to update it.
if (isset($this->profile)) {
$this->user = $this->profile;
return $this->user()->id === $this->profile->id;
}
// If this request is by an admin, then allow them to update any profile.
$this->user = $this->customer->user;
return $this->user()->isAdmin();
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|min:3|max:255',
'bio' => 'required|string|min:10|max:255',
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($this->user->id)],
'phone' => 'required|string|min:10|max:255',
'location' => 'required|string|min:3|max:255',
];
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ActiveUsersStatsCollection extends ResourceCollection
{
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ActiveUsersStatsResource extends JsonResource
{
/**
* @return array{data: string, userCount: int}
*/
public function toArray(Request $request): array
{
return [
'date' => $this->date,
'userCount' => (int) $this->user_count,
];
}
}

View File

@ -27,6 +27,7 @@ public function toArray(Request $request): array
'totalRedirection' => $this->total_redirection,
'isLiked' => $this->is_liked,
'isFavorite' => $this->is_favorite,
'isFollowed' => $this->is_followed,
];
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class DealsCountByCategoryCollection extends ResourceCollection
{
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
];
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class DealsCountByCategoryResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'name' => $this->name,
'dealsCount' => $this->deals_count,
];
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class PasswordResetMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(private readonly string $otp) {}
public function envelope(): Envelope
{
return new Envelope(
from: new Address(config('mail.from.address'), config('mail.from.name')),
subject: 'Password Reset',
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.password-reset',
with: ['otp' => $this->otp]
);
}
public function attachments(): array
{
return [];
}
}

29
app/Models/Admin.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
/**
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\User|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admin newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admin newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admin query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admin whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admin whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admin whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class Admin extends Model
{
public function user(): MorphOne
{
return $this->morphOne(User::class, 'role');
}
}

View File

@ -3,10 +3,39 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
/**
* @property int $id
* @property string|null $bio
* @property string|null $location
* @property string|null $phone
* @property bool $verified
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Follow|null $pivot
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Customer> $followers
* @property-read int|null $followers_count
* @property-read \App\Models\User|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker whereBio($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker whereLocation($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker wherePhone($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker whereVerified($value)
*
* @mixin \Eloquent
*/
class Broker extends Model
{
protected $fillable = ['bio', 'location', 'phone'];
protected function casts(): array
{
return [
@ -18,4 +47,14 @@ public function user(): MorphOne
{
return $this->morphOne(User::class, 'role');
}
public function followers(): BelongsToMany
{
return $this->belongsToMany(
Customer::class,
'follows',
'broker_id',
'customer_id'
)->using(Follow::class);
}
}

47
app/Models/Comment.php Normal file
View File

@ -0,0 +1,47 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property string $text
* @property int $deal_id
* @property int $user_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Deal|null $deal
* @property-read \App\Models\User|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereDealId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereText($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereUserId($value)
*
* @mixin \Eloquent
*/
class Comment extends Model
{
protected $fillable = [
'text',
'deal_id',
'user_id',
];
public function deal(): BelongsTo
{
return $this->belongsTo(Deal::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

45
app/Models/Customer.php Normal file
View File

@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
/**
* @property int $id
* @property string|null $bio
* @property string|null $location
* @property string|null $phone
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Follow> $followings
* @property-read int|null $followings_count
* @property-read \App\Models\User|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer whereBio($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer whereLocation($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer wherePhone($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class Customer extends Model
{
protected $fillable = ['bio', 'location', 'phone'];
public function user(): MorphOne
{
return $this->morphOne(User::class, 'role');
}
public function followings(): HasMany
{
return $this->hasMany(Follow::class, 'customer_id');
}
}

View File

@ -11,6 +11,52 @@
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Auth;
/**
* @property int $id
* @property string $title
* @property string $slug
* @property string $description
* @property string|null $image
* @property string|null $link
* @property int $active
* @property int $deal_category_id
* @property int $user_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\User|null $broker
* @property-read \App\Models\DealCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Interaction> $interactions
* @property-read int|null $interactions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Report> $reports
* @property-read int|null $reports_count
*
* @method static Builder<static>|Deal WithActiveDeals()
* @method static Builder<static>|Deal WithCurrentUserInteractions()
* @method static Builder<static>|Deal WithLikePerDeal()
* @method static Builder<static>|Deal WithRedirectionPerDeal()
* @method static Builder<static>|Deal filterByCategory(string $category)
* @method static Builder<static>|Deal newModelQuery()
* @method static Builder<static>|Deal newQuery()
* @method static Builder<static>|Deal query()
* @method static Builder<static>|Deal search(string $search)
* @method static Builder<static>|Deal whereActive($value)
* @method static Builder<static>|Deal whereCreatedAt($value)
* @method static Builder<static>|Deal whereDealCategoryId($value)
* @method static Builder<static>|Deal whereDescription($value)
* @method static Builder<static>|Deal whereId($value)
* @method static Builder<static>|Deal whereImage($value)
* @method static Builder<static>|Deal whereLink($value)
* @method static Builder<static>|Deal whereSlug($value)
* @method static Builder<static>|Deal whereTitle($value)
* @method static Builder<static>|Deal whereUpdatedAt($value)
* @method static Builder<static>|Deal whereUserId($value)
* @method static Builder<static>|Deal withIsFollowedByCurrentUser()
* @method static Builder<static>|Deal withViewPerDeal()
*
* @mixin \Eloquent
*/
class Deal extends Model
{
public function broker(): BelongsTo
@ -33,6 +79,11 @@ public function reports(): BelongsToMany
return $this->belongsToMany(Report::class);
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
/**
* Scope a query to only include active deals
*/
@ -86,15 +137,29 @@ public function WithRedirectionPerDeal(Builder $query): Builder
], 'count');
}
/**
* Scope a query to get a view count per deal
*/
#[Scope]
public function withViewPerDeal(Builder $query): Builder
{
return $query->withSum([
'interactions as total_views' => function ($query) {
$query->where('type', InteractionType::View);
},
], 'count');
}
/**
* Scope a search in a query
*/
#[Scope]
public function search(Builder $query, string $search): Builder
{
return $query
->where('title', 'LIKE', "%$search%")
return $query->where(function (Builder $query) use ($search) {
$query->where('title', 'LIKE', "%$search%")
->orWhereRelation('broker', 'name', 'LIKE', "%$search%");
});
}
/**
@ -105,4 +170,30 @@ public function filterByCategory(Builder $query, string $category): Builder
{
return $query->where('deal_category_id', $category);
}
// Add this to App\Models\Deal.php
/**
* Scope a query to check if the current user follows the deal's broker
*/
#[Scope]
public function withIsFollowedByCurrentUser(Builder $query): Builder
{
$user = Auth::user();
if (! $user || $user->role_type !== \App\Models\Customer::class) {
return $query->withExists(['broker as is_followed' => fn ($q) => $q->whereRaw('1 = 0')]);
}
return $query->withExists([
'broker as is_followed' => function ($query) use ($user) {
$query->where('role_type', \App\Models\Broker::class)
->whereHasMorph('type', [\App\Models\Broker::class], function ($query) use ($user) {
$query->whereHas('followers', function ($query) use ($user) {
$query->where('customer_id', $user->id);
});
});
},
]);
}
}

View File

@ -5,8 +5,36 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
* @property string $name
* @property string|null $description
* @property string $slug
* @property int $active
* @property int $order
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Deal> $deals
* @property-read int|null $deals_count
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereActive($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereDescription($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereOrder($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereSlug($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class DealCategory extends Model
{
protected $fillable = ['name', 'description', 'slug', 'active', 'order'];
public function deals(): HasMany
{
return $this->hasMany(Deal::class);

46
app/Models/Follow.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Pivot;
/**
* @property int $id
* @property int $customer_id
* @property int $broker_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Broker|null $broker
* @property-read \App\Models\Customer|null $customer
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereBrokerId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereCustomerId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class Follow extends Pivot
{
protected $table = 'follows';
protected $fillable = [
'customer_id',
'broker_id',
];
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
public function broker(): BelongsTo
{
return $this->belongsTo(Broker::class);
}
}

55
app/Models/Inbox.php Normal file
View File

@ -0,0 +1,55 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
* @property string|null $last_message
* @property int|null $last_user_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\User|null $lastUser
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $users
* @property-read int|null $users_count
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereLastMessage($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereLastUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class Inbox extends Model
{
protected $fillable = ['last_user_id', 'last_message'];
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
public function lastUser(): BelongsTo
{
return $this->belongsTo(User::class, 'id', 'last_user_id');
}
public function messages(): HasMany
{
return $this->hasMany(Message::class, 'inbox_id');
}
public function getRecipientAttribute(): User
{
// first user in the relationship that is NOT the authenticated user
return $this->users->where('id', '!=', auth()->id())->first();
}
}

View File

@ -6,6 +6,30 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property int $user_id
* @property int $deal_id
* @property InteractionType $type
* @property int $count
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Deal|null $deal
* @property-read \App\Models\Deal|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction whereCount($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction whereDealId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction whereUserId($value)
*
* @mixin \Eloquent
*/
class Interaction extends Model
{
protected $fillable = [

62
app/Models/Message.php Normal file
View File

@ -0,0 +1,62 @@
<?php
namespace App\Models;
use Eloquent;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
/**
* @property int $id
* @property int $user_id
* @property int $inbox_id
* @property string $message
* @property string|null $read_at
* @property string|null $delivered_at
* @property string|null $deleted_at
* @property string|null $failed_at
* @property Carbon $created_at
* @property-read Inbox|null $inbox
* @property-read User|null $user
*
* @method static Builder<static>|Message newModelQuery()
* @method static Builder<static>|Message newQuery()
* @method static Builder<static>|Message query()
* @method static Builder<static>|Message whereCreatedAt($value)
* @method static Builder<static>|Message whereDeletedAt($value)
* @method static Builder<static>|Message whereDeliveredAt($value)
* @method static Builder<static>|Message whereFailedAt($value)
* @method static Builder<static>|Message whereId($value)
* @method static Builder<static>|Message whereInboxId($value)
* @method static Builder<static>|Message whereMessage($value)
* @method static Builder<static>|Message whereReadAt($value)
* @method static Builder<static>|Message whereUserId($value)
*
* @mixin Eloquent
*/
class Message extends Model
{
public $timestamps = false;
protected $fillable = [
'inbox_id', 'user_id', 'message',
'read_at', 'deleted_at', 'failed_at',
'created_at', 'delivered_at',
];
protected $casts = [
'created_at' => 'datetime',
];
public function inbox(): BelongsTo
{
return $this->belongsTo(Inbox::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

41
app/Models/PageVisit.php Normal file
View File

@ -0,0 +1,41 @@
<?php
namespace App\Models;
use App\Enums\UserTypes;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property int|null $user_id
* @property string $page
* @property UserTypes|null $user_type
* @property string $created_at
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit wherePage($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit whereUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit whereUserType($value)
*
* @mixin \Eloquent
*/
class PageVisit extends Model
{
protected $fillable = [
'user_id', 'page', 'user_type',
'created_at',
];
public $timestamps = false;
protected function casts(): array
{
return [
'user_type' => UserTypes::class,
];
}
}

View File

@ -5,6 +5,25 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property int $user_id
* @property string $query
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\User|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch whereQuery($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch whereUserId($value)
*
* @mixin \Eloquent
*/
class RecentSearch extends Model
{
protected $fillable = ['query'];

View File

@ -4,10 +4,38 @@
use App\Enums\ReportStatus;
use App\Enums\ReportType;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @property int $id
* @property ReportType $type
* @property ReportStatus $status
* @property string $description
* @property int $user_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Deal> $deals
* @property-read int|null $deals_count
* @property-read \App\Models\User|null $user
*
* @method static Builder<static>|Report newModelQuery()
* @method static Builder<static>|Report newQuery()
* @method static Builder<static>|Report orderByStatus(array $statusOrder)
* @method static Builder<static>|Report query()
* @method static Builder<static>|Report whereCreatedAt($value)
* @method static Builder<static>|Report whereDescription($value)
* @method static Builder<static>|Report whereId($value)
* @method static Builder<static>|Report whereStatus($value)
* @method static Builder<static>|Report whereType($value)
* @method static Builder<static>|Report whereUpdatedAt($value)
* @method static Builder<static>|Report whereUserId($value)
*
* @mixin \Eloquent
*/
class Report extends Model
{
public function user(): BelongsTo
@ -27,4 +55,15 @@ protected function casts(): array
'status' => ReportStatus::class,
];
}
#[Scope]
public function orderByStatus(Builder $query, array $statusOrder): Builder
{
$values = array_map(fn ($enum) => $enum->value, $statusOrder);
// Create placeholders for each value: FIELD(status, ?, ?, ?)
$placeholders = implode(',', array_fill(0, count($values), '?'));
return $query->orderByRaw("FIELD(status, $placeholders)", $values);
}
}

View File

@ -5,15 +5,74 @@
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use NotificationChannels\WebPush\HasPushSubscriptions;
/**
* @property int $id
* @property string $name
* @property string $email
* @property \Illuminate\Support\Carbon|null $email_verified_at
* @property string $password
* @property string|null $remember_token
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string $status
* @property string $role
* @property string|null $role_type
* @property int|null $role_id
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Deal> $deals
* @property-read int|null $deals_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Interaction> $dealsInteractions
* @property-read int|null $deals_interactions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Inbox> $inboxes
* @property-read int|null $inboxes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Deal> $interactedDeals
* @property-read int|null $interacted_deals_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, User> $interactions
* @property-read int|null $interactions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
* @property-read int|null $notifications_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \NotificationChannels\WebPush\PushSubscription> $pushSubscriptions
* @property-read int|null $push_subscriptions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\RecentSearch> $recentSearches
* @property-read int|null $recent_searches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Report> $reports
* @property-read int|null $reports_count
* @property-read Model|\Eloquent|null $type
*
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|User query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmailVerifiedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePassword($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRememberToken($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRole($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRoleId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRoleType($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
use HasFactory, HasPushSubscriptions, Notifiable;
/**
* The attributes that are mass assignable.
@ -56,6 +115,16 @@ public function isBroker(): bool
return $this->type instanceof Broker;
}
public function isAdmin(): bool
{
return $this->type instanceof Admin;
}
public function isCustomer(): bool
{
return $this->type instanceof Customer;
}
public function deals(): HasMany
{
return $this->hasMany(Deal::class);
@ -78,4 +147,34 @@ public function recentSearches(): HasMany
{
return $this->hasMany(RecentSearch::class);
}
public function dealsInteractions(): HasManyThrough
{
return $this->hasManyThrough(Interaction::class, Deal::class);
}
public function interactedDeals(): HasManyThrough
{
return $this->hasManyThrough(Deal::class, Interaction::class, 'user_id', 'id', 'id', 'deal_id');
}
public function reports(): HasMany
{
return $this->hasMany(Report::class);
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
public function inboxes(): BelongsToMany
{
return $this->belongsToMany(Inbox::class);
}
public function messages(): HasMany
{
return $this->hasMany(Message::class);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class NewContactNotification extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
private readonly string $customerName,
private readonly string $customerEmail,
private readonly string $customerMessage
) {}
public function via($notifiable): array
{
return ['mail'];
}
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->subject('New Contact Submission: '.$this->customerName)
->greeting('Hello Admin,') // Or keep it empty if you prefer
->line('You have received a new message from your contact form:')
->line("**Name:** {$this->customerName}")
->line("**Email:** {$this->customerEmail}")
->line('**Message:**')
->line($this->customerMessage)
->action('Reply via Email', 'mailto:'.$this->customerEmail);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Notifications;
use App\Models\Deal;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Str;
use NotificationChannels\WebPush\WebPushChannel;
use NotificationChannels\WebPush\WebPushMessage;
class NewDealNotification extends Notification
{
/**
* Create a new notification instance.
*/
public function __construct(private readonly Deal $deal)
{
//
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return [WebPushChannel::class];
}
public function toWebPush($notifiable, $notification): WebPushMessage
{
\Log::info('Building WebPush for user: '.$notifiable->id);
return (new WebPushMessage)
->title("New deal from {$this->deal->broker->name}")
->body('Check out this deal:'.Str::limit($this->deal->title, 30, '...'))
->action('View deal', route('explore', ['show' => $this->deal->id]));
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ReportRejectedNotificationToUser extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
private readonly string $dealTitle,
) {}
public function via($notifiable): array
{
return ['mail'];
}
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->subject('Update on Your Recent Report: '.$this->dealTitle)
->greeting('Hello!')
->line('Thank you for helping us maintain the integrity of our marketplace.')
->line("We have completed our review of the deal you reported: **{$this->dealTitle}**.")
->line('Based on our moderation policy, we have rejected your report.')
->action('View Marketplace', route('explore'))
->line('Your feedback helps make our community a safer place for everyone.');
}
public function toArray($notifiable): array
{
return [
'report_outcome' => $this->isContentRemoved ? 'violation_confirmed' : 'no_violation_found',
'deal_title' => $this->dealTitle,
];
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ReportResolvedNotificationToBroker extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
private readonly string $dealTitle,
private readonly bool $isContentRemoved
) {}
public function via($notifiable): array
{
return ['mail'];
}
public function toMail($notifiable): MailMessage
{
$status = $this->isContentRemoved
? 'has been removed following a policy review.'
: 'has been reviewed and remains active.';
return (new MailMessage)
->subject('Update Regarding Your Reported Deal: '.$this->dealTitle)
->greeting('Hello!')
->line("We are writing to inform you that the report regarding your deal, **{$this->dealTitle}**, has been resolved.")
->line("Our moderation team has completed their review, and the content {$status}")
->action('View My Deals', route('broker.dashboard')) // Adjusted for your UMS/Project structure
->line('Thank you for being a part of our marketplace.');
}
public function toArray($notifiable): array
{
return [
'deal_title' => $this->dealTitle,
'action_taken' => $this->isContentRemoved ? 'removed' : 'kept',
];
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ReportResolvedNotificationToUser extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
private readonly string $dealTitle,
private readonly bool $isContentRemoved
) {}
public function via($notifiable): array
{
return ['mail'];
}
public function toMail($notifiable): MailMessage
{
$outcome = $this->isContentRemoved
? 'has been removed following our investigation.'
: 'will remain active as it was found to be in compliance with our guidelines.';
return (new MailMessage)
->subject('Update on Your Recent Report: '.$this->dealTitle)
->greeting('Hello!')
->line('Thank you for helping us maintain the integrity of our marketplace.')
->line("We have completed our review of the deal you reported: **{$this->dealTitle}**.")
->line("Based on our moderation policy, the content {$outcome}")
->action('View Marketplace', route('explore'))
->line('Your feedback helps make our community a safer place for everyone.');
}
public function toArray($notifiable): array
{
return [
'report_outcome' => $this->isContentRemoved ? 'violation_confirmed' : 'no_violation_found',
'deal_title' => $this->dealTitle,
];
}
}

View File

@ -25,11 +25,11 @@ public function builder(): Builder
->with('type');
},
])
// Select only admin-approved deals
->tap(fn ($q) => (new Deal)->withActiveDeals($q))
// Check if the current user interacted with the deal
->tap(fn ($q) => (new Deal)->withCurrentUserInteractions($q))
->tap(fn ($q) => (new Deal)->withLikePerDeal($q))
->tap(fn ($q) => (new Deal)->withIsFollowedByCurrentUser($q))
->tap(fn ($q) => (new Deal)->withRedirectionPerDeal($q));
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Queries;
use App\Enums\UserTypes;
use App\Models\PageVisit;
use Illuminate\Database\Eloquent\Builder;
final readonly class PageVisitStatsQuery
{
/**
* @return Builder<PageVisit>
*/
public function builder(UserTypes $userType, string $startDay, string $endDay): Builder
{
return PageVisit::query()
->selectRaw('count(distinct user_id) as user_count, created_at as date')
->where('user_type', $userType)
->whereBetween('created_at', [$startDay.' 00:00:00', $endDay.' 23:59:59'])
->groupBy('date');
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
class OTPService
{
public function generate(User $user, int $length = 6): string
{
$code = \Str::random($length);
Cache::put("otp_$user->id", $code, now()->addMinutes((int) config('auth.otp_lifespan', '10')));
return $code;
}
public function verify(User $user, string $otp): bool
{
$code = Cache::get("otp_$user->id");
if ($code === $otp) {
Cache::forget("otp_$user->id");
return true;
}
return false;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Services;
use Illuminate\Support\Str;
class ProfileInitialsService
{
/**
* Create the initials from a full name (e.g. John Doe, Alex Mark, jane clerk)
* to display on the profile page (e.g. JD, AM, JC).
*/
public function create(string $fullname)
{
return Str::of($fullname)
->explode(' ')
->map(fn ($word) => Str::substr(ucfirst($word), 0, 1))
->join('');
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Services;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Client;
class TwilioService
{
private Client $client;
public function __construct()
{
$this->client = new Client(config('services.twilio.sid'), config('services.twilio.token'));
}
/**
* @throws TwilioException
*/
public function sendSms(string $to, string $message): void
{
$this->client->messages->create($to, ['from' => config('services.twilio.from'), 'body' => $message]);
}
}

View File

@ -8,6 +8,7 @@
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
channels: __DIR__.'/../routes/channels.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {

View File

@ -1,2 +0,0 @@
*
!.gitignore

105
bootstrap/cache/packages.php vendored Executable file
View File

@ -0,0 +1,105 @@
<?php return array (
'barryvdh/laravel-ide-helper' =>
array (
'providers' =>
array (
0 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
),
),
'blade-ui-kit/blade-heroicons' =>
array (
'providers' =>
array (
0 => 'BladeUI\\Heroicons\\BladeHeroiconsServiceProvider',
),
),
'blade-ui-kit/blade-icons' =>
array (
'providers' =>
array (
0 => 'BladeUI\\Icons\\BladeIconsServiceProvider',
),
),
'laradumps/laradumps' =>
array (
'providers' =>
array (
0 => 'LaraDumps\\LaraDumps\\LaraDumpsServiceProvider',
),
),
'laravel-notification-channels/webpush' =>
array (
'providers' =>
array (
0 => 'NotificationChannels\\WebPush\\WebPushServiceProvider',
),
),
'laravel/pail' =>
array (
'providers' =>
array (
0 => 'Laravel\\Pail\\PailServiceProvider',
),
),
'laravel/reverb' =>
array (
'providers' =>
array (
0 => 'Laravel\\Reverb\\ApplicationManagerServiceProvider',
1 => 'Laravel\\Reverb\\ReverbServiceProvider',
),
),
'laravel/sail' =>
array (
'providers' =>
array (
0 => 'Laravel\\Sail\\SailServiceProvider',
),
),
'laravel/tinker' =>
array (
'providers' =>
array (
0 => 'Laravel\\Tinker\\TinkerServiceProvider',
),
),
'nesbot/carbon' =>
array (
'providers' =>
array (
0 => 'Carbon\\Laravel\\ServiceProvider',
),
),
'nunomaduro/collision' =>
array (
'providers' =>
array (
0 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
),
),
'nunomaduro/termwind' =>
array (
'providers' =>
array (
0 => 'Termwind\\Laravel\\TermwindServiceProvider',
),
),
'opcodesio/log-viewer' =>
array (
'aliases' =>
array (
'LogViewer' => 'Opcodes\\LogViewer\\Facades\\LogViewer',
),
'providers' =>
array (
0 => 'Opcodes\\LogViewer\\LogViewerServiceProvider',
),
),
'pestphp/pest-plugin-laravel' =>
array (
'providers' =>
array (
0 => 'Pest\\Laravel\\PestServiceProvider',
),
),
);

289
bootstrap/cache/services.php vendored Executable file
View File

@ -0,0 +1,289 @@
<?php return array (
'providers' =>
array (
0 => 'Illuminate\\Auth\\AuthServiceProvider',
1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
2 => 'Illuminate\\Bus\\BusServiceProvider',
3 => 'Illuminate\\Cache\\CacheServiceProvider',
4 => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
5 => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
6 => 'Illuminate\\Cookie\\CookieServiceProvider',
7 => 'Illuminate\\Database\\DatabaseServiceProvider',
8 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
9 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
10 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
11 => 'Illuminate\\Hashing\\HashServiceProvider',
12 => 'Illuminate\\Mail\\MailServiceProvider',
13 => 'Illuminate\\Notifications\\NotificationServiceProvider',
14 => 'Illuminate\\Pagination\\PaginationServiceProvider',
15 => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
16 => 'Illuminate\\Pipeline\\PipelineServiceProvider',
17 => 'Illuminate\\Queue\\QueueServiceProvider',
18 => 'Illuminate\\Redis\\RedisServiceProvider',
19 => 'Illuminate\\Session\\SessionServiceProvider',
20 => 'Illuminate\\Translation\\TranslationServiceProvider',
21 => 'Illuminate\\Validation\\ValidationServiceProvider',
22 => 'Illuminate\\View\\ViewServiceProvider',
23 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
24 => 'BladeUI\\Heroicons\\BladeHeroiconsServiceProvider',
25 => 'BladeUI\\Icons\\BladeIconsServiceProvider',
26 => 'LaraDumps\\LaraDumps\\LaraDumpsServiceProvider',
27 => 'NotificationChannels\\WebPush\\WebPushServiceProvider',
28 => 'Laravel\\Pail\\PailServiceProvider',
29 => 'Laravel\\Reverb\\ApplicationManagerServiceProvider',
30 => 'Laravel\\Reverb\\ReverbServiceProvider',
31 => 'Laravel\\Sail\\SailServiceProvider',
32 => 'Laravel\\Tinker\\TinkerServiceProvider',
33 => 'Carbon\\Laravel\\ServiceProvider',
34 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
35 => 'Termwind\\Laravel\\TermwindServiceProvider',
36 => 'Opcodes\\LogViewer\\LogViewerServiceProvider',
37 => 'Pest\\Laravel\\PestServiceProvider',
38 => 'App\\Providers\\AppServiceProvider',
),
'eager' =>
array (
0 => 'Illuminate\\Auth\\AuthServiceProvider',
1 => 'Illuminate\\Cookie\\CookieServiceProvider',
2 => 'Illuminate\\Database\\DatabaseServiceProvider',
3 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
4 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
5 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
6 => 'Illuminate\\Notifications\\NotificationServiceProvider',
7 => 'Illuminate\\Pagination\\PaginationServiceProvider',
8 => 'Illuminate\\Session\\SessionServiceProvider',
9 => 'Illuminate\\View\\ViewServiceProvider',
10 => 'BladeUI\\Heroicons\\BladeHeroiconsServiceProvider',
11 => 'BladeUI\\Icons\\BladeIconsServiceProvider',
12 => 'LaraDumps\\LaraDumps\\LaraDumpsServiceProvider',
13 => 'NotificationChannels\\WebPush\\WebPushServiceProvider',
14 => 'Laravel\\Pail\\PailServiceProvider',
15 => 'Laravel\\Reverb\\ReverbServiceProvider',
16 => 'Carbon\\Laravel\\ServiceProvider',
17 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
18 => 'Termwind\\Laravel\\TermwindServiceProvider',
19 => 'Opcodes\\LogViewer\\LogViewerServiceProvider',
20 => 'Pest\\Laravel\\PestServiceProvider',
21 => 'App\\Providers\\AppServiceProvider',
),
'deferred' =>
array (
'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Contracts\\Broadcasting\\Broadcaster' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Contracts\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Contracts\\Bus\\QueueingDispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Bus\\BatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Bus\\DatabaseBatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
'cache' => 'Illuminate\\Cache\\CacheServiceProvider',
'cache.store' => 'Illuminate\\Cache\\CacheServiceProvider',
'cache.psr6' => 'Illuminate\\Cache\\CacheServiceProvider',
'memcached.connector' => 'Illuminate\\Cache\\CacheServiceProvider',
'Illuminate\\Cache\\RateLimiter' => 'Illuminate\\Cache\\CacheServiceProvider',
'Illuminate\\Foundation\\Console\\AboutCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\ForgetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ClearCompiledCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Auth\\Console\\ClearResetsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\DbCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\PruneCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\ShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\WipeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\DownCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentDecryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentEncryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Concurrency\\Console\\InvokeSerializedClosureCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\KeyGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\OptimizeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\OptimizeClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\PackageDiscoverCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\PruneStaleTagsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ListFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\FlushFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ForgetFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ListenCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PauseCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PruneBatchesCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PruneFailedJobsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RestartCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ResumeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RetryCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RetryBatchCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\WorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ReloadCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\DumpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Seeds\\SeedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleFinishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleRunCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleClearCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleTestCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleWorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleInterruptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\ShowModelCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StorageLinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StorageUnlinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\UpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ApiInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\BroadcastingInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\CacheTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\CastMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ChannelListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ChannelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ClassMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ComponentMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConsoleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Routing\\Console\\ControllerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\DocsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnumMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ExceptionMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Factories\\FactoryMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\InterfaceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\JobMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\JobMiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\LangPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ListenerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\MailMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Routing\\Console\\MiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ModelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\NotificationMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Notifications\\Console\\NotificationTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ObserverMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\PolicyMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ProviderMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\FailedTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\BatchesTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RequestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ResourceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RuleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ScopeMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Seeds\\SeederMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Session\\Console\\SessionTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ServeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StubPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\TestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\TraitMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\VendorPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migration.repository' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migration.creator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Migrations\\Migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\MigrateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\FreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\InstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\RefreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\ResetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\RollbackCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\StatusCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\MigrateMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'composer' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Concurrency\\ConcurrencyManager' => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
'hash' => 'Illuminate\\Hashing\\HashServiceProvider',
'hash.driver' => 'Illuminate\\Hashing\\HashServiceProvider',
'mail.manager' => 'Illuminate\\Mail\\MailServiceProvider',
'mailer' => 'Illuminate\\Mail\\MailServiceProvider',
'Illuminate\\Mail\\Markdown' => 'Illuminate\\Mail\\MailServiceProvider',
'auth.password' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
'auth.password.broker' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
'Illuminate\\Contracts\\Pipeline\\Hub' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
'pipeline' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
'queue' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.connection' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.failer' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.listener' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.worker' => 'Illuminate\\Queue\\QueueServiceProvider',
'redis' => 'Illuminate\\Redis\\RedisServiceProvider',
'redis.connection' => 'Illuminate\\Redis\\RedisServiceProvider',
'translator' => 'Illuminate\\Translation\\TranslationServiceProvider',
'translation.loader' => 'Illuminate\\Translation\\TranslationServiceProvider',
'validator' => 'Illuminate\\Validation\\ValidationServiceProvider',
'validation.presence' => 'Illuminate\\Validation\\ValidationServiceProvider',
'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider',
'Barryvdh\\LaravelIdeHelper\\Console\\GeneratorCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Barryvdh\\LaravelIdeHelper\\Console\\ModelsCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Barryvdh\\LaravelIdeHelper\\Console\\MetaCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Barryvdh\\LaravelIdeHelper\\Console\\EloquentCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Laravel\\Reverb\\ApplicationManager' => 'Laravel\\Reverb\\ApplicationManagerServiceProvider',
'Laravel\\Reverb\\Contracts\\ApplicationProvider' => 'Laravel\\Reverb\\ApplicationManagerServiceProvider',
'Laravel\\Sail\\Console\\InstallCommand' => 'Laravel\\Sail\\SailServiceProvider',
'Laravel\\Sail\\Console\\PublishCommand' => 'Laravel\\Sail\\SailServiceProvider',
'command.tinker' => 'Laravel\\Tinker\\TinkerServiceProvider',
),
'when' =>
array (
'Illuminate\\Broadcasting\\BroadcastServiceProvider' =>
array (
),
'Illuminate\\Bus\\BusServiceProvider' =>
array (
),
'Illuminate\\Cache\\CacheServiceProvider' =>
array (
),
'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider' =>
array (
),
'Illuminate\\Concurrency\\ConcurrencyServiceProvider' =>
array (
),
'Illuminate\\Hashing\\HashServiceProvider' =>
array (
),
'Illuminate\\Mail\\MailServiceProvider' =>
array (
),
'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider' =>
array (
),
'Illuminate\\Pipeline\\PipelineServiceProvider' =>
array (
),
'Illuminate\\Queue\\QueueServiceProvider' =>
array (
),
'Illuminate\\Redis\\RedisServiceProvider' =>
array (
),
'Illuminate\\Translation\\TranslationServiceProvider' =>
array (
),
'Illuminate\\Validation\\ValidationServiceProvider' =>
array (
),
'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider' =>
array (
),
'Laravel\\Reverb\\ApplicationManagerServiceProvider' =>
array (
),
'Laravel\\Sail\\SailServiceProvider' =>
array (
),
'Laravel\\Tinker\\TinkerServiceProvider' =>
array (
),
),
);

View File

@ -11,10 +11,14 @@
"require": {
"php": "^8.2",
"blade-ui-kit/blade-heroicons": "^2.6",
"laravel-notification-channels/webpush": "^10.4",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1"
"laravel/reverb": "^1.0",
"laravel/tinker": "^2.10.1",
"twilio/sdk": "^8.10"
},
"require-dev": {
"barryvdh/laravel-ide-helper": "^3.6",
"fakerphp/faker": "^1.23",
"laradumps/laradumps": "^5.0",
"laravel/pail": "^1.2.2",
@ -22,6 +26,7 @@
"laravel/sail": "^1.41",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"opcodesio/log-viewer": "^3.21",
"pestphp/pest": "^4.3",
"pestphp/pest-plugin-laravel": "^4.0"
},

2487
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -111,5 +111,6 @@
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
'otp_lifespan' => env('OTP_LIFESPAN', 10),
];

82
config/broadcasting.php Normal file
View File

@ -0,0 +1,82 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
*/
'default' => env('BROADCAST_CONNECTION', 'null'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over WebSockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'reverb' => [
'driver' => 'reverb',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true,
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];

95
config/reverb.php Normal file
View File

@ -0,0 +1,95 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Reverb Server
|--------------------------------------------------------------------------
|
| This option controls the default server used by Reverb to handle
| incoming messages as well as broadcasting message to all your
| connected clients. At this time only "reverb" is supported.
|
*/
'default' => env('REVERB_SERVER', 'reverb'),
/*
|--------------------------------------------------------------------------
| Reverb Servers
|--------------------------------------------------------------------------
|
| Here you may define details for each of the supported Reverb servers.
| Each server has its own configuration options that are defined in
| the array below. You should ensure all the options are present.
|
*/
'servers' => [
'reverb' => [
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
'port' => env('REVERB_SERVER_PORT', 8080),
'path' => env('REVERB_SERVER_PATH', ''),
'hostname' => env('REVERB_HOST'),
'options' => [
'tls' => [],
],
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
'scaling' => [
'enabled' => env('REVERB_SCALING_ENABLED', false),
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
'server' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_DB', '0'),
'timeout' => env('REDIS_TIMEOUT', 60),
],
],
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
],
],
/*
|--------------------------------------------------------------------------
| Reverb Applications
|--------------------------------------------------------------------------
|
| Here you may define how Reverb applications are managed. If you choose
| to use the "config" provider, you may define an array of apps which
| your server will support, including their connection credentials.
|
*/
'apps' => [
'provider' => 'config',
'apps' => [
[
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'allowed_origins' => ['*'],
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
],
],
],
];

View File

@ -35,4 +35,10 @@
],
],
'twilio' => [
'sid' => env('TWILIO_SID'),
'token' => env('TWILIO_AUTH_TOKEN'),
'from' => env('TWILIO_NUMBER'),
],
];

44
config/webpush.php Normal file
View File

@ -0,0 +1,44 @@
<?php
return [
/**
* These are the keys for authentication (VAPID).
* These keys must be safely stored and should not change.
*/
'vapid' => [
'subject' => env('VAPID_SUBJECT'),
'public_key' => env('VAPID_PUBLIC_KEY'),
'private_key' => env('VAPID_PRIVATE_KEY'),
'pem_file' => env('VAPID_PEM_FILE'),
],
/**
* This is model that will be used to for push subscriptions.
*/
'model' => \NotificationChannels\WebPush\PushSubscription::class,
/**
* This is the name of the table that will be created by the migration and
* used by the PushSubscription model shipped with this package.
*/
'table_name' => env('WEBPUSH_DB_TABLE', 'push_subscriptions'),
/**
* This is the database connection that will be used by the migration and
* the PushSubscription model shipped with this package.
*/
'database_connection' => env('WEBPUSH_DB_CONNECTION', env('DB_CONNECTION', 'mysql')),
/**
* The Guzzle client options used by Minishlink\WebPush.
*/
'client_options' => [],
/**
* The automatic padding in bytes used by Minishlink\WebPush.
* Set to false to support Firefox Android with v1 endpoint.
*/
'automatic_padding' => env('WEBPUSH_AUTOMATIC_PADDING', true),
];

View File

@ -0,0 +1,30 @@
<?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('customers', function (Blueprint $table) {
$table->id();
$table->text('bio')->nullable();
$table->string('location')->nullable();
$table->string('phone')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('customers');
}
};

View File

@ -0,0 +1,21 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('admins', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('admins');
}
};

View File

@ -0,0 +1,32 @@
<?php
use App\Enums\UserTypes;
use App\Models\User;
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('page_visits', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(User::class)->nullable();
$table->string('page');
$table->enum('user_type', UserTypes::values())->nullable();
$table->date('created_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('page_visits');
}
};

View File

@ -0,0 +1,26 @@
<?php
use App\Models\Deal;
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->string('text');
$table->foreignIdFor(Deal::class);
$table->foreignIdFor(User::class);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('comments');
}
};

View File

@ -0,0 +1,25 @@
<?php
use App\Models\Broker;
use App\Models\Customer;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('follows', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Customer::class);
$table->foreignIdFor(Broker::class);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('follows');
}
};

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.
*
* @return void
*/
public function up()
{
Schema::connection(config('webpush.database_connection'))->create(config('webpush.table_name'), function (Blueprint $table) {
$table->bigIncrements('id');
$table->morphs('subscribable', 'push_subscriptions_subscribable_morph_idx');
$table->string('endpoint', 500)->unique();
$table->string('public_key')->nullable();
$table->string('auth_token')->nullable();
$table->string('content_encoding')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::connection(config('webpush.database_connection'))->dropIfExists(config('webpush.table_name'));
}
};

Some files were not shown because too many files have changed in this diff Show More