Compare commits
No commits in common. "master" and "main" have entirely different histories.
27
.env.example
27
.env.example
@ -1,4 +1,4 @@
|
|||||||
APP_NAME=Laravel
|
APP_NAME=DealHub
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
@ -63,3 +63,28 @@ AWS_BUCKET=
|
|||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
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}"
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,3 +22,4 @@
|
|||||||
Homestead.json
|
Homestead.json
|
||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
laradumps.yaml
|
||||||
|
|||||||
2371
.phpstorm.meta.php
Normal file
2371
.phpstorm.meta.php
Normal file
File diff suppressed because it is too large
Load Diff
147
README.md
147
README.md
@ -1,59 +1,128 @@
|
|||||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
# 🚀 DealHub
|
||||||
|
|
||||||
<p align="center">
|
**DelHub** is a **Recommendation & Deal Discovery Platform** built with **Laravel**, where verified brokers share deals and recommendations, and users discover trusted content through likes, favorites, search, and trending logic.
|
||||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
|
||||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
|
||||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
|
||||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## About Laravel
|
---
|
||||||
|
|
||||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
## 📌 Features
|
||||||
|
|
||||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
### 🌐 Public / User
|
||||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
|
||||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
|
||||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
|
||||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
|
||||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
|
||||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
|
||||||
|
|
||||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
* View all approved deals and listings
|
||||||
|
* Like ❤️, Favorite ⭐, and Report 🚩 deals
|
||||||
|
* Search deals with category filters
|
||||||
|
* Discover trending, most liked, and recent deals
|
||||||
|
* View broker contact information
|
||||||
|
|
||||||
## Learning Laravel
|
### 🧑💼 Broker
|
||||||
|
|
||||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
|
* Create and manage deal posts
|
||||||
|
* Upload images and external links
|
||||||
|
* Public broker profile with listings
|
||||||
|
* View analytics (views, likes, clicks)
|
||||||
|
* Broker dashboard with performance insights
|
||||||
|
|
||||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
### 👑 Admin
|
||||||
|
|
||||||
## Laravel Sponsors
|
* Manage users and brokers (CRUD)
|
||||||
|
* Login as User/Broker (for support/debugging)
|
||||||
|
* Approve or reject broker listings
|
||||||
|
* Feature or block deals
|
||||||
|
* Admin analytics dashboard
|
||||||
|
|
||||||
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
---
|
||||||
|
|
||||||
### Premium Partners
|
## 🧱 Tech Stack
|
||||||
|
|
||||||
- **[Vehikl](https://vehikl.com)**
|
* **Backend:** Laravel
|
||||||
- **[Tighten Co.](https://tighten.co)**
|
* **Frontend:** Blade + Tailwind CSS
|
||||||
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
* **Authentication:** Laravel Breeze
|
||||||
- **[64 Robots](https://64robots.com)**
|
* **Database:** MySQL
|
||||||
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
|
* **Version Control:** Git & GitHub
|
||||||
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
|
||||||
- **[Redberry](https://redberry.international/laravel-development)**
|
|
||||||
- **[Active Logic](https://activelogic.com)**
|
|
||||||
|
|
||||||
## Contributing
|
---
|
||||||
|
|
||||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
## 🗂️ Core Modules
|
||||||
|
|
||||||
## Code of Conduct
|
* Authentication & Role Management
|
||||||
|
* Listings (Deals)
|
||||||
|
* User Interactions (Like, Favorite, Report)
|
||||||
|
* Tracking (Views & Clicks)
|
||||||
|
* Search & Discovery
|
||||||
|
* Broker Profiles & Analytics
|
||||||
|
* Admin Dashboard & Controls
|
||||||
|
* Landing Page (Home, About, Contact)
|
||||||
|
|
||||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
---
|
||||||
|
|
||||||
## Security Vulnerabilities
|
## 🖥️ Landing Page Includes
|
||||||
|
|
||||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
* Header with navigation
|
||||||
|
* Hero banner
|
||||||
|
* About section
|
||||||
|
* How it works
|
||||||
|
* Contact form
|
||||||
|
* Footer with links
|
||||||
|
|
||||||
## License
|
---
|
||||||
|
|
||||||
|
Used for:
|
||||||
|
|
||||||
|
* Trending deals
|
||||||
|
* Homepage highlights
|
||||||
|
* Category top listings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ 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
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Roles
|
||||||
|
|
||||||
|
* **Admin**
|
||||||
|
* **Broker**
|
||||||
|
* **User**
|
||||||
|
|
||||||
|
Role-based access is handled via middleware.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Future Enhancements
|
||||||
|
|
||||||
|
* Notifications
|
||||||
|
* Deal expiry & auto-hide
|
||||||
|
* API support
|
||||||
|
* Mobile app integration
|
||||||
|
* Advanced analytics charts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👨💻 Author
|
||||||
|
|
||||||
|
**DealHub**
|
||||||
|
Built with ❤️ using Laravel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
|
||||||
|
|||||||
4
TODO.md
Normal file
4
TODO.md
Normal 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
28340
_ide_helper.php
Normal file
File diff suppressed because it is too large
Load Diff
30
app/Actions/AddRecentSearchAction.php
Normal file
30
app/Actions/AddRecentSearchAction.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
final readonly class AddRecentSearchAction
|
||||||
|
{
|
||||||
|
public function execute(User $user, array $data): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($user, $data) {
|
||||||
|
$user->recentSearches()->updateOrcreate($data);
|
||||||
|
$recentSearchCount = $user->recentSearches()->count();
|
||||||
|
if ($recentSearchCount > 5) {
|
||||||
|
$user->recentSearches()->oldest()->limit(1)->delete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Error adding recent search',
|
||||||
|
[
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTrace(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Actions/CreateOrGetInboxAction.php
Normal file
31
app/Actions/CreateOrGetInboxAction.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Actions/GetAdminStatsAction.php
Normal file
22
app/Actions/GetAdminStatsAction.php
Normal 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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Actions/GetBrokerStatsAction.php
Normal file
22
app/Actions/GetBrokerStatsAction.php
Normal 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'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app/Actions/GetUserFavoritesAction.php
Normal file
20
app/Actions/GetUserFavoritesAction.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
17
app/Actions/GetUserReportedDealsAction.php
Normal file
17
app/Actions/GetUserReportedDealsAction.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/Actions/PasswordReset/ResendOTPAction.php
Normal file
29
app/Actions/PasswordReset/ResendOTPAction.php
Normal 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]);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Actions/PasswordReset/SendOTPAction.php
Normal file
30
app/Actions/PasswordReset/SendOTPAction.php
Normal 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]);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Actions/PasswordReset/SendOTPToUserAction.php
Normal file
35
app/Actions/PasswordReset/SendOTPToUserAction.php
Normal 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()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Actions/PasswordReset/VerifyOTPAction.php
Normal file
25
app/Actions/PasswordReset/VerifyOTPAction.php
Normal 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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
app/Actions/RecordUserPageVisitAction.php
Normal file
14
app/Actions/RecordUserPageVisitAction.php
Normal 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()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/Actions/SendDealCreatedNotificationCustomerAction.php
Normal file
27
app/Actions/SendDealCreatedNotificationCustomerAction.php
Normal 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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
45
app/Actions/SendMessageAction.php
Normal file
45
app/Actions/SendMessageAction.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Actions/SendPasswordResetMailAction.php
Normal file
21
app/Actions/SendPasswordResetMailAction.php
Normal 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()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Actions/UpdateBrokerAction.php
Normal file
30
app/Actions/UpdateBrokerAction.php
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Actions/UpdateCustomerAction.php
Normal file
30
app/Actions/UpdateCustomerAction.php
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
13
app/Enums/ExplorePageFilters.php
Normal file
13
app/Enums/ExplorePageFilters.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Traits\EnumAsArray;
|
||||||
|
|
||||||
|
enum ExplorePageFilters: string
|
||||||
|
{
|
||||||
|
use EnumAsArray;
|
||||||
|
|
||||||
|
case Like = 'like';
|
||||||
|
case Click = 'click';
|
||||||
|
}
|
||||||
15
app/Enums/InteractionType.php
Normal file
15
app/Enums/InteractionType.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Traits\EnumAsArray;
|
||||||
|
|
||||||
|
enum InteractionType: string
|
||||||
|
{
|
||||||
|
use EnumAsArray;
|
||||||
|
|
||||||
|
case Like = 'like';
|
||||||
|
case Favorite = 'favorite';
|
||||||
|
case Redirection = 'redirect';
|
||||||
|
case View = 'view';
|
||||||
|
}
|
||||||
14
app/Enums/ReportStatus.php
Normal file
14
app/Enums/ReportStatus.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Traits\EnumAsArray;
|
||||||
|
|
||||||
|
enum ReportStatus: string
|
||||||
|
{
|
||||||
|
use EnumAsArray;
|
||||||
|
|
||||||
|
case Pending = 'pending';
|
||||||
|
case Resolved = 'resolved';
|
||||||
|
case Rejected = 'rejected';
|
||||||
|
}
|
||||||
17
app/Enums/ReportType.php
Normal file
17
app/Enums/ReportType.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Traits\EnumAsArray;
|
||||||
|
|
||||||
|
enum ReportType: string
|
||||||
|
{
|
||||||
|
use EnumAsArray;
|
||||||
|
|
||||||
|
case Spam = 'spam';
|
||||||
|
case Harmful = 'harmful';
|
||||||
|
case Nudity = 'nudity';
|
||||||
|
case Misinformation = 'misinformation';
|
||||||
|
case Unauthorized = 'unauthorized';
|
||||||
|
|
||||||
|
}
|
||||||
14
app/Enums/UserStatus.php
Normal file
14
app/Enums/UserStatus.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Traits\EnumAsArray;
|
||||||
|
|
||||||
|
enum UserStatus: string
|
||||||
|
{
|
||||||
|
use EnumAsArray;
|
||||||
|
|
||||||
|
case Active = 'active';
|
||||||
|
case Blocked = 'blocked';
|
||||||
|
case Pending = 'pending';
|
||||||
|
}
|
||||||
34
app/Enums/UserTypes.php
Normal file
34
app/Enums/UserTypes.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Traits\EnumAsArray;
|
||||||
|
|
||||||
|
enum UserTypes: string
|
||||||
|
{
|
||||||
|
use EnumAsArray;
|
||||||
|
|
||||||
|
case Admin = 'admin';
|
||||||
|
case User = 'user';
|
||||||
|
case Broker = 'broker';
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
UserTypes::Broker => 'Broker (Posts deals)',
|
||||||
|
UserTypes::User => 'User (Browse deals)',
|
||||||
|
self::Admin => 'Admin',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function labels(): array
|
||||||
|
{
|
||||||
|
$labels = array_map(function ($enum) {
|
||||||
|
return ['value' => $enum->value, 'label' => $enum->label()];
|
||||||
|
}, self::cases());
|
||||||
|
|
||||||
|
return array_filter($labels, function ($kv) {
|
||||||
|
return $kv['value'] !== self::Admin->value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/Events/MessageSent.php
Normal file
38
app/Events/MessageSent.php
Normal 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]}"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
7
app/Exceptions/MessageNotSendException.php
Normal file
7
app/Exceptions/MessageNotSendException.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class MessageNotSendException extends Exception {}
|
||||||
7
app/Exceptions/UserNotFoundException.php
Normal file
7
app/Exceptions/UserNotFoundException.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class UserNotFoundException extends Exception {}
|
||||||
15
app/Http/Controllers/Admin/AdminDashboardController.php
Normal file
15
app/Http/Controllers/Admin/AdminDashboardController.php
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
79
app/Http/Controllers/Admin/BrokerController.php
Normal file
79
app/Http/Controllers/Admin/BrokerController.php
Normal 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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/Http/Controllers/Admin/CustomerController.php
Normal file
59
app/Http/Controllers/Admin/CustomerController.php
Normal 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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
app/Http/Controllers/Admin/DealController.php
Normal file
58
app/Http/Controllers/Admin/DealController.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
87
app/Http/Controllers/Admin/ReportController.php
Normal file
87
app/Http/Controllers/Admin/ReportController.php
Normal 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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
app/Http/Controllers/Auth/AuthenticatedUserController.php
Normal file
61
app/Http/Controllers/Auth/AuthenticatedUserController.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
class AuthenticatedUserController extends Controller
|
||||||
|
{
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('auth.login');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(AuthenticateUserRequest $request, RecordUserPageVisitAction $action)
|
||||||
|
{
|
||||||
|
$data = $request->validated();
|
||||||
|
if (Auth::attempt($data, $data['remember_me'] ?? false)) {
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if ($user->status !== UserStatus::Active->value) {
|
||||||
|
Auth::logout();
|
||||||
|
|
||||||
|
return back()->with('error', 'Your account is not active.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->session()->regenerate();
|
||||||
|
|
||||||
|
$route = match ($user->role) {
|
||||||
|
UserTypes::Admin->value => 'admin.dashboard',
|
||||||
|
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()
|
||||||
|
->withInput()
|
||||||
|
->with('error', 'Invalid Credentials');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request)
|
||||||
|
{
|
||||||
|
Auth::logout();
|
||||||
|
$request->session()->invalidate();
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return to_route('home');
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Http/Controllers/Auth/ImpersonatedUserController.php
Normal file
32
app/Http/Controllers/Auth/ImpersonatedUserController.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
91
app/Http/Controllers/Auth/PasswordResetController.php
Normal file
91
app/Http/Controllers/Auth/PasswordResetController.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
app/Http/Controllers/Auth/RegisteredUserController.php
Normal file
58
app/Http/Controllers/Auth/RegisteredUserController.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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\Customer;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class RegisteredUserController extends Controller
|
||||||
|
{
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('auth.register');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StoreRegisterdUser $request)
|
||||||
|
{
|
||||||
|
$data = $request->validated();
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($data) {
|
||||||
|
switch ($data['role']) {
|
||||||
|
case UserTypes::Broker->value:
|
||||||
|
|
||||||
|
$data['status'] = UserStatus::Pending->value;
|
||||||
|
|
||||||
|
// Create Broker first, then link the user
|
||||||
|
$broker = Broker::create();
|
||||||
|
$broker->user()->create($data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserTypes::User->value:
|
||||||
|
|
||||||
|
$data['status'] = UserStatus::Active->value;
|
||||||
|
|
||||||
|
$customer = Customer::create();
|
||||||
|
$customer->user()->create($data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return to_route('login.create')
|
||||||
|
->with('success', 'User registered successfully.');
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Registration Failed: '.$e->getMessage());
|
||||||
|
|
||||||
|
return back()
|
||||||
|
->withInput()
|
||||||
|
->with('error', 'Something went wrong during registration.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/Http/Controllers/Broker/BrokerDashboardController.php
Normal file
38
app/Http/Controllers/Broker/BrokerDashboardController.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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(GetBrokerStatsAction $getBrokerStatsAction)
|
||||||
|
{
|
||||||
|
return view('dashboards.broker.index')
|
||||||
|
->with('deals', $this->deals())
|
||||||
|
->with('stats', $getBrokerStatsAction->execute(Auth::user()));
|
||||||
|
}
|
||||||
|
|
||||||
|
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()->take(3)->get();
|
||||||
|
}
|
||||||
|
}
|
||||||
144
app/Http/Controllers/Broker/BrokerDealController.php
Normal file
144
app/Http/Controllers/Broker/BrokerDealController.php
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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\Str;
|
||||||
|
|
||||||
|
class BrokerDealController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('dashboards.broker.deals.index')
|
||||||
|
->with('deals', $this->deals());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('dashboards.broker.deals.create')
|
||||||
|
->with('categories', DealCategory::all('id', 'name'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(StoreBrokerDeal $request, FileService $fileService)
|
||||||
|
{
|
||||||
|
$data = $request->validated();
|
||||||
|
$data['slug'] = Str::slug($data['title']);
|
||||||
|
$data['user_id'] = $request->user()->id;
|
||||||
|
|
||||||
|
if ($request->hasFile('image')) {
|
||||||
|
$image = $request->file('image');
|
||||||
|
$data['image'] = $fileService->upload($image, 'images/deals',
|
||||||
|
$data['slug'].'.'.$image->extension());
|
||||||
|
}
|
||||||
|
|
||||||
|
Deal::unguard();
|
||||||
|
Deal::create($data);
|
||||||
|
Deal::reguard();
|
||||||
|
|
||||||
|
return to_route('broker.deals.index')->with('success', 'Deal has been created.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(Deal $deal)
|
||||||
|
{
|
||||||
|
return view('dashboards.broker.deals.edit')
|
||||||
|
->with('deal', $deal)
|
||||||
|
->with('categories', DealCategory::all('id', 'name'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(StoreBrokerDeal $request, Deal $deal, FileService $fileService)
|
||||||
|
{
|
||||||
|
$data = $request->validated();
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($deal, $data, $fileService, $request) {
|
||||||
|
$data['slug'] = Str::slug($data['title']);
|
||||||
|
|
||||||
|
if ($request->hasFile('image')) {
|
||||||
|
$fileService->delete($deal->image);
|
||||||
|
|
||||||
|
$image = $request->file('image');
|
||||||
|
$data['image'] = $fileService->upload($image, 'images/deals',
|
||||||
|
$data['slug'].'.'.$image->extension());
|
||||||
|
}
|
||||||
|
|
||||||
|
Deal::unguard();
|
||||||
|
$deal->update($data);
|
||||||
|
Deal::reguard();
|
||||||
|
|
||||||
|
});
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
Log::error($exception->getMessage(), $exception->getTrace());
|
||||||
|
|
||||||
|
return back()->with('error', 'Something gone wrong.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', 'Deal has been updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(Deal $deal, FileService $fileService)
|
||||||
|
{
|
||||||
|
// remove the image of deal
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
DB::transaction(function () use ($deal, $fileService) {
|
||||||
|
$fileService->delete($deal->image);
|
||||||
|
$deal->delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
Log::error($exception->getMessage(), $exception->getTrace());
|
||||||
|
|
||||||
|
return back()->with('error', 'Something gone wrong.');
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
app/Http/Controllers/Broker/BrokerProfileController.php
Normal file
90
app/Http/Controllers/Broker/BrokerProfileController.php
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Broker;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
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;
|
||||||
|
|
||||||
|
class BrokerProfileController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(User $profile, ProfileInitialsService $service)
|
||||||
|
{
|
||||||
|
// Get the broker profile
|
||||||
|
$broker = $profile->type;
|
||||||
|
|
||||||
|
// TODO: move this to middleware
|
||||||
|
if (! $broker instanceof Broker) {
|
||||||
|
abort(403, 'This user is not a broker.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$initials = $service->create($profile->name);
|
||||||
|
|
||||||
|
return view('dashboards.broker.profile.show')
|
||||||
|
->with('name', $profile->name)
|
||||||
|
->with('joinDate', $profile->created_at->format('F Y'))
|
||||||
|
->with('email', $profile->email)
|
||||||
|
->with('initials', $initials)
|
||||||
|
->with('verified', $broker->verified)
|
||||||
|
->with('location', $broker->location)
|
||||||
|
->with('bio', $broker->bio)
|
||||||
|
->with('phone', $broker->phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(User $profile)
|
||||||
|
{
|
||||||
|
return view('dashboards.broker.profile.edit')
|
||||||
|
->with('profile', $profile)
|
||||||
|
->with('broker', $profile->type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(StoreBrokerProfileRequest $request, User $profile)
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Separate the user fields from the broker fields
|
||||||
|
*/
|
||||||
|
$userFields = ['name', 'email'];
|
||||||
|
$data = collect($request->validated());
|
||||||
|
$userData = $data->only($userFields)->toArray();
|
||||||
|
$brokerData = $data->except($userFields)->toArray();
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($profile, $userData, $brokerData) {
|
||||||
|
$profile->update($userData);
|
||||||
|
$broker = $profile->type;
|
||||||
|
|
||||||
|
Broker::unguard();
|
||||||
|
$broker->update($brokerData);
|
||||||
|
Broker::reguard();
|
||||||
|
});
|
||||||
|
|
||||||
|
return to_route('broker.profile.show', $profile)
|
||||||
|
->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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(string $id)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/Http/Controllers/ChatController.php
Normal file
59
app/Http/Controllers/ChatController.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/Http/Controllers/CommentController.php
Normal file
44
app/Http/Controllers/CommentController.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Http/Controllers/ContactController.php
Normal file
26
app/Http/Controllers/ContactController.php
Normal 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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
app/Http/Controllers/ExplorePageController.php
Normal file
91
app/Http/Controllers/ExplorePageController.php
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Actions\AddRecentSearchAction;
|
||||||
|
use App\Enums\ExplorePageFilters;
|
||||||
|
use App\Enums\UserTypes;
|
||||||
|
use App\Http\Requests\ExploreSearchSortRequest;
|
||||||
|
use App\Models\Deal;
|
||||||
|
use App\Models\DealCategory;
|
||||||
|
use App\Queries\ExplorePageDealsQuery;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class ExplorePageController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
ExploreSearchSortRequest $request,
|
||||||
|
ExplorePageDealsQuery $query,
|
||||||
|
AddRecentSearchAction $addRecentSearchAction
|
||||||
|
) {
|
||||||
|
return view('explore')
|
||||||
|
->with('profileLink', $this->profileLink())
|
||||||
|
->with('categories', $this->categories())
|
||||||
|
->with('recentSearches', $this->recentSearches())
|
||||||
|
->with('deals', $this->deals($request, $query->builder(), $addRecentSearchAction));
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
\Illuminate\Support\defer(function () use ($action, $request) {
|
||||||
|
$action->execute($request->user(), ['query' => $request->search]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add category sorting filter
|
||||||
|
if ($request->has('category') && $request->get('category') !== null) {
|
||||||
|
$query->tap(fn ($q) => (new Deal)->filterByCategory($q, $request->category));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sorting filters
|
||||||
|
$query = match (ExplorePageFilters::tryFrom($request->sortBy)) {
|
||||||
|
ExplorePageFilters::Like => $query->orderBy('total_likes', 'desc'),
|
||||||
|
ExplorePageFilters::Click => $query->orderBy('total_redirection', 'desc'),
|
||||||
|
default => $query->orderByRaw(
|
||||||
|
'((COALESCE(total_likes, 0) * 70.0) / 100.0) + ((COALESCE(total_redirection, 0) * 30.0) / 100.0) DESC'
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
return $query->latest()->paginate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the link to the user's profile dashboard
|
||||||
|
* based on the user's role.
|
||||||
|
*
|
||||||
|
* @return string The URL for the user's dashboard.
|
||||||
|
*/
|
||||||
|
protected function profileLink(): string
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
return DealCategory::all(['id', 'name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function recentSearches(): Collection
|
||||||
|
{
|
||||||
|
if (! Auth::check()) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Auth::user()->recentSearches()->latest()->select(['id', 'query'])->get();
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/Http/Controllers/FollowController.php
Normal file
43
app/Http/Controllers/FollowController.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
136
app/Http/Controllers/Interaction/InteractionController.php
Normal file
136
app/Http/Controllers/Interaction/InteractionController.php
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
class InteractionController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Interact to a deal by Like or Favorite state
|
||||||
|
*
|
||||||
|
* @param InteractionType $type [InteractionType::Like, InteractionType::Favorite]
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check for existing like of the user with deal
|
||||||
|
$existingInteraction = $deal->interactions()
|
||||||
|
->where('user_id', Auth::id())
|
||||||
|
->where('type', $type)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// Delete the existing like if exists, else add the like
|
||||||
|
$message = '';
|
||||||
|
if ($existingInteraction) {
|
||||||
|
$existingInteraction->delete();
|
||||||
|
$message = ucfirst($type->value).' removed from deal';
|
||||||
|
} else {
|
||||||
|
$data = [
|
||||||
|
'type' => $type,
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
];
|
||||||
|
|
||||||
|
Interaction::unguard();
|
||||||
|
$deal->interactions()->create($data);
|
||||||
|
Interaction::reguard();
|
||||||
|
|
||||||
|
$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',
|
||||||
|
[
|
||||||
|
'deal_id' => $deal->id,
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'type' => $type,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'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)
|
||||||
|
{
|
||||||
|
if (blank($deal->link)) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
\Illuminate\Support\defer(function () use ($deal) {
|
||||||
|
try {
|
||||||
|
$interaction = $deal->interactions()->firstOrCreate([
|
||||||
|
'type' => InteractionType::Redirection,
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! $interaction->wasRecentlyCreated) {
|
||||||
|
$interaction->increment('count');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Error when redirecting a deal external link',
|
||||||
|
[
|
||||||
|
'deal_id' => $deal->id,
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
abort(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return redirect()->away($deal->link);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(Deal $deal)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$interaction = $deal->interactions()->firstOrCreate([
|
||||||
|
'type' => InteractionType::View,
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! $interaction->wasRecentlyCreated) {
|
||||||
|
$interaction->increment('count');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Error when view a deal',
|
||||||
|
[
|
||||||
|
'deal_id' => $deal->id,
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json(['error' => 'Something went wrong.'], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'View counted',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
app/Http/Controllers/Interaction/ReportController.php
Normal file
98
app/Http/Controllers/Interaction/ReportController.php
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Interaction;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\StoreReportRequest;
|
||||||
|
use App\Models\Deal;
|
||||||
|
use App\Models\Report;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class ReportController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(StoreReportRequest $request, Deal $deal)
|
||||||
|
{
|
||||||
|
$data = $request->validated();
|
||||||
|
$data['user_id'] = Auth::id();
|
||||||
|
|
||||||
|
// Check if the user already reported the deal
|
||||||
|
$alreadyReported = $deal->reports()->where('user_id', Auth::id())->exists();
|
||||||
|
if ($alreadyReported) {
|
||||||
|
return response()->json(['message' => 'You had already reported this deal'], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($data, $deal) {
|
||||||
|
Report::unguard();
|
||||||
|
Report::create($data)->deals()->attach($deal);
|
||||||
|
Report::reguard();
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Report submitted. Thank you for keeping DealHub safe'], 201);
|
||||||
|
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
Log::error('Error creating report', [
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'deal_id' => $deal->id,
|
||||||
|
'error' => $exception->getMessage(),
|
||||||
|
'trace' => $exception->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Error creating report'], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(Report $report)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Report $report)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Http/Controllers/PushSubscriptionController.php
Normal file
19
app/Http/Controllers/PushSubscriptionController.php
Normal 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.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/Http/Controllers/RecentSearchController.php
Normal file
15
app/Http/Controllers/RecentSearchController.php
Normal 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.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/Http/Controllers/StatsController.php
Normal file
36
app/Http/Controllers/StatsController.php
Normal 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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
app/Http/Controllers/User/UserProfileController.php
Normal file
73
app/Http/Controllers/User/UserProfileController.php
Normal 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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/Http/Middleware/EnsureUserFollowedBroker.php
Normal file
38
app/Http/Middleware/EnsureUserFollowedBroker.php
Normal 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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Http/Middleware/HasRole.php
Normal file
24
app/Http/Middleware/HasRole.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class HasRole
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next, ...$roles): Response
|
||||||
|
{
|
||||||
|
if (in_array($request->user()->role, $roles)) {
|
||||||
|
return $next($request);
|
||||||
|
} else {
|
||||||
|
abort('401');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Http/Requests/AuthenticateUserRequest.php
Normal file
31
app/Http/Requests/AuthenticateUserRequest.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class AuthenticateUserRequest 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, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email' => ['required', 'email', 'max:255'],
|
||||||
|
'password' => 'required',
|
||||||
|
'remember_me' => 'nullable|boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app/Http/Requests/CommentRequest.php
Normal file
20
app/Http/Requests/CommentRequest.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Http/Requests/ContactRequest.php
Normal file
35
app/Http/Requests/ContactRequest.php
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Http/Requests/DealOutboundRequest.php
Normal file
28
app/Http/Requests/DealOutboundRequest.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class DealOutboundRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Http/Requests/ExploreSearchSortRequest.php
Normal file
32
app/Http/Requests/ExploreSearchSortRequest.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Enums\ExplorePageFilters;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ExploreSearchSortRequest 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 [
|
||||||
|
'sortBy' => ['nullable', 'string', Rule::in(ExplorePageFilters::values())],
|
||||||
|
'search' => ['nullable', 'string', 'min:1', 'max:255'],
|
||||||
|
'category' => ['nullable', 'exists:deal_categories,id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Http/Requests/PushSubscriptionRequest.php
Normal file
30
app/Http/Requests/PushSubscriptionRequest.php
Normal 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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app/Http/Requests/SendMessageRequest.php
Normal file
20
app/Http/Requests/SendMessageRequest.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/Http/Requests/StoreBrokerDeal.php
Normal file
40
app/Http/Requests/StoreBrokerDeal.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreBrokerDeal extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user()->isBroker();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => 'required|min:10|max:255',
|
||||||
|
'description' => 'required|min:10|max:300',
|
||||||
|
'image' => [$this->isMethod('post') ? 'required' : 'nullable', 'image', 'mimes:jpeg,png,jpg', 'max:10240'],
|
||||||
|
'link' => 'nullable|url',
|
||||||
|
'deal_category_id' => 'required|exists:deal_categories,id',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'category_id.required' => 'The category field is required.',
|
||||||
|
'category_id.exists' => 'The category does not exist.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
45
app/Http/Requests/StoreBrokerProfileRequest.php
Normal file
45
app/Http/Requests/StoreBrokerProfileRequest.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use AllowDynamicProperties;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
#[AllowDynamicProperties]
|
||||||
|
class StoreBrokerProfileRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
45
app/Http/Requests/StoreCustomerProfileRequest.php
Normal file
45
app/Http/Requests/StoreCustomerProfileRequest.php
Normal 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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Http/Requests/StoreRegisterdUser.php
Normal file
35
app/Http/Requests/StoreRegisterdUser.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Enums\UserTypes;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
class StoreRegisterdUser 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, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'password' => ['required', 'confirmed', Password::min(8)->letters()->mixedCase()->numbers()->symbols()],
|
||||||
|
'email' => 'required|string|email|max:255|unique:users',
|
||||||
|
'role' => ['required', Rule::in(UserTypes::values()), Rule::notIn(UserTypes::Admin->value)],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Http/Requests/StoreReportRequest.php
Normal file
31
app/Http/Requests/StoreReportRequest.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Enums\ReportType;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class StoreReportRequest 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 [
|
||||||
|
'type' => ['required', Rule::in(ReportType::values())],
|
||||||
|
'description' => 'required|string|min:10|max:500',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Http/Resources/ActiveUsersStatsCollection.php
Normal file
16
app/Http/Resources/ActiveUsersStatsCollection.php
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app/Http/Resources/ActiveUsersStatsResource.php
Normal file
20
app/Http/Resources/ActiveUsersStatsResource.php
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Http/Resources/BrokerRoleResource.php
Normal file
23
app/Http/Resources/BrokerRoleResource.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class BrokerRoleResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'bio' => $this->bio,
|
||||||
|
'location' => $this->location,
|
||||||
|
'phone' => $this->phone,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/Http/Resources/DealResource.php
Normal file
33
app/Http/Resources/DealResource.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
|
|
||||||
|
class DealResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'title' => $this->title,
|
||||||
|
'description' => $this->description,
|
||||||
|
'image' => asset('storage/'.$this->image),
|
||||||
|
'link' => $this->link !== null ? URL::signedRoute('redirect', $this->id) : null,
|
||||||
|
'category' => $this->whenLoaded('category'),
|
||||||
|
'broker' => new UserResource($this->whenLoaded('broker')),
|
||||||
|
'totalLikes' => $this->total_likes,
|
||||||
|
'totalRedirection' => $this->total_redirection,
|
||||||
|
'isLiked' => $this->is_liked,
|
||||||
|
'isFavorite' => $this->is_favorite,
|
||||||
|
'isFollowed' => $this->is_followed,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Http/Resources/DealsCountByCategoryCollection.php
Normal file
16
app/Http/Resources/DealsCountByCategoryCollection.php
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
17
app/Http/Resources/DealsCountByCategoryResource.php
Normal file
17
app/Http/Resources/DealsCountByCategoryResource.php
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Http/Resources/UserResource.php
Normal file
24
app/Http/Resources/UserResource.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class UserResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'name' => $this->name,
|
||||||
|
'email' => $this->email,
|
||||||
|
'role' => new BrokerRoleResource($this->whenLoaded('role')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/Mail/PasswordResetMail.php
Normal file
38
app/Mail/PasswordResetMail.php
Normal 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
29
app/Models/Admin.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
60
app/Models/Broker.php
Normal file
60
app/Models/Broker.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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 [
|
||||||
|
'verified' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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
47
app/Models/Comment.php
Normal 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
45
app/Models/Customer.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
199
app/Models/Deal.php
Normal file
199
app/Models/Deal.php
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\InteractionType;
|
||||||
|
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;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function category(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(DealCategory::class, 'deal_category_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function interactions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Interaction::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
#[Scope]
|
||||||
|
public function WithActiveDeals(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->where('active', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a query to determine if the current user has liked and favorite a deal
|
||||||
|
*/
|
||||||
|
#[Scope]
|
||||||
|
public function WithCurrentUserInteractions(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->withExists([
|
||||||
|
'interactions as is_liked' => function ($query) {
|
||||||
|
$query->where('user_id', Auth::id())
|
||||||
|
->where('type', InteractionType::Like);
|
||||||
|
},
|
||||||
|
'interactions as is_favorite' => function ($query) {
|
||||||
|
$query->where('user_id', Auth::id())
|
||||||
|
->where('type', InteractionType::Favorite);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a query to get total like count per deal
|
||||||
|
*/
|
||||||
|
#[Scope]
|
||||||
|
public function WithLikePerDeal(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->withCount([
|
||||||
|
'interactions as total_likes' => function ($query) {
|
||||||
|
$query->where('type', InteractionType::Like);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a query to get click count per deal
|
||||||
|
*/
|
||||||
|
#[Scope]
|
||||||
|
public function WithRedirectionPerDeal(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->withSum([
|
||||||
|
'interactions as total_redirection' => function ($query) {
|
||||||
|
$query->where('type', InteractionType::Redirection);
|
||||||
|
},
|
||||||
|
], '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(function (Builder $query) use ($search) {
|
||||||
|
$query->where('title', 'LIKE', "%$search%")
|
||||||
|
->orWhereRelation('broker', 'name', 'LIKE', "%$search%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a category filter in a query
|
||||||
|
*/
|
||||||
|
#[Scope]
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Models/DealCategory.php
Normal file
42
app/Models/DealCategory.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
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
46
app/Models/Follow.php
Normal 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
55
app/Models/Inbox.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/Models/Interaction.php
Normal file
56
app/Models/Interaction.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\InteractionType;
|
||||||
|
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 = [
|
||||||
|
'type',
|
||||||
|
'user_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Deal::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deal(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Deal::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => InteractionType::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/Models/Message.php
Normal file
62
app/Models/Message.php
Normal 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
41
app/Models/PageVisit.php
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Models/RecentSearch.php
Normal file
35
app/Models/RecentSearch.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
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'];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/Models/Report.php
Normal file
69
app/Models/Report.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deals(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Deal::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => ReportType::class,
|
||||||
|
'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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,13 +4,75 @@
|
|||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
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\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
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
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, HasPushSubscriptions, Notifiable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
@ -21,6 +83,8 @@ class User extends Authenticatable
|
|||||||
'name',
|
'name',
|
||||||
'email',
|
'email',
|
||||||
'password',
|
'password',
|
||||||
|
'status',
|
||||||
|
'role',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,4 +109,72 @@ protected function casts(): array
|
|||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns model of User's role type
|
||||||
|
*/
|
||||||
|
public function type(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo('role');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function interactions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
app/Notifications/NewContactNotification.php
Normal file
37
app/Notifications/NewContactNotification.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/Notifications/NewDealNotification.php
Normal file
40
app/Notifications/NewDealNotification.php
Normal 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]));
|
||||||
|
}
|
||||||
|
}
|
||||||
43
app/Notifications/ReportRejectedNotificationToUser.php
Normal file
43
app/Notifications/ReportRejectedNotificationToUser.php
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/Notifications/ReportResolvedNotificationToBroker.php
Normal file
46
app/Notifications/ReportResolvedNotificationToBroker.php
Normal 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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
47
app/Notifications/ReportResolvedNotificationToUser.php
Normal file
47
app/Notifications/ReportResolvedNotificationToUser.php
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Queries/ExplorePageDealsQuery.php
Normal file
35
app/Queries/ExplorePageDealsQuery.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Queries;
|
||||||
|
|
||||||
|
use App\Models\Deal;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
final readonly class ExplorePageDealsQuery
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return Builder<Deal>
|
||||||
|
*/
|
||||||
|
public function builder(): Builder
|
||||||
|
{
|
||||||
|
return Deal::query()
|
||||||
|
->select([
|
||||||
|
'id', 'title', 'description', 'image', 'active', 'slug', 'link',
|
||||||
|
'deal_category_id', 'user_id',
|
||||||
|
])
|
||||||
|
// Select additional details
|
||||||
|
->with([
|
||||||
|
'category:id,name',
|
||||||
|
'broker' => function ($query) {
|
||||||
|
$query->select('id', 'name', 'email', 'role_type', 'role_id')
|
||||||
|
->with('type');
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Queries/PageVisitStatsQuery.php
Normal file
22
app/Queries/PageVisitStatsQuery.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Services/FileService.php
Normal file
24
app/Services/FileService.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class FileService
|
||||||
|
{
|
||||||
|
public function upload(UploadedFile $file, string $folder, string $filename): string
|
||||||
|
{
|
||||||
|
// This prevents overriding of same image name
|
||||||
|
$filename = time().$filename.'.'.$file->extension();
|
||||||
|
|
||||||
|
return $file->storeAs($folder, $filename, 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(?string $path): void
|
||||||
|
{
|
||||||
|
if ($path && Storage::disk('public')->exists($path)) {
|
||||||
|
Storage::disk('public')->delete($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/Services/OTPService.php
Normal file
29
app/Services/OTPService.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app/Services/ProfileInitialsService.php
Normal file
20
app/Services/ProfileInitialsService.php
Normal 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('');
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user