Files
hub/app/Http/Controllers/Seller/Sales/CompetitorController.php
kelly 51047fc315
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
feat: add Competitor Replacements and Prospect Management (Sprint 5)
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
2025-12-15 17:28:34 -07:00

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(),
]),
]);
}
}