- Redesign dashboard as daily briefing format with action-first layout - Consolidate sidebar menu structure (Dashboard as single link) - Fix CRM form styling to use consistent UI patterns - Add PWA icons and push notification groundwork - Update SuiteMenuResolver for cleaner navigation
811 lines
30 KiB
PHP
811 lines
30 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Seller\Crm;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Activity;
|
|
use App\Models\Business;
|
|
use App\Models\Contact;
|
|
use App\Models\Crm\CrmEvent;
|
|
use App\Models\Crm\CrmQuote;
|
|
use App\Models\Crm\CrmTask;
|
|
use App\Models\Invoice;
|
|
use App\Models\Location;
|
|
use App\Models\SalesOpportunity;
|
|
use App\Models\SendMenuLog;
|
|
use App\Services\Cannaiq\CannaiqClient;
|
|
use Illuminate\Http\Request;
|
|
|
|
class AccountController extends Controller
|
|
{
|
|
/**
|
|
* Display accounts listing - only buyers who have ordered from this seller
|
|
*/
|
|
public function index(Request $request, Business $business)
|
|
{
|
|
$query = Business::where('type', 'buyer')
|
|
->whereHas('orders', function ($q) use ($business) {
|
|
$q->whereHas('items.product.brand', fn ($b) => $b->where('business_id', $business->id));
|
|
})
|
|
->with(['contacts']);
|
|
|
|
// Search filter
|
|
if ($request->filled('q')) {
|
|
$search = $request->q;
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('name', 'ILIKE', "%{$search}%")
|
|
->orWhere('dba_name', 'ILIKE', "%{$search}%")
|
|
->orWhere('business_email', 'ILIKE', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// Status filter - default to approved, but allow viewing all
|
|
if ($request->filled('status') && $request->status !== 'all') {
|
|
$query->where('status', $request->status);
|
|
} else {
|
|
$query->where('status', 'approved');
|
|
}
|
|
|
|
$accounts = $query->orderBy('name')->paginate(25);
|
|
|
|
// Return JSON for AJAX/API requests (live search)
|
|
if ($request->wantsJson()) {
|
|
return response()->json([
|
|
'data' => $accounts->map(fn ($a) => [
|
|
'slug' => $a->slug,
|
|
'name' => $a->name,
|
|
'email' => $a->business_email,
|
|
'status' => $a->status,
|
|
])->values()->toArray(),
|
|
]);
|
|
}
|
|
|
|
return view('seller.crm.accounts.index', compact('business', 'accounts'));
|
|
}
|
|
|
|
/**
|
|
* Show create customer form
|
|
*/
|
|
public function create(Request $request, Business $business)
|
|
{
|
|
return view('seller.crm.accounts.create', compact('business'));
|
|
}
|
|
|
|
/**
|
|
* Store a new customer (buyer business)
|
|
*/
|
|
public function store(Request $request, Business $business)
|
|
{
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'dba_name' => 'nullable|string|max:255',
|
|
'license_number' => 'nullable|string|max:100',
|
|
'business_email' => 'nullable|email|max:255',
|
|
'business_phone' => 'nullable|string|max:50',
|
|
'physical_address' => 'nullable|string|max:255',
|
|
'physical_city' => 'nullable|string|max:100',
|
|
'physical_state' => 'nullable|string|max:50',
|
|
'physical_zipcode' => 'nullable|string|max:20',
|
|
'contact_name' => 'nullable|string|max:255',
|
|
'contact_email' => 'nullable|email|max:255',
|
|
'contact_phone' => 'nullable|string|max:50',
|
|
'contact_title' => 'nullable|string|max:100',
|
|
]);
|
|
|
|
// Create the buyer business
|
|
$account = Business::create([
|
|
'name' => $validated['name'],
|
|
'dba_name' => $validated['dba_name'] ?? null,
|
|
'license_number' => $validated['license_number'] ?? null,
|
|
'business_email' => $validated['business_email'] ?? null,
|
|
'business_phone' => $validated['business_phone'] ?? null,
|
|
'physical_address' => $validated['physical_address'] ?? null,
|
|
'physical_city' => $validated['physical_city'] ?? null,
|
|
'physical_state' => $validated['physical_state'] ?? null,
|
|
'physical_zipcode' => $validated['physical_zipcode'] ?? null,
|
|
'type' => 'buyer',
|
|
'status' => 'approved', // Auto-approve customers created by sellers
|
|
]);
|
|
|
|
// Create contact if provided
|
|
if (! empty($validated['contact_name'])) {
|
|
$account->contacts()->create([
|
|
'first_name' => explode(' ', $validated['contact_name'])[0],
|
|
'last_name' => implode(' ', array_slice(explode(' ', $validated['contact_name']), 1)) ?: null,
|
|
'email' => $validated['contact_email'] ?? null,
|
|
'phone' => $validated['contact_phone'] ?? null,
|
|
'title' => $validated['contact_title'] ?? null,
|
|
]);
|
|
}
|
|
|
|
// Log the creation event
|
|
CrmEvent::log(
|
|
sellerBusinessId: $business->id,
|
|
eventType: 'account_created',
|
|
summary: "Customer {$account->name} created",
|
|
buyerBusinessId: $account->id,
|
|
userId: auth()->id(),
|
|
channel: 'system'
|
|
);
|
|
|
|
// Return JSON for AJAX requests
|
|
if ($request->expectsJson()) {
|
|
return response()->json([
|
|
'id' => $account->id,
|
|
'name' => $account->name,
|
|
'slug' => $account->slug,
|
|
]);
|
|
}
|
|
|
|
return redirect()
|
|
->route('seller.business.crm.accounts.show', [$business->slug, $account->slug])
|
|
->with('success', 'Customer created successfully.');
|
|
}
|
|
|
|
/**
|
|
* Show edit customer form
|
|
*/
|
|
public function edit(Request $request, Business $business, Business $account)
|
|
{
|
|
return view('seller.crm.accounts.edit', compact('business', 'account'));
|
|
}
|
|
|
|
/**
|
|
* Update a customer (buyer business)
|
|
*/
|
|
public function update(Request $request, Business $business, Business $account)
|
|
{
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'dba_name' => 'nullable|string|max:255',
|
|
'license_number' => 'nullable|string|max:100',
|
|
'business_email' => 'nullable|email|max:255',
|
|
'business_phone' => 'nullable|string|max:50',
|
|
'physical_address' => 'nullable|string|max:255',
|
|
'physical_city' => 'nullable|string|max:100',
|
|
'physical_state' => 'nullable|string|max:50',
|
|
'physical_zipcode' => 'nullable|string|max:20',
|
|
]);
|
|
|
|
$account->update($validated);
|
|
|
|
return redirect()
|
|
->route('seller.business.crm.accounts.show', [$business->slug, $account->slug])
|
|
->with('success', 'Customer updated successfully.');
|
|
}
|
|
|
|
/**
|
|
* Show account details
|
|
*/
|
|
public function show(Request $request, Business $business, Business $account)
|
|
{
|
|
$account->load(['contacts']);
|
|
|
|
// Location filtering
|
|
$locationId = $request->query('location');
|
|
$selectedLocation = $locationId ? $account->locations()->find($locationId) : null;
|
|
|
|
// Load all locations for this account with contacts pivot
|
|
$locations = $account->locations()
|
|
->with(['contacts' => function ($q) {
|
|
$q->wherePivot('role', 'buyer');
|
|
}])
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
// Base order query for this seller
|
|
$baseOrderQuery = fn () => $account->orders()
|
|
->whereHas('items.product.brand', function ($q) use ($business) {
|
|
$q->where('business_id', $business->id);
|
|
});
|
|
|
|
// Get orders (filtered by location if selected)
|
|
$ordersQuery = $baseOrderQuery();
|
|
if ($selectedLocation) {
|
|
$ordersQuery->where('location_id', $selectedLocation->id);
|
|
}
|
|
$orders = $ordersQuery->with(['invoice', 'location'])->latest()->limit(10)->get();
|
|
|
|
// Get quotes for this account (filtered by location if selected)
|
|
$quotesQuery = CrmQuote::where('business_id', $business->id)
|
|
->where('account_id', $account->id);
|
|
if ($selectedLocation) {
|
|
$quotesQuery->where('location_id', $selectedLocation->id);
|
|
}
|
|
$quotes = $quotesQuery->with(['contact', 'items'])->latest()->limit(10)->get();
|
|
|
|
// Base invoice query
|
|
$baseInvoiceQuery = fn () => Invoice::whereHas('order', function ($q) use ($business, $account) {
|
|
$q->where('business_id', $account->id)
|
|
->whereHas('items.product.brand', function ($q2) use ($business) {
|
|
$q2->where('business_id', $business->id);
|
|
});
|
|
});
|
|
|
|
// Get invoices (filtered by location if selected)
|
|
$invoicesQuery = $baseInvoiceQuery();
|
|
if ($selectedLocation) {
|
|
$invoicesQuery->whereHas('order', function ($q) use ($selectedLocation) {
|
|
$q->where('location_id', $selectedLocation->id);
|
|
});
|
|
}
|
|
$invoices = $invoicesQuery->with(['order', 'payments'])->latest()->limit(10)->get();
|
|
|
|
// Get opportunities for this account from this seller
|
|
$opportunities = SalesOpportunity::where('seller_business_id', $business->id)
|
|
->where('business_id', $account->id)
|
|
->with(['stage', 'brand'])
|
|
->latest()
|
|
->get();
|
|
|
|
// Get tasks related to this account
|
|
$tasks = CrmTask::where('seller_business_id', $business->id)
|
|
->where('business_id', $account->id)
|
|
->whereNull('completed_at')
|
|
->with('assignee')
|
|
->orderBy('due_at')
|
|
->limit(5)
|
|
->get();
|
|
|
|
// Get conversation events for this account
|
|
$conversationEvents = CrmEvent::where('seller_business_id', $business->id)
|
|
->where('buyer_business_id', $account->id)
|
|
->latest('occurred_at')
|
|
->limit(20)
|
|
->get();
|
|
|
|
// Get menu send history for this account
|
|
$sendHistory = SendMenuLog::where('business_id', $business->id)
|
|
->where('customer_id', $account->id)
|
|
->with(['menu', 'brand'])
|
|
->latest('sent_at')
|
|
->limit(10)
|
|
->get();
|
|
|
|
// Get activity log for this account
|
|
$activities = Activity::where('seller_business_id', $business->id)
|
|
->where('business_id', $account->id)
|
|
->with(['causer'])
|
|
->latest()
|
|
->limit(20)
|
|
->get();
|
|
|
|
// Compute stats - if location selected, show location-specific stats
|
|
if ($selectedLocation) {
|
|
$orderStats = $baseOrderQuery()
|
|
->where('location_id', $selectedLocation->id)
|
|
->selectRaw('COUNT(*) as total_orders, COALESCE(SUM(total), 0) as total_revenue')
|
|
->first();
|
|
} else {
|
|
$orderStats = $baseOrderQuery()
|
|
->selectRaw('COUNT(*) as total_orders, COALESCE(SUM(total), 0) as total_revenue')
|
|
->first();
|
|
}
|
|
|
|
$opportunityStats = SalesOpportunity::where('seller_business_id', $business->id)
|
|
->where('business_id', $account->id)
|
|
->where('status', 'open')
|
|
->selectRaw('COUNT(*) as open_count, COALESCE(SUM(value), 0) as pipeline_value')
|
|
->first();
|
|
|
|
// Financial stats from invoices (location-filtered if applicable)
|
|
$financialStatsQuery = $baseInvoiceQuery();
|
|
if ($selectedLocation) {
|
|
$financialStatsQuery->whereHas('order', function ($q) use ($selectedLocation) {
|
|
$q->where('location_id', $selectedLocation->id);
|
|
});
|
|
}
|
|
$financialStats = $financialStatsQuery->selectRaw('
|
|
COALESCE(SUM(amount_due), 0) as outstanding_balance,
|
|
COALESCE(SUM(CASE WHEN due_date < CURRENT_DATE AND amount_due > 0 THEN amount_due ELSE 0 END), 0) as past_due_amount,
|
|
COUNT(CASE WHEN amount_due > 0 THEN 1 END) as open_invoice_count,
|
|
MIN(CASE WHEN due_date < CURRENT_DATE AND amount_due > 0 THEN due_date END) as oldest_past_due_date
|
|
')
|
|
->first();
|
|
|
|
// Get last payment info
|
|
$lastPayment = \App\Models\InvoicePayment::whereHas('invoice.order', function ($q) use ($business, $account) {
|
|
$q->where('business_id', $account->id)
|
|
->whereHas('items.product.brand', function ($q2) use ($business) {
|
|
$q2->where('business_id', $business->id);
|
|
});
|
|
})
|
|
->latest('payment_date')
|
|
->first();
|
|
|
|
$stats = [
|
|
'total_orders' => $orderStats->total_orders ?? 0,
|
|
'total_revenue' => $orderStats->total_revenue ?? 0,
|
|
'open_opportunities' => $opportunityStats->open_count ?? 0,
|
|
'pipeline_value' => $opportunityStats->pipeline_value ?? 0,
|
|
];
|
|
|
|
$financials = [
|
|
'outstanding_balance' => $financialStats->outstanding_balance ?? 0,
|
|
'past_due_amount' => $financialStats->past_due_amount ?? 0,
|
|
'open_invoice_count' => $financialStats->open_invoice_count ?? 0,
|
|
'oldest_past_due_days' => $financialStats->oldest_past_due_date
|
|
? (int) ceil(abs(now()->diffInDays($financialStats->oldest_past_due_date)))
|
|
: null,
|
|
'last_payment_amount' => $lastPayment->amount ?? null,
|
|
'last_payment_date' => $lastPayment->payment_date ?? null,
|
|
];
|
|
|
|
// Calculate unattributed orders/invoices (those without location_id)
|
|
$unattributedOrdersCount = $baseOrderQuery()->whereNull('location_id')->count();
|
|
$unattributedInvoicesCount = $baseInvoiceQuery()
|
|
->whereHas('order', function ($q) {
|
|
$q->whereNull('location_id');
|
|
})
|
|
->count();
|
|
|
|
// Calculate per-location stats for location tiles
|
|
$locationStats = [];
|
|
if ($locations->count() > 0) {
|
|
$locationIds = $locations->pluck('id')->toArray();
|
|
|
|
// Order stats by location
|
|
$ordersByLocation = $baseOrderQuery()
|
|
->whereIn('location_id', $locationIds)
|
|
->selectRaw('location_id, COUNT(*) as orders_count, COALESCE(SUM(total), 0) as revenue')
|
|
->groupBy('location_id')
|
|
->get()
|
|
->keyBy('location_id');
|
|
|
|
// Invoice stats by location
|
|
$invoicesByLocation = Invoice::whereHas('order', function ($q) use ($business, $account, $locationIds) {
|
|
$q->where('business_id', $account->id)
|
|
->whereIn('location_id', $locationIds)
|
|
->whereHas('items.product.brand', function ($q2) use ($business) {
|
|
$q2->where('business_id', $business->id);
|
|
});
|
|
})
|
|
->selectRaw('
|
|
(SELECT location_id FROM orders WHERE orders.id = invoices.order_id) as location_id,
|
|
COALESCE(SUM(amount_due), 0) as outstanding,
|
|
COALESCE(SUM(CASE WHEN due_date < CURRENT_DATE AND amount_due > 0 THEN amount_due ELSE 0 END), 0) as past_due,
|
|
COUNT(CASE WHEN amount_due > 0 THEN 1 END) as open_invoices
|
|
')
|
|
->groupByRaw('(SELECT location_id FROM orders WHERE orders.id = invoices.order_id)')
|
|
->get()
|
|
->keyBy('location_id');
|
|
|
|
foreach ($locations as $location) {
|
|
$orderData = $ordersByLocation->get($location->id);
|
|
$invoiceData = $invoicesByLocation->get($location->id);
|
|
|
|
$ordersCount = $orderData->orders_count ?? 0;
|
|
$openInvoices = $invoiceData->open_invoices ?? 0;
|
|
|
|
$locationStats[$location->id] = [
|
|
'orders' => $ordersCount,
|
|
'revenue' => $orderData->revenue ?? 0,
|
|
'outstanding' => $invoiceData->outstanding ?? 0,
|
|
'past_due' => $invoiceData->past_due ?? 0,
|
|
'open_invoices' => $openInvoices,
|
|
'has_attributed_data' => ($ordersCount + $openInvoices) > 0,
|
|
];
|
|
}
|
|
}
|
|
|
|
return view('seller.crm.accounts.show', compact(
|
|
'business',
|
|
'account',
|
|
'stats',
|
|
'financials',
|
|
'orders',
|
|
'quotes',
|
|
'invoices',
|
|
'opportunities',
|
|
'tasks',
|
|
'conversationEvents',
|
|
'sendHistory',
|
|
'activities',
|
|
'locations',
|
|
'selectedLocation',
|
|
'locationStats',
|
|
'unattributedOrdersCount',
|
|
'unattributedInvoicesCount'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Show account contacts
|
|
*/
|
|
public function contacts(Request $request, Business $business, Business $account)
|
|
{
|
|
// Location filtering
|
|
$locationId = $request->query('location');
|
|
$selectedLocation = $locationId ? $account->locations()->find($locationId) : null;
|
|
|
|
// Base query for contacts
|
|
$contactsQuery = $account->contacts();
|
|
|
|
// If location selected, filter to contacts assigned to that location
|
|
if ($selectedLocation) {
|
|
$contactsQuery->whereHas('locations', function ($q) use ($selectedLocation) {
|
|
$q->where('locations.id', $selectedLocation->id);
|
|
});
|
|
}
|
|
|
|
$contacts = $contactsQuery->paginate(25);
|
|
|
|
// Load locations for the scope bar
|
|
$locations = $account->locations()->orderBy('name')->get();
|
|
|
|
return view('seller.crm.accounts.contacts', compact('business', 'account', 'contacts', 'locations', 'selectedLocation'));
|
|
}
|
|
|
|
/**
|
|
* Show account opportunities
|
|
*/
|
|
public function opportunities(Request $request, Business $business, Business $account)
|
|
{
|
|
// Location filtering (note: opportunities don't have location_id yet, so we just pass the context)
|
|
$locationId = $request->query('location');
|
|
$selectedLocation = $locationId ? $account->locations()->find($locationId) : null;
|
|
|
|
// Load opportunities for this account
|
|
$opportunities = SalesOpportunity::where('seller_business_id', $business->id)
|
|
->where('business_id', $account->id)
|
|
->with(['stage', 'brand', 'owner'])
|
|
->latest()
|
|
->paginate(25);
|
|
|
|
// Load locations for the scope bar
|
|
$locations = $account->locations()->orderBy('name')->get();
|
|
|
|
return view('seller.crm.accounts.opportunities', compact('business', 'account', 'opportunities', 'locations', 'selectedLocation'));
|
|
}
|
|
|
|
/**
|
|
* Show account orders
|
|
*/
|
|
public function orders(Request $request, Business $business, Business $account)
|
|
{
|
|
// Location filtering
|
|
$locationId = $request->query('location');
|
|
$selectedLocation = $locationId ? $account->locations()->find($locationId) : null;
|
|
|
|
$ordersQuery = $account->orders()
|
|
->whereHas('items.product.brand', function ($q) use ($business) {
|
|
$q->where('business_id', $business->id);
|
|
});
|
|
|
|
// Filter by location if selected
|
|
if ($selectedLocation) {
|
|
$ordersQuery->where('location_id', $selectedLocation->id);
|
|
}
|
|
|
|
$orders = $ordersQuery->with(['items.product.brand', 'location'])
|
|
->latest()
|
|
->paginate(25);
|
|
|
|
// Load locations for the scope bar
|
|
$locations = $account->locations()->orderBy('name')->get();
|
|
|
|
return view('seller.crm.accounts.orders', compact('business', 'account', 'orders', 'locations', 'selectedLocation'));
|
|
}
|
|
|
|
/**
|
|
* Show account activity
|
|
*/
|
|
public function activity(Request $request, Business $business, Business $account)
|
|
{
|
|
// Location filtering (note: activities don't have location_id yet, so we just pass the context)
|
|
$locationId = $request->query('location');
|
|
$selectedLocation = $locationId ? $account->locations()->find($locationId) : null;
|
|
|
|
$activities = Activity::where('seller_business_id', $business->id)
|
|
->where('business_id', $account->id)
|
|
->with(['causer'])
|
|
->latest()
|
|
->paginate(50);
|
|
|
|
// Load locations for the scope bar
|
|
$locations = $account->locations()->orderBy('name')->get();
|
|
|
|
return view('seller.crm.accounts.activity', compact('business', 'account', 'activities', 'locations', 'selectedLocation'));
|
|
}
|
|
|
|
/**
|
|
* Show account tasks
|
|
*/
|
|
public function tasks(Request $request, Business $business, Business $account)
|
|
{
|
|
// Location filtering (note: tasks don't have location_id yet, so we just pass the context)
|
|
$locationId = $request->query('location');
|
|
$selectedLocation = $locationId ? $account->locations()->find($locationId) : null;
|
|
|
|
// Load tasks for this account
|
|
$tasks = CrmTask::where('seller_business_id', $business->id)
|
|
->where('business_id', $account->id)
|
|
->with(['assignee', 'opportunity'])
|
|
->orderByRaw('completed_at IS NOT NULL')
|
|
->orderBy('due_at')
|
|
->paginate(25);
|
|
|
|
// Load locations for the scope bar
|
|
$locations = $account->locations()->orderBy('name')->get();
|
|
|
|
return view('seller.crm.accounts.tasks', compact('business', 'account', 'tasks', 'locations', 'selectedLocation'));
|
|
}
|
|
|
|
/**
|
|
* Store a note for an account
|
|
*/
|
|
public function storeNote(Request $request, Business $business, Business $account)
|
|
{
|
|
$request->validate([
|
|
'note' => 'required|string|max:5000',
|
|
]);
|
|
|
|
CrmEvent::log(
|
|
sellerBusinessId: $business->id,
|
|
eventType: 'note_added',
|
|
summary: $request->input('note'),
|
|
buyerBusinessId: $account->id,
|
|
userId: auth()->id(),
|
|
channel: 'system'
|
|
);
|
|
|
|
return redirect()
|
|
->route('seller.business.crm.accounts.show', [$business->slug, $account->slug])
|
|
->with('success', 'Note added successfully.');
|
|
}
|
|
|
|
/**
|
|
* Store a new contact for an account
|
|
*/
|
|
public function storeContact(Request $request, Business $business, Business $account)
|
|
{
|
|
$validated = $request->validate([
|
|
'first_name' => 'required|string|max:100',
|
|
'last_name' => 'nullable|string|max:100',
|
|
'email' => 'nullable|email|max:255',
|
|
'phone' => 'nullable|string|max:50',
|
|
'title' => 'nullable|string|max:100',
|
|
]);
|
|
|
|
$contact = $account->contacts()->create($validated);
|
|
|
|
// Return JSON for AJAX requests
|
|
if ($request->expectsJson()) {
|
|
return response()->json([
|
|
'id' => $contact->id,
|
|
'first_name' => $contact->first_name,
|
|
'last_name' => $contact->last_name,
|
|
'email' => $contact->email,
|
|
'phone' => $contact->phone,
|
|
'title' => $contact->title,
|
|
]);
|
|
}
|
|
|
|
return redirect()
|
|
->route('seller.business.crm.accounts.contacts', [$business->slug, $account->slug])
|
|
->with('success', 'Contact added successfully.');
|
|
}
|
|
|
|
/**
|
|
* Show edit contact form
|
|
*/
|
|
public function editContact(Request $request, Business $business, Business $account, Contact $contact)
|
|
{
|
|
// Verify contact belongs to this account
|
|
if ($contact->business_id !== $account->id) {
|
|
abort(404);
|
|
}
|
|
|
|
return view('seller.crm.accounts.contacts-edit', compact('business', 'account', 'contact'));
|
|
}
|
|
|
|
/**
|
|
* Update a contact
|
|
*/
|
|
public function updateContact(Request $request, Business $business, Business $account, Contact $contact)
|
|
{
|
|
// Verify contact belongs to this account
|
|
if ($contact->business_id !== $account->id) {
|
|
abort(404);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'first_name' => 'required|string|max:100',
|
|
'last_name' => 'nullable|string|max:100',
|
|
'email' => 'nullable|email|max:255',
|
|
'phone' => 'nullable|string|max:50',
|
|
'title' => 'nullable|string|max:100',
|
|
'is_active' => 'boolean',
|
|
]);
|
|
|
|
// Handle checkbox - if not sent, default to false
|
|
$validated['is_active'] = $request->boolean('is_active');
|
|
|
|
$contact->update($validated);
|
|
|
|
return redirect()
|
|
->route('seller.business.crm.accounts.contacts', [$business->slug, $account->slug])
|
|
->with('success', 'Contact updated successfully.');
|
|
}
|
|
|
|
/**
|
|
* Delete a contact
|
|
*/
|
|
public function destroyContact(Request $request, Business $business, Business $account, Contact $contact)
|
|
{
|
|
// Verify contact belongs to this account
|
|
if ($contact->business_id !== $account->id) {
|
|
abort(404);
|
|
}
|
|
|
|
$contact->delete();
|
|
|
|
return redirect()
|
|
->route('seller.business.crm.accounts.contacts', [$business->slug, $account->slug])
|
|
->with('success', 'Contact deleted successfully.');
|
|
}
|
|
|
|
/**
|
|
* Show location edit form
|
|
*/
|
|
public function editLocation(Request $request, Business $business, Business $account, Location $location)
|
|
{
|
|
// Verify location belongs to this account
|
|
if ($location->business_id !== $account->id) {
|
|
abort(404);
|
|
}
|
|
|
|
// Load contacts that can be assigned to this location
|
|
$contacts = $account->contacts()->orderBy('first_name')->get();
|
|
|
|
// Load currently assigned contacts with their roles
|
|
$locationContacts = $location->contacts()->get();
|
|
|
|
// Available roles for location contacts
|
|
$contactRoles = [
|
|
'buyer' => 'Buyer',
|
|
'ap' => 'Accounts Payable',
|
|
'marketing' => 'Marketing',
|
|
'gm' => 'General Manager',
|
|
'inventory' => 'Inventory Manager',
|
|
'other' => 'Other',
|
|
];
|
|
|
|
// CannaiQ platforms
|
|
$cannaiqPlatforms = [
|
|
'dutchie' => 'Dutchie',
|
|
'jane' => 'Jane',
|
|
'weedmaps' => 'Weedmaps',
|
|
'leafly' => 'Leafly',
|
|
'iheartjane' => 'iHeartJane',
|
|
'other' => 'Other',
|
|
];
|
|
|
|
return view('seller.crm.accounts.locations-edit', compact(
|
|
'business',
|
|
'account',
|
|
'location',
|
|
'contacts',
|
|
'locationContacts',
|
|
'contactRoles',
|
|
'cannaiqPlatforms'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Update location
|
|
*/
|
|
public function updateLocation(Request $request, Business $business, Business $account, Location $location)
|
|
{
|
|
// Verify location belongs to this account
|
|
if ($location->business_id !== $account->id) {
|
|
abort(404);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'address' => 'nullable|string|max:255',
|
|
'city' => 'nullable|string|max:100',
|
|
'state' => 'nullable|string|max:50',
|
|
'zipcode' => 'nullable|string|max:20',
|
|
'phone' => 'nullable|string|max:50',
|
|
'email' => 'nullable|email|max:255',
|
|
'is_active' => 'boolean',
|
|
'cannaiq_platform' => 'nullable|string|max:50',
|
|
'cannaiq_store_slug' => 'nullable|string|max:255',
|
|
'cannaiq_store_id' => 'nullable|string|max:100',
|
|
'cannaiq_store_name' => 'nullable|string|max:255',
|
|
'contact_roles' => 'nullable|array',
|
|
'contact_roles.*.contact_id' => 'required|exists:contacts,id',
|
|
'contact_roles.*.role' => 'required|string|max:50',
|
|
'contact_roles.*.is_primary' => 'boolean',
|
|
]);
|
|
|
|
// Handle checkbox
|
|
$validated['is_active'] = $request->boolean('is_active');
|
|
|
|
// Clear CannaiQ fields if platform is cleared
|
|
if (empty($validated['cannaiq_platform'])) {
|
|
$validated['cannaiq_store_slug'] = null;
|
|
$validated['cannaiq_store_id'] = null;
|
|
$validated['cannaiq_store_name'] = null;
|
|
}
|
|
|
|
// Update location
|
|
$location->update([
|
|
'name' => $validated['name'],
|
|
'address' => $validated['address'] ?? null,
|
|
'city' => $validated['city'] ?? null,
|
|
'state' => $validated['state'] ?? null,
|
|
'zipcode' => $validated['zipcode'] ?? null,
|
|
'phone' => $validated['phone'] ?? null,
|
|
'email' => $validated['email'] ?? null,
|
|
'is_active' => $validated['is_active'],
|
|
'cannaiq_platform' => $validated['cannaiq_platform'] ?? null,
|
|
'cannaiq_store_slug' => $validated['cannaiq_store_slug'] ?? null,
|
|
'cannaiq_store_id' => $validated['cannaiq_store_id'] ?? null,
|
|
'cannaiq_store_name' => $validated['cannaiq_store_name'] ?? null,
|
|
]);
|
|
|
|
// Sync location contacts
|
|
if (isset($validated['contact_roles'])) {
|
|
$syncData = [];
|
|
foreach ($validated['contact_roles'] as $contactRole) {
|
|
// Verify contact belongs to this account
|
|
$contact = Contact::where('business_id', $account->id)
|
|
->where('id', $contactRole['contact_id'])
|
|
->first();
|
|
|
|
if ($contact) {
|
|
$syncData[$contact->id] = [
|
|
'role' => $contactRole['role'],
|
|
'is_primary' => $contactRole['is_primary'] ?? false,
|
|
];
|
|
}
|
|
}
|
|
$location->contacts()->sync($syncData);
|
|
} else {
|
|
$location->contacts()->detach();
|
|
}
|
|
|
|
return redirect()
|
|
->route('seller.business.crm.accounts.show', [$business->slug, $account->slug])
|
|
->with('success', 'Location updated successfully.');
|
|
}
|
|
|
|
/**
|
|
* Search CannaiQ stores for linking
|
|
*/
|
|
public function searchCannaiqStores(Request $request, Business $business, Business $account, Location $location)
|
|
{
|
|
// Verify location belongs to this account
|
|
if ($location->business_id !== $account->id) {
|
|
abort(404);
|
|
}
|
|
|
|
$request->validate([
|
|
'platform' => 'required|string|max:50',
|
|
'query' => 'required|string|min:2|max:100',
|
|
]);
|
|
|
|
try {
|
|
$client = app(CannaiqClient::class);
|
|
$results = $client->searchStores(
|
|
platform: $request->input('platform'),
|
|
query: $request->input('query')
|
|
);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'stores' => $results,
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'Failed to search stores: '.$e->getMessage(),
|
|
'stores' => [],
|
|
], 500);
|
|
}
|
|
}
|
|
}
|