315 lines
9.9 KiB
PHP
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();
|
|
}
|
|
}
|