All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
Competitor Intelligence: - CompetitorController with store/destroy for replacement mappings - Map competitor products to our alternatives with advantage notes - Competitor index view with grouped replacements by brand Prospect Management: - ProspectController with full CSV import functionality - Upload CSV, map columns, and process imports - Prospect insights (gaps, pain points, opportunities, objections) - Success story matching for similar accounts Views: - competitors/index - replacement mappings with modal form - prospects/index - assigned leads with insight summary badges - prospects/imports - upload form and import history - prospects/map-columns - CSV column mapping interface - prospects/show - lead detail with insights and success stories Dashboard: - Added Prospects and Competitors buttons to sales dashboard
154 lines
5.1 KiB
PHP
154 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Seller\Sales;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Business;
|
|
use App\Models\CompetitorReplacement;
|
|
use App\Models\Product;
|
|
use Illuminate\Http\Request;
|
|
|
|
class CompetitorController extends Controller
|
|
{
|
|
/**
|
|
* List competitor replacements - when you see competitor X, pitch product Y
|
|
*/
|
|
public function index(Request $request, Business $business)
|
|
{
|
|
$replacements = CompetitorReplacement::forBusiness($business->id)
|
|
->with(['product', 'product.brand', 'creator'])
|
|
->orderBy('competitor_name')
|
|
->get()
|
|
->groupBy('competitor_name');
|
|
|
|
// Get products for the add form
|
|
$products = Product::whereHas('brand', function ($q) use ($business) {
|
|
$q->where('business_id', $business->id);
|
|
})
|
|
->with('brand')
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
// Get unique competitor names for filtering
|
|
$competitors = CompetitorReplacement::forBusiness($business->id)
|
|
->distinct()
|
|
->pluck('competitor_name')
|
|
->sort();
|
|
|
|
return view('seller.sales.competitors.index', compact(
|
|
'business',
|
|
'replacements',
|
|
'products',
|
|
'competitors'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Store a new competitor replacement mapping
|
|
*/
|
|
public function store(Request $request, Business $business)
|
|
{
|
|
$validated = $request->validate([
|
|
'competitor_name' => 'required|string|max:255',
|
|
'competitor_product_name' => 'nullable|string|max:255',
|
|
'cannaiq_product_id' => 'nullable|string|max:255',
|
|
'product_id' => 'required|exists:products,id',
|
|
'advantage_notes' => 'nullable|string|max:2000',
|
|
]);
|
|
|
|
// Verify product belongs to this business
|
|
$product = Product::whereHas('brand', function ($q) use ($business) {
|
|
$q->where('business_id', $business->id);
|
|
})->findOrFail($validated['product_id']);
|
|
|
|
CompetitorReplacement::create([
|
|
'business_id' => $business->id,
|
|
'cannaiq_product_id' => $validated['cannaiq_product_id'] ?? uniqid('manual_'),
|
|
'competitor_name' => $validated['competitor_name'],
|
|
'competitor_product_name' => $validated['competitor_product_name'],
|
|
'product_id' => $product->id,
|
|
'advantage_notes' => $validated['advantage_notes'],
|
|
'created_by' => $request->user()->id,
|
|
]);
|
|
|
|
return back()->with('success', 'Competitor replacement added.');
|
|
}
|
|
|
|
/**
|
|
* Update a competitor replacement
|
|
*/
|
|
public function update(Request $request, Business $business, CompetitorReplacement $replacement)
|
|
{
|
|
if ($replacement->business_id !== $business->id) {
|
|
abort(403);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'competitor_product_name' => 'nullable|string|max:255',
|
|
'product_id' => 'required|exists:products,id',
|
|
'advantage_notes' => 'nullable|string|max:2000',
|
|
]);
|
|
|
|
// Verify product belongs to this business
|
|
$product = Product::whereHas('brand', function ($q) use ($business) {
|
|
$q->where('business_id', $business->id);
|
|
})->findOrFail($validated['product_id']);
|
|
|
|
$replacement->update([
|
|
'competitor_product_name' => $validated['competitor_product_name'],
|
|
'product_id' => $product->id,
|
|
'advantage_notes' => $validated['advantage_notes'],
|
|
]);
|
|
|
|
return back()->with('success', 'Replacement updated.');
|
|
}
|
|
|
|
/**
|
|
* Delete a competitor replacement
|
|
*/
|
|
public function destroy(Request $request, Business $business, CompetitorReplacement $replacement)
|
|
{
|
|
if ($replacement->business_id !== $business->id) {
|
|
abort(403);
|
|
}
|
|
|
|
$replacement->delete();
|
|
|
|
return back()->with('success', 'Replacement deleted.');
|
|
}
|
|
|
|
/**
|
|
* Quick lookup - get our replacement for a competitor product
|
|
*/
|
|
public function lookup(Request $request, Business $business)
|
|
{
|
|
$competitorName = $request->get('competitor');
|
|
$productName = $request->get('product');
|
|
|
|
$query = CompetitorReplacement::forBusiness($business->id)
|
|
->with(['product', 'product.brand']);
|
|
|
|
if ($competitorName) {
|
|
$query->where('competitor_name', 'ILIKE', "%{$competitorName}%");
|
|
}
|
|
|
|
if ($productName) {
|
|
$query->where('competitor_product_name', 'ILIKE', "%{$productName}%");
|
|
}
|
|
|
|
$replacements = $query->limit(10)->get();
|
|
|
|
return response()->json([
|
|
'replacements' => $replacements->map(fn ($r) => [
|
|
'id' => $r->id,
|
|
'competitor' => $r->competitor_name,
|
|
'competitor_product' => $r->competitor_product_name,
|
|
'our_product' => $r->product->name,
|
|
'our_sku' => $r->product->sku,
|
|
'advantage' => $r->advantage_notes,
|
|
'pitch' => $r->getPitchSummary(),
|
|
]),
|
|
]);
|
|
}
|
|
}
|