Files
hub/app/Http/Controllers/Seller/Sales/ProspectController.php
kelly c7250e26e2
Some checks failed
ci/woodpecker/push/ci Pipeline failed
fix: use seller_business_id in ProspectController (matches migration)
2025-12-15 21:07:19 -07:00

315 lines
9.9 KiB
PHP

<?php
namespace App\Http\Controllers\Seller\Sales;
use App\Http\Controllers\Controller;
use App\Models\Business;
use App\Models\Crm\CrmLead;
use App\Models\ProspectImport;
use App\Models\ProspectInsight;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class ProspectController extends Controller
{
/**
* List prospects (leads) assigned to this sales rep
*/
public function index(Request $request, Business $business)
{
$user = $request->user();
// Get leads assigned to this user
$leads = CrmLead::where('seller_business_id', $business->id)
->where('assigned_to', $user->id)
->with('insights')
->orderByDesc('created_at')
->paginate(20);
// Get insight counts by type
$insightCounts = ProspectInsight::forBusiness($business->id)
->whereNotNull('lead_id')
->selectRaw('insight_type, COUNT(*) as count')
->groupBy('insight_type')
->pluck('count', 'insight_type');
return view('seller.sales.prospects.index', compact(
'business',
'leads',
'insightCounts'
));
}
/**
* Show prospect detail with insights
*/
public function show(Request $request, Business $business, CrmLead $lead)
{
if ($lead->seller_business_id !== $business->id) {
abort(403);
}
$lead->load('insights.creator');
// Get similar successful accounts for reference
$successStories = $this->findSimilarSuccessStories($business, $lead);
return view('seller.sales.prospects.show', compact(
'business',
'lead',
'successStories'
));
}
/**
* Add insight to a prospect
*/
public function storeInsight(Request $request, Business $business, CrmLead $lead)
{
if ($lead->seller_business_id !== $business->id) {
abort(403);
}
$validated = $request->validate([
'insight_type' => 'required|in:gap,pain_point,opportunity,objection,competitor_weakness',
'category' => 'nullable|in:price_point,quality,consistency,service,margin,reliability,selection',
'description' => 'required|string|max:2000',
]);
ProspectInsight::create([
'business_id' => $business->id,
'lead_id' => $lead->id,
'insight_type' => $validated['insight_type'],
'category' => $validated['category'],
'description' => $validated['description'],
'created_by' => $request->user()->id,
]);
return back()->with('success', 'Insight added.');
}
/**
* Delete an insight
*/
public function destroyInsight(Request $request, Business $business, ProspectInsight $insight)
{
if ($insight->business_id !== $business->id) {
abort(403);
}
$insight->delete();
return back()->with('success', 'Insight deleted.');
}
/**
* Show import history and upload form
*/
public function imports(Request $request, Business $business)
{
$imports = ProspectImport::forBusiness($business->id)
->with('importer')
->orderByDesc('created_at')
->paginate(10);
return view('seller.sales.prospects.imports', compact('business', 'imports'));
}
/**
* Upload and process import file
*/
public function upload(Request $request, Business $business)
{
$validated = $request->validate([
'file' => 'required|file|mimes:csv,txt|max:5120', // 5MB max
]);
$file = $request->file('file');
$filename = $file->getClientOriginalName();
$path = $file->store("imports/{$business->id}", 'local');
// Count rows
$content = file_get_contents($file->getRealPath());
$lines = explode("\n", trim($content));
$totalRows = count($lines) - 1; // Exclude header
// Create import record
$import = ProspectImport::create([
'business_id' => $business->id,
'user_id' => $request->user()->id,
'filename' => $filename,
'status' => ProspectImport::STATUS_PENDING,
'total_rows' => max(0, $totalRows),
'processed_rows' => 0,
'created_count' => 0,
'updated_count' => 0,
'skipped_count' => 0,
'error_count' => 0,
]);
// Get headers for mapping
$headers = str_getcsv($lines[0]);
return view('seller.sales.prospects.map-columns', compact(
'business',
'import',
'headers',
'path'
));
}
/**
* Process import with column mapping
*/
public function processImport(Request $request, Business $business, ProspectImport $import)
{
if ($import->business_id !== $business->id) {
abort(403);
}
$validated = $request->validate([
'mapping' => 'required|array',
'mapping.company_name' => 'required|string',
'path' => 'required|string',
]);
$import->update([
'column_mapping' => $validated['mapping'],
'status' => ProspectImport::STATUS_PROCESSING,
]);
// Process synchronously for now (could dispatch to queue for large files)
$this->processImportFile($import, $validated['path'], $validated['mapping']);
return redirect()
->route('seller.business.sales.prospects.imports', $business)
->with('success', "Import completed. {$import->created_count} created, {$import->updated_count} updated, {$import->error_count} errors.");
}
/**
* Process the import file
*/
protected function processImportFile(ProspectImport $import, string $path, array $mapping): void
{
$content = Storage::disk('local')->get($path);
$lines = explode("\n", trim($content));
$headers = str_getcsv(array_shift($lines));
// Create column index map
$columnMap = [];
foreach ($mapping as $field => $column) {
$index = array_search($column, $headers);
if ($index !== false) {
$columnMap[$field] = $index;
}
}
foreach ($lines as $lineNum => $line) {
if (empty(trim($line))) {
continue;
}
$row = str_getcsv($line);
$import->incrementProcessed();
try {
$companyName = $row[$columnMap['company_name']] ?? null;
if (! $companyName) {
$import->addError($lineNum + 2, 'Missing company name');
continue;
}
// Check for duplicate
$existing = CrmLead::where('seller_business_id', $import->business_id)
->where('company_name', $companyName)
->first();
if ($existing) {
// Update existing
$this->updateLeadFromRow($existing, $row, $columnMap);
$import->incrementUpdated();
} else {
// Create new
$this->createLeadFromRow($import, $row, $columnMap);
$import->incrementCreated();
}
} catch (\Exception $e) {
$import->addError($lineNum + 2, $e->getMessage());
}
}
$import->markCompleted();
// Clean up file
Storage::disk('local')->delete($path);
}
/**
* Create a new lead from import row
*/
protected function createLeadFromRow(ProspectImport $import, array $row, array $columnMap): CrmLead
{
return CrmLead::create([
'seller_business_id' => $import->business_id,
'company_name' => $row[$columnMap['company_name']] ?? null,
'contact_name' => $row[$columnMap['contact_name'] ?? -1] ?? null,
'email' => $row[$columnMap['email'] ?? -1] ?? null,
'phone' => $row[$columnMap['phone'] ?? -1] ?? null,
'address' => $row[$columnMap['address'] ?? -1] ?? null,
'city' => $row[$columnMap['city'] ?? -1] ?? null,
'state' => $row[$columnMap['state'] ?? -1] ?? null,
'zipcode' => $row[$columnMap['zipcode'] ?? -1] ?? null,
'license_number' => $row[$columnMap['license_number'] ?? -1] ?? null,
'notes' => $row[$columnMap['notes'] ?? -1] ?? null,
'source' => 'import',
'status' => 'new',
'assigned_to' => $import->user_id,
]);
}
/**
* Update existing lead from import row
*/
protected function updateLeadFromRow(CrmLead $lead, array $row, array $columnMap): void
{
$updates = [];
foreach (['contact_name', 'email', 'phone', 'address', 'city', 'state', 'zipcode', 'license_number'] as $field) {
if (isset($columnMap[$field]) && ! empty($row[$columnMap[$field]])) {
$updates[$field] = $row[$columnMap[$field]];
}
}
if (! empty($updates)) {
$lead->update($updates);
}
}
/**
* Find similar successful accounts for a prospect
*/
protected function findSimilarSuccessStories(Business $business, CrmLead $lead): \Illuminate\Support\Collection
{
// Get successful accounts in same city/state
$query = Business::query()
->whereHas('orders', function ($q) {
$q->where('status', 'completed');
})
->with('locations');
if ($lead->city) {
$query->whereHas('locations', function ($q) use ($lead) {
$q->where('city', 'ILIKE', "%{$lead->city}%");
});
} elseif ($lead->state) {
$query->whereHas('locations', function ($q) use ($lead) {
$q->where('state', $lead->state);
});
}
return $query->limit(5)->get();
}
}