ExportController with CSV exports for: - accounts: All assigned accounts with order history summary - account-history: Detailed order history for meeting prep - prospects: Lead data with insights for pitch preparation - competitors: Competitor replacement mappings for sales training - pitch: Pitch builder with contact info, insights, success stories Added export buttons to: - Accounts index (Export CSV button) - Account show (Export History button) - Prospects index (Export CSV button) - Prospect show (Export Pitch button) - Competitors index (Export CSV button) All exports stream as CSV for instant download without memory issues.
335 lines
12 KiB
PHP
335 lines
12 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\Order;
|
|
use App\Models\SalesRepAssignment;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Str;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
class ExportController extends Controller
|
|
{
|
|
/**
|
|
* Export accounts assigned to the current sales rep as CSV.
|
|
*/
|
|
public function accounts(Business $business): StreamedResponse
|
|
{
|
|
$user = Auth::user();
|
|
|
|
// Get assigned account IDs
|
|
$assignedAccountIds = SalesRepAssignment::where('business_id', $business->id)
|
|
->where('user_id', $user->id)
|
|
->where('assignable_type', Business::class)
|
|
->pluck('assignable_id');
|
|
|
|
$accounts = Business::whereIn('id', $assignedAccountIds)
|
|
->with(['locations', 'contacts'])
|
|
->get();
|
|
|
|
$filename = 'my-accounts-'.now()->format('Y-m-d').'.csv';
|
|
|
|
return $this->streamCsv($filename, function () use ($accounts, $business) {
|
|
$handle = fopen('php://output', 'w');
|
|
|
|
// Header row
|
|
fputcsv($handle, [
|
|
'Account Name',
|
|
'Status',
|
|
'Primary Location',
|
|
'City',
|
|
'State',
|
|
'ZIP',
|
|
'Primary Contact',
|
|
'Email',
|
|
'Phone',
|
|
'Last Order Date',
|
|
'Last Order Total',
|
|
'Days Since Order',
|
|
]);
|
|
|
|
foreach ($accounts as $account) {
|
|
$location = $account->locations->first();
|
|
$contact = $account->contacts->first();
|
|
|
|
// Get last order
|
|
$lastOrder = Order::where('business_id', $account->id)
|
|
->whereHas('items.product.brand', fn ($q) => $q->where('business_id', $business->id))
|
|
->orderBy('created_at', 'desc')
|
|
->first();
|
|
|
|
fputcsv($handle, [
|
|
$account->name,
|
|
$account->status ?? 'active',
|
|
$location?->name ?? '',
|
|
$location?->city ?? '',
|
|
$location?->state ?? '',
|
|
$location?->zipcode ?? '',
|
|
$contact?->name ?? '',
|
|
$contact?->email ?? '',
|
|
$contact?->phone ?? '',
|
|
$lastOrder?->created_at?->format('Y-m-d') ?? '',
|
|
$lastOrder ? '$'.number_format($lastOrder->total / 100, 2) : '',
|
|
$lastOrder ? now()->diffInDays($lastOrder->created_at) : '',
|
|
]);
|
|
}
|
|
|
|
fclose($handle);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Export a single account's order history for meeting prep.
|
|
*/
|
|
public function accountHistory(Business $business, Business $account): StreamedResponse
|
|
{
|
|
// Verify assignment
|
|
$isAssigned = SalesRepAssignment::where('business_id', $business->id)
|
|
->where('user_id', Auth::id())
|
|
->where('assignable_type', Business::class)
|
|
->where('assignable_id', $account->id)
|
|
->exists();
|
|
|
|
if (! $isAssigned) {
|
|
abort(403, 'Account not assigned to you');
|
|
}
|
|
|
|
$orders = Order::where('business_id', $account->id)
|
|
->whereHas('items.product.brand', fn ($q) => $q->where('business_id', $business->id))
|
|
->with(['items.product'])
|
|
->orderBy('created_at', 'desc')
|
|
->limit(50)
|
|
->get();
|
|
|
|
$filename = Str::slug($account->name).'-history-'.now()->format('Y-m-d').'.csv';
|
|
|
|
return $this->streamCsv($filename, function () use ($orders, $account) {
|
|
$handle = fopen('php://output', 'w');
|
|
|
|
// Account summary header
|
|
fputcsv($handle, ['Account Summary: '.$account->name]);
|
|
fputcsv($handle, ['Generated: '.now()->format('F j, Y')]);
|
|
fputcsv($handle, ['Total Orders: '.$orders->count()]);
|
|
fputcsv($handle, ['']);
|
|
|
|
// Order details header
|
|
fputcsv($handle, [
|
|
'Order Number',
|
|
'Date',
|
|
'Status',
|
|
'Product',
|
|
'SKU',
|
|
'Quantity',
|
|
'Unit Price',
|
|
'Line Total',
|
|
'Order Total',
|
|
]);
|
|
|
|
foreach ($orders as $order) {
|
|
$firstItem = true;
|
|
foreach ($order->items as $item) {
|
|
fputcsv($handle, [
|
|
$firstItem ? $order->order_number : '',
|
|
$firstItem ? $order->created_at->format('Y-m-d') : '',
|
|
$firstItem ? ucfirst($order->status) : '',
|
|
$item->product?->name ?? 'Unknown',
|
|
$item->product?->sku ?? '',
|
|
$item->quantity,
|
|
'$'.number_format($item->price / 100, 2),
|
|
'$'.number_format(($item->price * $item->quantity) / 100, 2),
|
|
$firstItem ? '$'.number_format($order->total / 100, 2) : '',
|
|
]);
|
|
$firstItem = false;
|
|
}
|
|
}
|
|
|
|
fclose($handle);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Export prospect data with insights for pitch preparation.
|
|
*/
|
|
public function prospects(Business $business): StreamedResponse
|
|
{
|
|
$user = Auth::user();
|
|
|
|
$leads = CrmLead::where('business_id', $business->id)
|
|
->where('assigned_to', $user->id)
|
|
->with(['insights'])
|
|
->get();
|
|
|
|
$filename = 'prospects-'.now()->format('Y-m-d').'.csv';
|
|
|
|
return $this->streamCsv($filename, function () use ($leads) {
|
|
$handle = fopen('php://output', 'w');
|
|
|
|
// Header row
|
|
fputcsv($handle, [
|
|
'Company Name',
|
|
'Contact Name',
|
|
'Email',
|
|
'Phone',
|
|
'City',
|
|
'State',
|
|
'Status',
|
|
'Source',
|
|
'License Number',
|
|
'Gaps',
|
|
'Pain Points',
|
|
'Opportunities',
|
|
'Notes',
|
|
]);
|
|
|
|
foreach ($leads as $lead) {
|
|
$gaps = $lead->insights->where('insight_type', 'gap')->pluck('description')->implode('; ');
|
|
$painPoints = $lead->insights->where('insight_type', 'pain_point')->pluck('description')->implode('; ');
|
|
$opportunities = $lead->insights->where('insight_type', 'opportunity')->pluck('description')->implode('; ');
|
|
|
|
fputcsv($handle, [
|
|
$lead->company_name,
|
|
$lead->contact_name ?? '',
|
|
$lead->email ?? '',
|
|
$lead->phone ?? '',
|
|
$lead->city ?? '',
|
|
$lead->state ?? '',
|
|
ucfirst($lead->status),
|
|
$lead->source ?? '',
|
|
$lead->license_number ?? '',
|
|
$gaps,
|
|
$painPoints,
|
|
$opportunities,
|
|
$lead->notes ?? '',
|
|
]);
|
|
}
|
|
|
|
fclose($handle);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Export competitor replacement data for sales training.
|
|
*/
|
|
public function competitors(Business $business): StreamedResponse
|
|
{
|
|
$replacements = \App\Models\CompetitorReplacement::where('business_id', $business->id)
|
|
->with(['product.brand'])
|
|
->orderBy('competitor_name')
|
|
->get();
|
|
|
|
$filename = 'competitor-replacements-'.now()->format('Y-m-d').'.csv';
|
|
|
|
return $this->streamCsv($filename, function () use ($replacements) {
|
|
$handle = fopen('php://output', 'w');
|
|
|
|
fputcsv($handle, [
|
|
'Competitor Brand',
|
|
'Competitor Product',
|
|
'Our Product',
|
|
'Our SKU',
|
|
'Our Brand',
|
|
'Why Ours is Better',
|
|
]);
|
|
|
|
foreach ($replacements as $replacement) {
|
|
fputcsv($handle, [
|
|
$replacement->competitor_name,
|
|
$replacement->competitor_product_name ?? 'Any product',
|
|
$replacement->product->name,
|
|
$replacement->product->sku,
|
|
$replacement->product->brand?->name ?? '',
|
|
$replacement->advantage_notes ?? '',
|
|
]);
|
|
}
|
|
|
|
fclose($handle);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generate a pitch builder export for a specific prospect.
|
|
*/
|
|
public function pitchBuilder(Business $business, CrmLead $lead): StreamedResponse
|
|
{
|
|
// Verify assignment
|
|
if ($lead->assigned_to !== Auth::id()) {
|
|
abort(403, 'Lead not assigned to you');
|
|
}
|
|
|
|
// Get similar successful accounts for reference
|
|
$successStories = Business::where('type', 'buyer')
|
|
->whereHas('orders', function ($query) use ($business) {
|
|
$query->whereHas('items.product.brand', fn ($q) => $q->where('business_id', $business->id))
|
|
->where('created_at', '>=', now()->subMonths(3));
|
|
})
|
|
->when($lead->city, fn ($q) => $q->whereHas('locations', fn ($l) => $l->where('city', $lead->city)))
|
|
->with(['locations'])
|
|
->limit(5)
|
|
->get();
|
|
|
|
$filename = 'pitch-'.Str::slug($lead->company_name).'-'.now()->format('Y-m-d').'.csv';
|
|
|
|
return $this->streamCsv($filename, function () use ($lead, $successStories) {
|
|
$handle = fopen('php://output', 'w');
|
|
|
|
// Prospect info
|
|
fputcsv($handle, ['PITCH PREPARATION: '.$lead->company_name]);
|
|
fputcsv($handle, ['Generated: '.now()->format('F j, Y')]);
|
|
fputcsv($handle, ['']);
|
|
|
|
// Contact info
|
|
fputcsv($handle, ['CONTACT INFORMATION']);
|
|
fputcsv($handle, ['Contact Name', $lead->contact_name ?? 'N/A']);
|
|
fputcsv($handle, ['Email', $lead->email ?? 'N/A']);
|
|
fputcsv($handle, ['Phone', $lead->phone ?? 'N/A']);
|
|
fputcsv($handle, ['Location', ($lead->city ?? '').($lead->city && $lead->state ? ', ' : '').($lead->state ?? '')]);
|
|
fputcsv($handle, ['License', $lead->license_number ?? 'N/A']);
|
|
fputcsv($handle, ['']);
|
|
|
|
// Insights
|
|
fputcsv($handle, ['IDENTIFIED GAPS & OPPORTUNITIES']);
|
|
foreach ($lead->insights as $insight) {
|
|
fputcsv($handle, [
|
|
ucfirst(str_replace('_', ' ', $insight->insight_type)),
|
|
$insight->description,
|
|
]);
|
|
}
|
|
fputcsv($handle, ['']);
|
|
|
|
// Success stories
|
|
fputcsv($handle, ['SIMILAR SUCCESSFUL ACCOUNTS (Reference for pitch)']);
|
|
fputcsv($handle, ['Account Name', 'Location']);
|
|
foreach ($successStories as $account) {
|
|
$location = $account->locations->first();
|
|
fputcsv($handle, [
|
|
$account->name,
|
|
($location?->city ?? '').($location?->city && $location?->state ? ', ' : '').($location?->state ?? ''),
|
|
]);
|
|
}
|
|
|
|
fputcsv($handle, ['']);
|
|
fputcsv($handle, ['NOTES']);
|
|
fputcsv($handle, [$lead->notes ?? 'No additional notes']);
|
|
|
|
fclose($handle);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper to stream a CSV response.
|
|
*/
|
|
private function streamCsv(string $filename, callable $callback): StreamedResponse
|
|
{
|
|
return response()->stream($callback, 200, [
|
|
'Content-Type' => 'text/csv',
|
|
'Content-Disposition' => 'attachment; filename="'.$filename.'"',
|
|
'Pragma' => 'no-cache',
|
|
'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0',
|
|
'Expires' => '0',
|
|
]);
|
|
}
|
|
}
|