feature(admin-panel): manage broker section

- admin can edit, approve or reject broker registration
- admin can edit, delete or impersonate as broker
This commit is contained in:
kusowl 2026-01-28 13:44:05 +05:30
parent aa3056e1d1
commit c087126080
18 changed files with 428 additions and 372 deletions

View File

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

View File

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

View File

@ -35,7 +35,7 @@ public function update(StoreCustomerProfileRequest $request, Customer $customer,
return to_route('admin.customers.index') return to_route('admin.customers.index')
->with('success', 'Profile updated successfully.'); ->with('success', 'Profile updated successfully.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error('Customer Profile Update Failed: '.$e->getMessage(), $e->getTrace()); Log::error('Customer Profile Update Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->withInput()->with('error', 'Something went wrong.'); return back()->withInput()->with('error', 'Something went wrong.');
} }
@ -51,7 +51,7 @@ public function destroy(Customer $customer)
return back()->with('success', 'Customer deleted successfully.'); return back()->with('success', 'Customer deleted successfully.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error('Customer Delete Failed: '.$e->getMessage(), $e->getTrace()); Log::error('Customer Delete Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->with('error', 'Something went wrong.'); return back()->with('error', 'Something went wrong.');
} }

View File

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

View File

@ -7,6 +7,8 @@
class Broker extends Model class Broker extends Model
{ {
protected $fillable = ['bio', 'location', 'phone'];
protected function casts(): array protected function casts(): array
{ {
return [ return [

View File

@ -16,7 +16,6 @@
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",
"laradumps/laradumps": "^5.0",
"laravel/pail": "^1.2.2", "laravel/pail": "^1.2.2",
"laravel/pint": "^1.24", "laravel/pint": "^1.24",
"laravel/sail": "^1.41", "laravel/sail": "^1.41",

484
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -69,3 +69,7 @@ dialog {
dialog[open] { dialog[open] {
animation: appear 300ms ease-in-out; animation: appear 300ms ease-in-out;
} }
.ui-table tr td,th {
@apply py-1
}

View File

@ -0,0 +1,44 @@
@props(['activeBrokers'])
<x-dashboard.card class="w-full">
<h3 class="text-md font-bold mb-4">Active Brokers</h3>
<x-ui.table>
<x-ui.table.head>
<th>Name</th>
<th>Email</th>
<th>Location</th>
</x-ui.table.head>
@forelse($activeBrokers as $broker)
<x-ui.table.row>
<td class="text-sm px-4 text-center">{{$broker->user->name}}</td>
<td class="text-sm px-4 text-center">{{$broker->user->email}}</td>
<td class="text-sm px-4 text-center">{{$broker->location}}</td>
<x-slot:actions>
<div class="flex items-center justify-center space-x-2 py-1 px-4">
<x-ui.button-sm tooltip="Edit Broker" :link="route('admin.brokers.edit', $broker)" variant="ghost">
<x-heroicon-o-pencil-square class="w-4"/>
</x-ui.button-sm>
<form action="{{route('admin.brokers.destroy', $broker)}}"
onsubmit="return confirm('Are you sure to delete this ?')" method="post"
class=" h-full items-center flex justify-center">
@csrf
@method('DELETE')
<x-ui.button-sm tooltip="Remove Broker" variant="red">
<x-heroicon-o-trash class="w-4"/>
</x-ui.button-sm>
</form>
<x-ui.button-sm tooltip="Login as this user" :link="route('impersonate', ['user' => $broker->user->id])"
variant="neutral">
<x-heroicon-o-camera class="w-4"/>
</x-ui.button-sm>
</div>
</x-slot:actions>
</x-ui.table.row>
@empty
<x-ui.table.row>
<td colspan="4" class="text-center text-sm text-accent-600 py-2">No Broker found</td>
</x-ui.table.row>
@endforelse
</x-ui.table>
</x-dashboard.card>

View File

@ -0,0 +1,49 @@
@props(['pendingBrokers'])
<x-dashboard.card class="w-full">
<h3 class="text-md font-bold mb-4">Broker Approvals</h3>
<x-ui.table>
<x-ui.table.head>
<th>Name</th>
<th>Email</th>
<th>Location</th>
</x-ui.table.head>
@forelse($pendingBrokers as $broker)
<x-ui.table.row>
<td class="text-sm px-4 text-center">{{$broker->user->name}}</td>
<td class="text-sm px-4 text-center">{{$broker->user->email}}</td>
<td class="text-sm px-4 text-center">{{$broker->location}}</td>
<x-slot:actions>
<div class="flex items-center justify-center space-x-2 py-1 px-4">
<form action="{{route('admin.brokers.approve', $broker)}}"
method="post"
class=" h-full items-center flex justify-center">
@csrf
<x-ui.button-sm variant="green" tooltip="Approve Broker">
<x-heroicon-o-check class="w-4"/>
</x-ui.button-sm>
</form>
<form action="{{route('admin.brokers.destroy', $broker)}}"
onsubmit="return confirm('Are you sure to reject this ?')" method="post"
class=" h-full items-center flex justify-center">
@csrf
@method('DELETE')
<x-ui.button-sm tooltip="Reject Broker" variant="red">
<x-heroicon-o-x-mark class="w-4"/>
</x-ui.button-sm>
</form>
<x-ui.button-sm tooltip="Edit Details" :link="route('admin.brokers.edit', $broker)"
variant="ghost">
<x-heroicon-o-pencil-square class="w-4"/>
</x-ui.button-sm>
</div>
</x-slot:actions>
</x-ui.table.row>
@empty
<x-ui.table.row>
<td colspan="4" class="text-center text-sm text-accent-600 py-2">No Broker found</td>
</x-ui.table.row>
@endforelse
</x-ui.table>
</x-dashboard.card>

View File

@ -20,13 +20,16 @@ class="flex flex-col p-4 pt-6 justify-between font-medium h-full w-full overflow
<p class="sidebar-text transition-opacity duration-300 ease-in-out ">Dashboard</p> <p class="sidebar-text transition-opacity duration-300 ease-in-out ">Dashboard</p>
</x-dashboard.broker.sidebar.item> </x-dashboard.broker.sidebar.item>
<x-dashboard.broker.sidebar.item :active="\Illuminate\Support\Facades\Route::is('admin.customers.*')" :link="route('admin.customers.index')"> <x-dashboard.broker.sidebar.item
:active="\Illuminate\Support\Facades\Route::is('admin.customers.*')"
:link="route('admin.customers.index')">
<x-heroicon-o-users class="min-w-5 w-5"/> <x-heroicon-o-users class="min-w-5 w-5"/>
<p class="sidebar-text transition-opacity duration-300 ease-in-out ">Manage Customers</p> <p class="sidebar-text transition-opacity duration-300 ease-in-out ">Manage Customers</p>
</x-dashboard.broker.sidebar.item> </x-dashboard.broker.sidebar.item>
<x-dashboard.broker.sidebar.item :link="route('broker.deals.index')"> <x-dashboard.broker.sidebar.item :active="\Illuminate\Support\Facades\Route::is('admin.brokers.*')"
<svg class="w-5 min-w-5" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#000000" stroke="#000000" stroke-width="0.2"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M512.536516 506.562145l-112.606908 197.063113 112.606908 225.21484 112.607932-225.21484z" fill="#000000"></path><path d="M680.856096 476.172205c36.824036-40.482378 59.488761-94.060249 59.488761-152.967444 0-125.613323-102.193994-227.807318-227.807317-227.807318s-227.807318 102.193994-227.807318 227.807318c0 58.906171 22.664726 112.485066 59.488761 152.967444-58.595933 32.817572-187.285008 138.139536-187.285008 430.442414 0 12.273313 9.951141 22.225479 22.225479 22.225479 12.273313 0 22.225479-9.951141 22.225479-22.225479 0-304.35708 145.515606-384.465868 178.379253-398.671253 37.433247 26.981426 83.210136 43.069736 132.773354 43.069737 49.564241 0 95.342154-16.08831 132.775401-43.070761 32.907674 14.226887 178.376182 94.379701 178.376182 398.673301 0 12.273313 9.951141 22.225479 22.225479 22.225479s22.225479-9.951141 22.225478-22.225479c-0.001024-292.303902-128.690099-397.625866-187.283984-430.443438zM329.179132 323.204761c0-101.098437 82.258947-183.357384 183.357384-183.357384s183.357384 82.258947 183.357384 183.357384-82.258947 183.357384-183.357384 183.357384-183.357384-82.258947-183.357384-183.357384z" fill="#020b07"></path></g></svg> :link="route('admin.brokers.index')">
<x-heroicon-o-user class="min-w-5 w-5"/>
<p class="sidebar-text transition-opacity duration-300 ease-in-out">Manage Brokers</p> <p class="sidebar-text transition-opacity duration-300 ease-in-out">Manage Brokers</p>
</x-dashboard.broker.sidebar.item> </x-dashboard.broker.sidebar.item>

View File

@ -1,19 +1,30 @@
@props(['variant' => '', 'link' => '', 'external' => false]) @props(['variant' => '', 'link' => '', 'external' => false, 'tooltip' => ''])
@php @php
$variants = [ $variants = [
'neutral' => 'bg-primary-600 text-white', 'neutral' => 'bg-primary-600 text-white',
'ghost' => 'bg-gray-100 text-black text-sm', 'ghost' => 'bg-gray-200 text-gray-700 text-sm',
'red' => 'bg-red-500 text-red-100 text-sm' 'red' => 'bg-red-100 text-red-500 text-sm',
]; 'green' => 'bg-green-100 text-green-700 text-sm',
];
$variantClass = $variants[$variant] ?? ''; $variantClass = $variants[$variant] ?? '';
@endphp @endphp
@if($link !== '') <div class="relative group w-fit hover:z-1">
<a href="{{$link}}" @if($external) target="_blank" @endif {{$attributes->merge(['class' => "inline-flex px-2 py-1 text-xs rounded-md font-medium hover:opacity-80 hover: active:scale-80 transition-all duration-300 ease-in-out $variantClass"])}}> @if($link !== '')
<a href="{{$link}}"
@if($external) target="_blank" @endif {{$attributes->merge(['class' => "inline-flex px-2 py-1 text-xs rounded-md font-medium hover:opacity-80 hover: active:scale-80 transition-all duration-300 ease-in-out $variantClass"])}}>
{{$slot}} {{$slot}}
</a> </a>
@else @else
<button {{$attributes->merge(['class' => "px-2 py-1 text-xs rounded-md font-medium hover:opacity-80 hover:scale-110 active:scale-80 transition-all duration-300 ease-in-out $variantClass"])}}> <button {{$attributes->merge(['class' => "px-2 py-1 text-xs rounded-md font-medium hover:opacity-80 hover:scale-110 active:scale-80 transition-all duration-300 ease-in-out $variantClass"])}}>
<p>{{$slot}}</p> <p>{{$slot}}</p>
</button>
@endif </button>
@endif
@if($tooltip !== '')
<span
class="absolute top-full mt-2 hidden group-hover:block right-0 py-1 px-2 rounded-lg bg-gray-900 text-xs whitespace-nowrap text-white">
{{$tooltip}}
</span>
@endif
</div>

View File

@ -1,5 +1,5 @@
<div class="pt-2 rounded-lg border border-gray-200"> <div class="pt-2 rounded-lg border border-gray-200">
<table class="table-auto w-full "> <table class="ui-table table-auto w-full ">
{{$slot}} {{$slot}}
</table> </table>
</div> </div>

View File

@ -1,7 +1,7 @@
<tr class=" border-t border-t-gray-200 "> <tr class=" border-t border-t-gray-200 ">
{{$slot}} {{$slot}}
@if(($actions ?? '') !== '') @if(($actions ?? '') !== '')
<td class="border-l border-l-gray-200"> <td class="border-l border-l-gray-200 w-8">
{{$actions ?? ''}} {{$actions ?? ''}}
</td> </td>
@endif @endif

View File

@ -0,0 +1,13 @@
<x-dashboard.admin.layout title="Edit Broker">
<x-slot:heading>
<x-dashboard.page-heading
title="Edit Broker Profile"
description="Update broker profile information."
:back-link="$backLink"
/>
</x-slot:heading>
<div class="flex items-center justify-center px-4 pb-4 pt-0 md:px-8 md:pb-8">
<x-dashboard.user.edit-profile-card :back-link="$backLink" :action-link="$actionLink" :profile="$profile" />
</div>
</x-dashboard.admin.layout>

View File

@ -0,0 +1,12 @@
<x-dashboard.admin.layout title="Manage Brokers">
<x-slot:heading>
<x-dashboard.page-heading
title="Manage Brokers"
description="Edit, Delete and Login as Broker"
/>
</x-slot:heading>
<div class="flex flex-col items-center justify-center space-y-4 md:space-y-8 px-4 pb-4 pt-0 md:px-8 md:pb-8">
<x-dashboard.admin.broker-approval :pending-brokers="$pendingBrokers" />
<x-dashboard.admin.active-broker :active-brokers="$activeBrokers" />
</div>
</x-dashboard.admin.layout>

View File

@ -44,7 +44,7 @@ class=" h-full items-center flex justify-center">
</x-ui.table.row> </x-ui.table.row>
@empty @empty
<x-ui.table.row> <x-ui.table.row>
<td colspan="2" class="text-center text-sm text-accent-600 py-2">No Deals found</td> <td colspan="2" class="text-center text-sm text-accent-600 py-2">No Customer found</td>
</x-ui.table.row> </x-ui.table.row>
@endforelse @endforelse
</x-ui.table> </x-ui.table>

View File

@ -2,6 +2,7 @@
use App\Enums\UserTypes; use App\Enums\UserTypes;
use App\Http\Controllers\Admin\AdminDashboardController; use App\Http\Controllers\Admin\AdminDashboardController;
use App\Http\Controllers\Admin\BrokerController;
use App\Http\Controllers\Admin\CustomerController; use App\Http\Controllers\Admin\CustomerController;
use App\Http\Middleware\HasRole; use App\Http\Middleware\HasRole;
@ -11,4 +12,7 @@
->group(function () { ->group(function () {
Route::get('dashboard', AdminDashboardController::class)->name('dashboard'); Route::get('dashboard', AdminDashboardController::class)->name('dashboard');
Route::resource('customers', CustomerController::class)->except('show', 'create', 'store'); Route::resource('customers', CustomerController::class)->except('show', 'create', 'store');
Route::resource('brokers', BrokerController::class)->except('show', 'create', 'store');
Route::post('/brokers/approve/{broker}', [BrokerController::class, 'approve'])->name('brokers.approve');
}); });