The Buyer CRM controllers were using incorrect namespace imports from Modules\Crm\Entities\* which doesn't exist. Updated all controllers to use the correct App\Models\Crm\* namespace: - DashboardController: CrmInvoice, CrmQuote, CrmThread - InboxController: CrmThread - InvoiceController: CrmInvoice, CrmThread - QuoteController: CrmQuote - BrandHubController: CrmThread - MessageController: CrmChannelMessage, CrmThread - OrderController: CrmThread (inline) Also changed CrmMessage to CrmChannelMessage as CrmMessage doesn't exist.
299 lines
9.5 KiB
PHP
299 lines
9.5 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Buyer\Crm;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Buyer\BuyerInvoiceRecord;
|
|
use App\Models\Buyer\BuyerSavedFilter;
|
|
use App\Models\Crm\CrmInvoice;
|
|
use App\Models\Crm\CrmThread;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
|
class InvoiceController extends Controller
|
|
{
|
|
public function index(Request $request)
|
|
{
|
|
$business = Auth::user()->business;
|
|
$user = Auth::user();
|
|
|
|
$query = CrmInvoice::forBuyerBusiness($business->id)
|
|
->with(['brand', 'order', 'buyerRecord']);
|
|
|
|
// Tab filter
|
|
$tab = $request->get('tab', 'outstanding');
|
|
$query = match ($tab) {
|
|
'outstanding' => $query->whereIn('status', [CrmInvoice::STATUS_DRAFT, CrmInvoice::STATUS_SENT, CrmInvoice::STATUS_VIEWED]),
|
|
'overdue' => $query->overdue(),
|
|
'paid' => $query->paid(),
|
|
'disputed' => $query->whereHas('buyerRecord', fn ($q) => $q->disputed()),
|
|
default => $query,
|
|
};
|
|
|
|
// Brand filter
|
|
if ($brandId = $request->get('brand_id')) {
|
|
$query->where('brand_id', $brandId);
|
|
}
|
|
|
|
// Date range
|
|
if ($from = $request->get('from_date')) {
|
|
$query->whereDate('issue_date', '>=', $from);
|
|
}
|
|
if ($to = $request->get('to_date')) {
|
|
$query->whereDate('issue_date', '<=', $to);
|
|
}
|
|
|
|
// Search
|
|
if ($search = $request->get('search')) {
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('invoice_number', 'ilike', "%{$search}%")
|
|
->orWhereHas('brand', fn ($bq) => $bq->where('name', 'ilike', "%{$search}%"));
|
|
});
|
|
}
|
|
|
|
$invoices = $query->orderByDesc('issue_date')
|
|
->paginate(20)
|
|
->withQueryString();
|
|
|
|
// Get saved filters
|
|
$savedFilters = BuyerSavedFilter::getFiltersForType($business->id, $user->id, BuyerSavedFilter::TYPE_INVOICES);
|
|
|
|
// Get brands for filter
|
|
$brands = \App\Models\Brand::whereHas('invoices', function ($q) use ($business) {
|
|
$q->where('buyer_business_id', $business->id);
|
|
})->orderBy('name')->get();
|
|
|
|
// Tab counts
|
|
$counts = [
|
|
'outstanding' => CrmInvoice::forBuyerBusiness($business->id)
|
|
->whereIn('status', [CrmInvoice::STATUS_DRAFT, CrmInvoice::STATUS_SENT, CrmInvoice::STATUS_VIEWED])
|
|
->count(),
|
|
'overdue' => CrmInvoice::forBuyerBusiness($business->id)->overdue()->count(),
|
|
'paid' => CrmInvoice::forBuyerBusiness($business->id)->paid()->count(),
|
|
'disputed' => BuyerInvoiceRecord::forBusiness($business->id)->disputed()->count(),
|
|
];
|
|
|
|
// Summary stats
|
|
$stats = [
|
|
'total_outstanding' => CrmInvoice::forBuyerBusiness($business->id)
|
|
->whereIn('status', [CrmInvoice::STATUS_SENT, CrmInvoice::STATUS_VIEWED])
|
|
->sum('total'),
|
|
'total_overdue' => CrmInvoice::forBuyerBusiness($business->id)
|
|
->overdue()
|
|
->sum('total'),
|
|
];
|
|
|
|
return view('buyer.crm.invoices.index', compact(
|
|
'invoices',
|
|
'savedFilters',
|
|
'brands',
|
|
'tab',
|
|
'counts',
|
|
'stats'
|
|
));
|
|
}
|
|
|
|
public function show(CrmInvoice $invoice)
|
|
{
|
|
$business = Auth::user()->business;
|
|
|
|
if ($invoice->buyer_business_id !== $business->id) {
|
|
abort(403);
|
|
}
|
|
|
|
$invoice->load(['brand', 'order', 'items.product', 'payments']);
|
|
|
|
// Get or create buyer record
|
|
$buyerRecord = BuyerInvoiceRecord::getOrCreateForInvoice(
|
|
$business->id,
|
|
$invoice,
|
|
$invoice->order_id
|
|
);
|
|
|
|
// Mark as viewed if not already
|
|
if ($invoice->status === CrmInvoice::STATUS_SENT) {
|
|
$invoice->markAsViewed();
|
|
}
|
|
|
|
// Get related thread if exists
|
|
$thread = CrmThread::where('buyer_business_id', $business->id)
|
|
->where(function ($q) use ($invoice) {
|
|
$q->where('order_id', $invoice->order_id)
|
|
->orWhere('subject', 'ilike', "%{$invoice->invoice_number}%");
|
|
})
|
|
->first();
|
|
|
|
return view('buyer.crm.invoices.show', compact('invoice', 'buyerRecord', 'thread'));
|
|
}
|
|
|
|
public function pay(Request $request, CrmInvoice $invoice)
|
|
{
|
|
$business = Auth::user()->business;
|
|
$user = Auth::user();
|
|
|
|
if ($invoice->buyer_business_id !== $business->id) {
|
|
abort(403);
|
|
}
|
|
|
|
$buyerRecord = BuyerInvoiceRecord::getOrCreateForInvoice(
|
|
$business->id,
|
|
$invoice,
|
|
$invoice->order_id
|
|
);
|
|
|
|
if (! $buyerRecord->can_pay) {
|
|
return back()->with('error', 'This invoice cannot be paid at this time.');
|
|
}
|
|
|
|
// Start payment process (integrate with payment gateway)
|
|
$buyerRecord->initiatePayment();
|
|
|
|
// TODO: Redirect to payment gateway or process payment
|
|
// For now, redirect to a payment form
|
|
return redirect()->route('buyer.crm.invoices.payment', $invoice);
|
|
}
|
|
|
|
public function payment(CrmInvoice $invoice)
|
|
{
|
|
$business = Auth::user()->business;
|
|
|
|
if ($invoice->buyer_business_id !== $business->id) {
|
|
abort(403);
|
|
}
|
|
|
|
$invoice->load('brand');
|
|
|
|
$buyerRecord = BuyerInvoiceRecord::where('invoice_id', $invoice->id)
|
|
->where('business_id', $business->id)
|
|
->first();
|
|
|
|
return view('buyer.crm.invoices.payment', compact('invoice', 'buyerRecord'));
|
|
}
|
|
|
|
public function confirmPayment(Request $request, CrmInvoice $invoice)
|
|
{
|
|
$business = Auth::user()->business;
|
|
$user = Auth::user();
|
|
|
|
if ($invoice->buyer_business_id !== $business->id) {
|
|
abort(403);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'payment_reference' => 'required|string|max:255',
|
|
'payment_method' => 'required|string|in:bank_transfer,check,cash,other',
|
|
'notes' => 'nullable|string|max:1000',
|
|
]);
|
|
|
|
$buyerRecord = BuyerInvoiceRecord::where('invoice_id', $invoice->id)
|
|
->where('business_id', $business->id)
|
|
->first();
|
|
|
|
if ($buyerRecord) {
|
|
$buyerRecord->confirmPayment($user->id, $validated['payment_reference']);
|
|
}
|
|
|
|
// Mark invoice as paid (pending seller confirmation)
|
|
$invoice->update([
|
|
'status' => CrmInvoice::STATUS_PAID,
|
|
'paid_at' => now(),
|
|
]);
|
|
|
|
// TODO: Notify seller of payment
|
|
|
|
return redirect()->route('buyer.crm.invoices.show', $invoice)
|
|
->with('success', 'Payment confirmed. The seller will be notified.');
|
|
}
|
|
|
|
public function dispute(Request $request, CrmInvoice $invoice)
|
|
{
|
|
$business = Auth::user()->business;
|
|
|
|
if ($invoice->buyer_business_id !== $business->id) {
|
|
abort(403);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'reason' => 'required|string|max:2000',
|
|
]);
|
|
|
|
$buyerRecord = BuyerInvoiceRecord::getOrCreateForInvoice(
|
|
$business->id,
|
|
$invoice,
|
|
$invoice->order_id
|
|
);
|
|
|
|
$buyerRecord->openDispute($validated['reason']);
|
|
|
|
// TODO: Notify seller of dispute
|
|
// TODO: Create thread for dispute discussion
|
|
|
|
return back()->with('success', 'Dispute opened. The seller will be notified.');
|
|
}
|
|
|
|
public function download(CrmInvoice $invoice)
|
|
{
|
|
$business = Auth::user()->business;
|
|
|
|
if ($invoice->buyer_business_id !== $business->id) {
|
|
abort(403);
|
|
}
|
|
|
|
$invoice->load(['brand', 'items.product']);
|
|
|
|
// Generate PDF
|
|
$pdf = \PDF::loadView('buyer.crm.invoices.pdf', compact('invoice'));
|
|
|
|
return $pdf->download("invoice_{$invoice->invoice_number}.pdf");
|
|
}
|
|
|
|
public function export(Request $request)
|
|
{
|
|
$business = Auth::user()->business;
|
|
|
|
$query = CrmInvoice::forBuyerBusiness($business->id)
|
|
->with(['brand', 'order']);
|
|
|
|
// Apply filters
|
|
if ($status = $request->get('status')) {
|
|
$query->where('status', $status);
|
|
}
|
|
if ($from = $request->get('from_date')) {
|
|
$query->whereDate('issue_date', '>=', $from);
|
|
}
|
|
if ($to = $request->get('to_date')) {
|
|
$query->whereDate('issue_date', '<=', $to);
|
|
}
|
|
|
|
$invoices = $query->orderByDesc('issue_date')->get();
|
|
|
|
$filename = 'invoices_export_'.now()->format('Y-m-d_His').'.csv';
|
|
|
|
$headers = [
|
|
'Content-Type' => 'text/csv',
|
|
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
|
|
];
|
|
|
|
$callback = function () use ($invoices) {
|
|
$file = fopen('php://output', 'w');
|
|
fputcsv($file, ['Invoice #', 'Brand', 'Issue Date', 'Due Date', 'Status', 'Total', 'Paid']);
|
|
|
|
foreach ($invoices as $invoice) {
|
|
fputcsv($file, [
|
|
$invoice->invoice_number,
|
|
$invoice->brand->name ?? 'N/A',
|
|
$invoice->issue_date?->format('Y-m-d'),
|
|
$invoice->due_date?->format('Y-m-d'),
|
|
$invoice->status,
|
|
number_format($invoice->total, 2),
|
|
$invoice->paid_at?->format('Y-m-d') ?? 'No',
|
|
]);
|
|
}
|
|
|
|
fclose($file);
|
|
};
|
|
|
|
return response()->stream($callback, 200, $headers);
|
|
}
|
|
}
|