diff --git a/Modules/Client/Entities/Client.php b/Modules/Client/Entities/Client.php index 597f5482d2..8e40bb6a9c 100644 --- a/Modules/Client/Entities/Client.php +++ b/Modules/Client/Entities/Client.php @@ -13,6 +13,7 @@ use Modules\Invoice\Entities\LedgerAccount; use Modules\Invoice\Services\InvoiceService; use Modules\Project\Entities\Project; +use Modules\Prospect\Entities\Prospect; use Modules\User\Entities\User; class Client extends Model @@ -396,6 +397,16 @@ public function hasCustomInvoiceTemplate() return false; } + public function prospect() + { + return $this->belongsTo(Prospect::class); + } + + public function getClientsAttribute() + { + return $this->query()->orderBy('name')->get(); + } + protected static function booted() { static::addGlobalScope(new ClientGlobalScope); diff --git a/Modules/Prospect/Config/config.php b/Modules/Prospect/Config/config.php index 5b307ee26c..5ff0f7a413 100644 --- a/Modules/Prospect/Config/config.php +++ b/Modules/Prospect/Config/config.php @@ -7,4 +7,13 @@ 'existing' => 'Existing', 'dormant' => 'Dormant', ], + 'status' => [ + 'pending' => 'Pending', + 'proposal-sent' => 'Proposal Sent', + 'discussions-ongoing' => 'Discussions Ongoing', + 'converted' => 'Converted', + 'rejected' => 'Rejected', + 'client-unresponsive' => 'Client Unresponsive', + 'final-decision-pending' => 'Final Decision Pending', + ], ]; diff --git a/Modules/Prospect/Database/Migrations/2024_10_25_220553_prospect-insights.php b/Modules/Prospect/Database/Migrations/2024_10_25_220553_prospect-insights.php new file mode 100644 index 0000000000..71bb8ec8d6 --- /dev/null +++ b/Modules/Prospect/Database/Migrations/2024_10_25_220553_prospect-insights.php @@ -0,0 +1,36 @@ +bigIncrements('id'); + $table->unsignedBigInteger('prospect_id'); + $table->integer('user_id')->unsigned(); + $table->longText('insight_learning'); + $table->timestamps(); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('prospect_id')->references('id')->on('prospects')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('prospect_insights'); + } +} diff --git a/Modules/Prospect/Database/Migrations/2024_10_28_221259_added-client-id-project-name.php b/Modules/Prospect/Database/Migrations/2024_10_28_221259_added-client-id-project-name.php new file mode 100644 index 0000000000..62320a32a9 --- /dev/null +++ b/Modules/Prospect/Database/Migrations/2024_10_28_221259_added-client-id-project-name.php @@ -0,0 +1,39 @@ +unsignedBigInteger('client_id')->nullable(); + $table->string('project_name')->nullable(); + $table->string('organization_name')->nullable()->change(); + + $table->foreign('client_id')->references('id')->on('clients'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('prospects', function (Blueprint $table) { + $table->dropForeign(['client_id']); + $table->dropColumn('client_id'); + $table->dropColumn('project_name'); + $table->string('organization_name')->nullable(false)->change(); + }); + } +} diff --git a/Modules/Prospect/Entities/Prospect.php b/Modules/Prospect/Entities/Prospect.php index 85d850d0d2..918deda0c1 100644 --- a/Modules/Prospect/Entities/Prospect.php +++ b/Modules/Prospect/Entities/Prospect.php @@ -4,6 +4,7 @@ use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; +use Modules\Client\Entities\Client; use Modules\User\Entities\User; class Prospect extends Model @@ -26,4 +27,27 @@ public function getFormattedDate($date) return $date ? Carbon::parse($date)->format('M d, Y') : '-'; } + + public function client() + { + return $this->belongsTo(Client::class, 'client_id'); + } + + public function getProspectDisplayName() + { + return $this->organization_name ?? optional($this->client)->name ?? 'N/A'; + } + + public function formattedAmount($amount) + { + $formattedAmount = preg_replace('/\B(?=(\d{2})+(?!\d))/', ',', substr($amount, 0, -3)) . + ',' . substr($amount, -3); + + return $formattedAmount; + } + + public function insights() + { + return $this->hasMany(ProspectInsight::class); + } } diff --git a/Modules/Prospect/Entities/ProspectComment.php b/Modules/Prospect/Entities/ProspectComment.php index 2dd59ea4e3..21dbcda366 100644 --- a/Modules/Prospect/Entities/ProspectComment.php +++ b/Modules/Prospect/Entities/ProspectComment.php @@ -18,6 +18,6 @@ public function prospect() public function user() { - return $this->belongsTo(User::class); + return $this->belongsTo(User::class, 'user_id'); } } diff --git a/Modules/Prospect/Entities/ProspectInsight.php b/Modules/Prospect/Entities/ProspectInsight.php new file mode 100644 index 0000000000..c6fd114f15 --- /dev/null +++ b/Modules/Prospect/Entities/ProspectInsight.php @@ -0,0 +1,23 @@ +belongsTo(Prospect::class); + } + + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/Modules/Prospect/Http/Controllers/ProspectController.php b/Modules/Prospect/Http/Controllers/ProspectController.php index 9a7a08b8c1..92b4b9b9e4 100644 --- a/Modules/Prospect/Http/Controllers/ProspectController.php +++ b/Modules/Prospect/Http/Controllers/ProspectController.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Support\Renderable; use Illuminate\Http\Request; use Illuminate\Routing\Controller; +use Modules\Client\Entities\Client; use Modules\Client\Entities\Country; use Modules\Prospect\Entities\Prospect; use Modules\Prospect\Http\Requests\ProspectRequest; @@ -26,14 +27,10 @@ public function __construct(ProspectService $service) */ public function index() { - $prospects = Prospect::with('pocUser')->get(); - $countries = Country::all(); - $currencySymbols = $countries->pluck('currency_symbol', 'currency'); + $requestData = request()->all(); + $data = $this->service->index($requestData); - return view('prospect::index', [ - 'prospects' => $prospects, - 'currencySymbols' => $currencySymbols, - ]); + return view('prospect::index', $data); } /** @@ -44,11 +41,13 @@ public function create() { $countries = Country::all(); $user = new User(); + $client = new Client(); $activeUsers = $user->active_users; return view('prospect::create', [ 'users' => $activeUsers, 'countries' => $countries, + 'clients' => $client->clients, ]); } @@ -59,9 +58,9 @@ public function create() public function store(ProspectRequest $request) { $validated = $request->validated(); - $data = $this->service->store($validated); + $this->service->store($validated); - return $data; + return redirect()->route('prospect.index')->with('status', 'Prospect created successfully!'); } /** @@ -70,7 +69,7 @@ public function store(ProspectRequest $request) */ public function show($id) { - $prospect = Prospect::with(['pocUser', 'comments', 'comments.user'])->find($id); + $prospect = Prospect::with(['comments', 'insights'])->find($id); $countries = Country::all(); $currencySymbols = $countries->pluck('currency_symbol', 'currency'); @@ -87,26 +86,28 @@ public function show($id) */ public function edit($id) { - $prospect = Prospect::with(['pocUser', 'comments'])->find($id); + $prospect = Prospect::with(['comments'])->find($id); $countries = Country::all(); $user = new User(); $activeUsers = $user->active_users; + $client = new Client(); return view('prospect::edit', [ 'prospect' => $prospect, 'users' => $activeUsers, 'countries' => $countries, + 'clients' => $client->clients, ]); } /** * Update the specified resource in storage. * @param Request $request - * @param int $id + * @param Prospect $prospect */ - public function update(Request $request, $id) + public function update(Request $request, Prospect $prospect) { - $data = $this->service->update($request, $id); + $data = $this->service->update($request, $prospect); return $data; } @@ -124,4 +125,14 @@ public function commentUpdate(Request $request, $id) return redirect()->route('prospect.show', $id)->with('status', 'Comment updated successfully!'); } + + public function insightsUpdate(Request $request, $id) + { + $validated = $request->validate([ + 'insight_learning' => 'required', + ]); + $this->service->insightsUpdate($validated, $id); + + return redirect()->route('prospect.show', $id)->with('status', 'Prospect Insights updated successfully!'); + } } diff --git a/Modules/Prospect/Http/Requests/ProspectRequest.php b/Modules/Prospect/Http/Requests/ProspectRequest.php index fe1ff644d3..6d5a5ac442 100644 --- a/Modules/Prospect/Http/Requests/ProspectRequest.php +++ b/Modules/Prospect/Http/Requests/ProspectRequest.php @@ -14,7 +14,7 @@ class ProspectRequest extends FormRequest public function rules() { return [ - 'org_name' => 'required', + 'org_name' => 'nullable', 'poc_user_id' => 'required', 'proposal_sent_date' => 'nullable|date', 'domain' => 'nullable', @@ -26,9 +26,30 @@ public function rules() 'rfp_link' => 'nullable|url', 'proposal_link' => 'nullable|url', 'currency' => 'nullable', + 'client_id' => 'nullable|exists:clients,id', + 'project_name' => 'nullable', ]; } + /** + * Configure the validator instance. + * + * @param \Illuminate\Validation\Validator $validator + * @return void + */ + public function withValidator($validator) + { + if ($this->customer_type === 'new') { + $validator->addRules([ + 'org_name' => 'required', + ]); + } elseif ($this->customer_type === 'existing') { + $validator->addRules([ + 'client_id' => 'required', + ]); + } + } + /** * Determine if the user is authorized to make this request. * @@ -47,7 +68,8 @@ public function authorize() public function messages() { return [ - 'org_name.required' => 'Organization name is required', + 'org_name.required' => 'Organization name is required when customer type is new.', + 'client_id.required' => 'Please select an organization when customer type is existing.', 'poc_user_id.required' => 'Point of contact user ID is required', ]; } diff --git a/Modules/Prospect/Resources/assets/js/app.js b/Modules/Prospect/Resources/assets/js/app.js index e69de29bb2..2c48291b48 100644 --- a/Modules/Prospect/Resources/assets/js/app.js +++ b/Modules/Prospect/Resources/assets/js/app.js @@ -0,0 +1,38 @@ +const CUSTOMER_TYPES = { + NEW: 'new', + EXISTING: 'existing', + DORMANT: 'dormant' +}; +document.addEventListener('DOMContentLoaded', function () { + const customerTypeField = document.getElementById('customer_type'); + const orgNameTextField = document.getElementById('org_name_text_field'); + const orgNameSelectField = document.getElementById('org_name_select_field'); + const orgNameTextInput = document.getElementById('org_name'); + const orgNameSelectInput = document.getElementById('org_name_select'); + + function toggleOrgNameField() { + if (customerTypeField.value === CUSTOMER_TYPES.NEW) { + orgNameTextField.classList.remove('d-none'); + orgNameSelectField.classList.add('d-none'); + orgNameTextInput.required = true; + orgNameSelectInput.value = ''; + orgNameSelectInput.required = false; + } else if (customerTypeField.value === CUSTOMER_TYPES.EXISTING) { + orgNameTextField.classList.add('d-none'); + orgNameSelectField.classList.remove('d-none'); + orgNameSelectInput.required = true; + orgNameTextInput.required = false; + orgNameTextInput.value = null; + } else { + orgNameTextField.classList.remove('d-none'); + orgNameSelectField.classList.add('d-none'); + orgNameTextInput.required = true; + orgNameSelectInput.value = ''; + orgNameSelectInput.required = false; + } + } + + toggleOrgNameField(); + + customerTypeField.addEventListener('change', toggleOrgNameField); +}); diff --git a/Modules/Prospect/Resources/views/create.blade.php b/Modules/Prospect/Resources/views/create.blade.php index 9a57422a9d..17716d2e50 100644 --- a/Modules/Prospect/Resources/views/create.blade.php +++ b/Modules/Prospect/Resources/views/create.blade.php @@ -16,12 +16,34 @@
- + + +
+
+ + placeholder="Enter Organization Name" value="{{ old('org_name') }}" required>
-
+ +
+ + +
+
+
+
+ +
+ + +
-
@@ -46,26 +73,13 @@ placeholder="Enter Domain" value="{{ old('domain') }}">
-
- - -
-
+ + @foreach (config('prospect.status') as $key => $status) + + @endforeach + +
-
- - -
-
-
-
-
-
+
diff --git a/Modules/Prospect/Resources/views/edit.blade.php b/Modules/Prospect/Resources/views/edit.blade.php index 62b98d02a3..7f8b2d9749 100644 --- a/Modules/Prospect/Resources/views/edit.blade.php +++ b/Modules/Prospect/Resources/views/edit.blade.php @@ -7,11 +7,15 @@ @@ -25,6 +29,10 @@
@include('prospect::subviews.edit-prospect-comment')
+ +
+ @include('prospect::subviews.edit-prospect-insights') +
@endsection diff --git a/Modules/Prospect/Resources/views/index.blade.php b/Modules/Prospect/Resources/views/index.blade.php index 43358d8b66..5993fbdea1 100644 --- a/Modules/Prospect/Resources/views/index.blade.php +++ b/Modules/Prospect/Resources/views/index.blade.php @@ -2,6 +2,7 @@ @section('content') @includeWhen(session('status'), 'toast', ['message' => session('status')])
+ @include('prospect::menu-header') @if (session('success'))
+ +
+ {{ $prospects->appends(request()->query())->links() }} +
@endsection diff --git a/Modules/Prospect/Resources/views/menu-header.blade.php b/Modules/Prospect/Resources/views/menu-header.blade.php new file mode 100644 index 0000000000..00e6b556d8 --- /dev/null +++ b/Modules/Prospect/Resources/views/menu-header.blade.php @@ -0,0 +1,27 @@ + diff --git a/Modules/Prospect/Resources/views/subviews/edit-prospect-details.blade.php b/Modules/Prospect/Resources/views/subviews/edit-prospect-details.blade.php index 93ed715021..45969a6fe8 100644 --- a/Modules/Prospect/Resources/views/subviews/edit-prospect-details.blade.php +++ b/Modules/Prospect/Resources/views/subviews/edit-prospect-details.blade.php @@ -7,22 +7,52 @@
-
- +
+ + +
+
+ + placeholder="Enter Organization Name" value="{{ $prospect->organization_name }}" required>
-
+ +
+ + +
+
+
+
+
+ + +
@@ -38,22 +68,10 @@
- - -
-
+ + @foreach (config('prospect.status') as $key => $status) + + @endforeach + +
- - -
-
-
-
-
+
-
+
+
+
-
-
-
+
diff --git a/Modules/Prospect/Resources/views/subviews/edit-prospect-insights.blade.php b/Modules/Prospect/Resources/views/subviews/edit-prospect-insights.blade.php new file mode 100644 index 0000000000..2f944339cb --- /dev/null +++ b/Modules/Prospect/Resources/views/subviews/edit-prospect-insights.blade.php @@ -0,0 +1,14 @@ +
+
+
+ @csrf + @method('PUT') +
+ + +
+ +
+
+
diff --git a/Modules/Prospect/Resources/views/subviews/prospect-comments.blade.php b/Modules/Prospect/Resources/views/subviews/prospect-comments.blade.php index fad786110e..cace719758 100644 --- a/Modules/Prospect/Resources/views/subviews/prospect-comments.blade.php +++ b/Modules/Prospect/Resources/views/subviews/prospect-comments.blade.php @@ -1,5 +1,5 @@
-
Prospect Comments
+
Prospect Comments ({{ count($prospect->comments) }})
@@ -15,7 +15,7 @@ width="50" data-toggle="tooltip" data-placement="top" title={{ $comment->user->name }}>
{{ \Carbon\Carbon::parse($comment->created_at)->format('M d, Y') }} + class="fz-16 font-weight-bold text-muted">{{ $prospect->getFormattedDate($comment->created_at) }}
{{ $comment->comment }}
diff --git a/Modules/Prospect/Resources/views/subviews/prospect-details.blade.php b/Modules/Prospect/Resources/views/subviews/prospect-details.blade.php index 65bc5e44d5..2b79ce2202 100644 --- a/Modules/Prospect/Resources/views/subviews/prospect-details.blade.php +++ b/Modules/Prospect/Resources/views/subviews/prospect-details.blade.php @@ -9,7 +9,7 @@
- {{ $prospect->organization_name ?? 'N/A' }} + {{ $prospect->getProspectDisplayName() }}
@@ -21,6 +21,12 @@
+
+
+ + {{ $prospect->project_name ?? 'N/A' }} +
+
@@ -28,15 +34,15 @@ class="ml-2">{{ $prospect->proposal_sent_date ? $prospect->getFormattedDate($prospect->proposal_sent_date) : 'N/A' }}
+
+ +
{{ $prospect->domain ?? 'N/A' }}
-
- -
@@ -44,18 +50,18 @@ class="ml-2">{{ $prospect->proposal_sent_date ? $prospect->getFormattedDate($pro class="ml-2">{{ config('prospect.customer-types')[$prospect->customer_type] ?? 'N/A' }}
+
+ +
{{ isset($prospect->currency) && isset($currencySymbols[$prospect->currency]) ? $currencySymbols[$prospect->currency] : '' }} - {{ $prospect->budget ? round($prospect->budget, 2) : 'N/A' }} + {{ $prospect->budget ? $prospect->formattedAmount($prospect->budget) : 'N/A' }}
-
- -
@@ -63,6 +69,9 @@ class="ml-2">{{ config('prospect.customer-types')[$prospect->customer_type] ?? ' class="ml-2">{{ $prospect->last_followup_date ? $prospect->getFormattedDate($prospect->last_followup_date) : 'N/A' }}
+
+ +
@@ -70,15 +79,15 @@ class="ml-2">{{ $prospect->last_followup_date ? $prospect->getFormattedDate($pro class="ml-2">{{ $prospect->introductory_call ? $prospect->getFormattedDate($prospect->introductory_call) : 'N/A' }}
-
- -
- {{ $prospect->proposal_status ?? 'N/A' }} + {{ config('prospect.status')[$prospect->proposal_status] ?? 'N/A' }}
+
+ +
@@ -90,9 +99,6 @@ class="fa fa-external-link"> @endif
-
- -
diff --git a/Modules/Prospect/Resources/views/subviews/prospect-insights.blade.php b/Modules/Prospect/Resources/views/subviews/prospect-insights.blade.php new file mode 100644 index 0000000000..0d7ada9795 --- /dev/null +++ b/Modules/Prospect/Resources/views/subviews/prospect-insights.blade.php @@ -0,0 +1,34 @@ + +
+
+
+
+
Prospect Insights
+
+ @foreach ($prospect->insights as $key => $insight) +
+
+ Useruser->name }}> +
+ {{ $prospect->getFormattedDate($insight->created_at) }} +
{{ $insight->insight_learning }}
+
+
+
+ @endforeach + @if (count($prospect->insights) == 0) +
+
+
No Insights
+
+
+ @endif +
+
+
diff --git a/Modules/Prospect/Resources/views/subviews/show.blade.php b/Modules/Prospect/Resources/views/subviews/show.blade.php index 9e83091b0a..005b5f63d7 100644 --- a/Modules/Prospect/Resources/views/subviews/show.blade.php +++ b/Modules/Prospect/Resources/views/subviews/show.blade.php @@ -4,10 +4,11 @@

-

Prospect Name: {{ $prospect->organization_name }}

+

Prospect Name: {{ $prospect->getProspectDisplayName() }}

Edit
@include('prospect::subviews.prospect-details') @include('prospect::subviews.prospect-comments') + @include('prospect::subviews.prospect-insights')
@endsection diff --git a/Modules/Prospect/Routes/web.php b/Modules/Prospect/Routes/web.php index 6d24018040..e0f935a91b 100644 --- a/Modules/Prospect/Routes/web.php +++ b/Modules/Prospect/Routes/web.php @@ -19,4 +19,5 @@ Route::get('/{prospect}/show', 'ProspectController@show')->name('prospect.show'); Route::put('/{prospect}/update', 'ProspectController@update')->name('prospect.update'); Route::put('/{prospect}/comment/update', 'ProspectController@commentUpdate')->name('prospect.comment.update'); + Route::put('/{prospect}/insights/update', 'ProspectController@insightsUpdate')->name('prospect.insights.update'); }); diff --git a/Modules/Prospect/Services/ProspectService.php b/Modules/Prospect/Services/ProspectService.php index 19b86e93b4..cb0addd13f 100644 --- a/Modules/Prospect/Services/ProspectService.php +++ b/Modules/Prospect/Services/ProspectService.php @@ -2,23 +2,30 @@ namespace Modules\Prospect\Services; +use Modules\Client\Entities\Country; use Modules\Prospect\Entities\Prospect; use Modules\Prospect\Entities\ProspectComment; +use Modules\Prospect\Entities\ProspectInsight; class ProspectService { + public function index(array $requestData = []) + { + return [ + 'prospects' => $this->getFilteredProspects($requestData), + 'currencySymbols' => $this->getCurrencySymbols(), + ]; + } + public function store($validated) { $prospect = new Prospect(); $this->saveProspectData($prospect, $validated); - - return redirect()->route('prospect.index')->with('status', 'Prospect created successfully!'); } - public function update($request, $id) + public function update($request, $prospect) { $budget = $request->budget ?? null; - $prospect = Prospect::find($id); $prospect->organization_name = $request->org_name; $prospect->poc_user_id = $request->poc_user_id; $prospect->proposal_sent_date = $request->proposal_sent_date; @@ -31,6 +38,8 @@ public function update($request, $id) $prospect->rfp_link = $request->rfp_link; $prospect->proposal_link = $request->proposal_link; $prospect->currency = $budget ? $request->currency : null; + $prospect->client_id = $request->client_id ?? null; + $prospect->project_name = $request->project_name; $prospect->save(); return redirect()->route('prospect.show', $prospect->id)->with('status', 'Prospect updated successfully!'); @@ -47,6 +56,38 @@ public function commentUpdate($validated, $id) return $prospectComment; } + public function insightsUpdate($validated, $id) + { + $prospectInsights = new ProspectInsight(); + $prospectInsights->prospect_id = $id; + $prospectInsights->user_id = auth()->user()->id; + $prospectInsights->insight_learning = $validated['insight_learning']; + $prospectInsights->save(); + } + + private function getFilteredProspects(array $requestData = []) + { + $filter = $requestData['status'] ?? 'open'; + + return Prospect::query()->when( + $filter === 'open', + fn ($query) => $query->where(function ($query) { + $query->whereNotIn('proposal_status', ['rejected', 'converted']) + ->orWhereNull('proposal_status') + ->orWhere('proposal_status', ''); + }), + fn ($query) => $query->where('proposal_status', $filter) + ) + ->orderBy('created_at', 'desc') + ->paginate(config('constants.pagination_size')) + ->appends($requestData); + } + + private function getCurrencySymbols() + { + return Country::pluck('currency_symbol', 'currency'); + } + private function saveProspectData($prospect, $validated) { $budget = $validated['budget'] ?? null; @@ -61,7 +102,9 @@ private function saveProspectData($prospect, $validated) $prospect->last_followup_date = $validated['last_followup_date'] ?? null; $prospect->rfp_link = $validated['rfp_link'] ?? null; $prospect->proposal_link = $validated['proposal_link'] ?? null; + $prospect->client_id = $validated['client_id'] ?? null; $prospect->currency = $budget ? $validated['currency'] : null; + $prospect->project_name = $validated['project_name'] ?? null; $prospect->save(); } } diff --git a/Modules/User/Entities/User.php b/Modules/User/Entities/User.php index 89243765be..99652539ff 100644 --- a/Modules/User/Entities/User.php +++ b/Modules/User/Entities/User.php @@ -14,6 +14,7 @@ use Modules\Project\Entities\ProjectTeamMember; use Modules\Prospect\Entities\Prospect; use Modules\Prospect\Entities\ProspectComment; +use Modules\Prospect\Entities\ProspectInsight; use Modules\User\Database\Factories\UserFactory; use Modules\User\Traits\CanBeExtended; use Modules\User\Traits\HasWebsiteUser; @@ -257,6 +258,11 @@ public function prospectsComments() return $this->hasMany(ProspectComment::class); } + public function prospectInsights() + { + return $this->hasMany(ProspectInsight::class); + } + protected static function newFactory() { return new UserFactory();