Issues fixed: - #161: Quote submission - add tax_rate migration (already existed) - #162: Contact edit 404 - change contact->id to contact->hashid in routes - #163: Batch creation - expand batch_type constraint to include component/homogenized - #164: Stock search - convert client-side to server-side search - #165: Product edit save button - make always visible with different states - #166: Product creation validation - make price_unit nullable with default - #167: Invoice products - change stockFilter default from true to false Additional fixes: - Fix layouts.seller (non-existent) to layouts.app-with-sidebar in 14 views - Fix CRM route names from seller.crm.* to seller.business.crm.*
241 lines
9.1 KiB
PHP
241 lines
9.1 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Seller;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Jobs\RunMarketingAutomationJob;
|
|
use App\Models\Business;
|
|
use App\Models\Marketing\MarketingAutomation;
|
|
use App\Models\Marketing\MarketingList;
|
|
use App\Models\Marketing\MarketingTemplate;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
|
class MarketingAutomationController extends Controller
|
|
{
|
|
public function index(Request $request, Business $business)
|
|
{
|
|
$this->authorizeForBusiness($business);
|
|
|
|
$automations = MarketingAutomation::where('business_id', $business->id)
|
|
->with('latestRun')
|
|
->when($request->status === 'active', fn ($q) => $q->where('is_active', true))
|
|
->when($request->status === 'inactive', fn ($q) => $q->where('is_active', false))
|
|
->when($request->trigger_type, fn ($q, $type) => $q->where('trigger_type', $type))
|
|
->latest()
|
|
->paginate(15);
|
|
|
|
return view('seller.marketing.automations.index', compact('business', 'automations'));
|
|
}
|
|
|
|
public function create(Request $request, Business $business)
|
|
{
|
|
$this->authorizeForBusiness($business);
|
|
|
|
$presets = MarketingAutomation::getTypePresets();
|
|
$selectedPreset = $request->query('preset');
|
|
|
|
$lists = MarketingList::where('business_id', $business->id)
|
|
->withCount('contacts')
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
$templates = MarketingTemplate::where('business_id', $business->id)
|
|
->where('is_active', true)
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
return view('seller.marketing.automations.create', compact(
|
|
'business',
|
|
'presets',
|
|
'selectedPreset',
|
|
'lists',
|
|
'templates'
|
|
));
|
|
}
|
|
|
|
public function store(Request $request, Business $business)
|
|
{
|
|
$this->authorizeForBusiness($business);
|
|
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'description' => 'nullable|string|max:1000',
|
|
'scope' => 'required|in:internal,portal',
|
|
'trigger_type' => 'required|in:'.implode(',', array_keys(MarketingAutomation::TRIGGER_TYPES)),
|
|
'trigger_config' => 'required|json',
|
|
'condition_config' => 'required|json',
|
|
'action_config' => 'required|json',
|
|
]);
|
|
|
|
// Decode JSON configs from the form
|
|
$triggerConfig = json_decode($validated['trigger_config'], true) ?? [];
|
|
$conditionConfig = json_decode($validated['condition_config'], true) ?? [];
|
|
$actionConfig = json_decode($validated['action_config'], true) ?? [];
|
|
|
|
// Normalize condition config - convert percentage values
|
|
if (isset($conditionConfig['min_price_advantage']) && $conditionConfig['min_price_advantage'] > 1) {
|
|
$conditionConfig['min_price_advantage'] = $conditionConfig['min_price_advantage'] / 100;
|
|
}
|
|
|
|
// Map velocity_threshold to velocity_30d_threshold for slow mover clearance
|
|
if (isset($conditionConfig['velocity_threshold'])) {
|
|
$conditionConfig['velocity_30d_threshold'] = $conditionConfig['velocity_threshold'];
|
|
unset($conditionConfig['velocity_threshold']);
|
|
}
|
|
|
|
$automation = MarketingAutomation::create([
|
|
'business_id' => $business->id,
|
|
'name' => $validated['name'],
|
|
'description' => $validated['description'],
|
|
'is_active' => true,
|
|
'scope' => $validated['scope'],
|
|
'trigger_type' => $validated['trigger_type'],
|
|
'trigger_config' => $triggerConfig,
|
|
'condition_config' => $conditionConfig,
|
|
'action_config' => $actionConfig,
|
|
]);
|
|
|
|
return redirect()
|
|
->route('seller.business.marketing.automations.index', $business)
|
|
->with('success', "Automation \"{$automation->name}\" created successfully.");
|
|
}
|
|
|
|
public function edit(Request $request, Business $business, MarketingAutomation $automation)
|
|
{
|
|
$this->authorizeForBusiness($business);
|
|
$this->ensureAutomationBelongsToBusiness($automation, $business);
|
|
|
|
$presets = MarketingAutomation::getTypePresets();
|
|
|
|
$lists = MarketingList::where('business_id', $business->id)
|
|
->withCount('contacts')
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
$templates = MarketingTemplate::where('business_id', $business->id)
|
|
->where('is_active', true)
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
return view('seller.marketing.automations.edit', compact(
|
|
'business',
|
|
'automation',
|
|
'presets',
|
|
'lists',
|
|
'templates'
|
|
));
|
|
}
|
|
|
|
public function update(Request $request, Business $business, MarketingAutomation $automation)
|
|
{
|
|
$this->authorizeForBusiness($business);
|
|
$this->ensureAutomationBelongsToBusiness($automation, $business);
|
|
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'description' => 'nullable|string|max:1000',
|
|
'scope' => 'required|in:internal,portal',
|
|
'trigger_type' => 'required|in:'.implode(',', array_keys(MarketingAutomation::TRIGGER_TYPES)),
|
|
'trigger_config' => 'required|json',
|
|
'condition_config' => 'required|json',
|
|
'action_config' => 'required|json',
|
|
]);
|
|
|
|
// Decode JSON configs from the form
|
|
$triggerConfig = json_decode($validated['trigger_config'], true) ?? [];
|
|
$conditionConfig = json_decode($validated['condition_config'], true) ?? [];
|
|
$actionConfig = json_decode($validated['action_config'], true) ?? [];
|
|
|
|
// Normalize condition config - convert percentage values
|
|
if (isset($conditionConfig['min_price_advantage']) && $conditionConfig['min_price_advantage'] > 1) {
|
|
$conditionConfig['min_price_advantage'] = $conditionConfig['min_price_advantage'] / 100;
|
|
}
|
|
|
|
// Map velocity_threshold to velocity_30d_threshold for slow mover clearance
|
|
if (isset($conditionConfig['velocity_threshold'])) {
|
|
$conditionConfig['velocity_30d_threshold'] = $conditionConfig['velocity_threshold'];
|
|
unset($conditionConfig['velocity_threshold']);
|
|
}
|
|
|
|
$automation->update([
|
|
'name' => $validated['name'],
|
|
'description' => $validated['description'],
|
|
'scope' => $validated['scope'],
|
|
'trigger_type' => $validated['trigger_type'],
|
|
'trigger_config' => $triggerConfig,
|
|
'condition_config' => $conditionConfig,
|
|
'action_config' => $actionConfig,
|
|
]);
|
|
|
|
return redirect()
|
|
->route('seller.business.marketing.automations.index', $business)
|
|
->with('success', "Automation \"{$automation->name}\" updated successfully.");
|
|
}
|
|
|
|
public function toggle(Request $request, Business $business, MarketingAutomation $automation)
|
|
{
|
|
$this->authorizeForBusiness($business);
|
|
$this->ensureAutomationBelongsToBusiness($automation, $business);
|
|
|
|
$automation->update([
|
|
'is_active' => ! $automation->is_active,
|
|
]);
|
|
|
|
$status = $automation->is_active ? 'enabled' : 'disabled';
|
|
|
|
return redirect()
|
|
->back()
|
|
->with('success', "Automation \"{$automation->name}\" has been {$status}.");
|
|
}
|
|
|
|
public function runNow(Request $request, Business $business, MarketingAutomation $automation)
|
|
{
|
|
$this->authorizeForBusiness($business);
|
|
$this->ensureAutomationBelongsToBusiness($automation, $business);
|
|
|
|
if (! $automation->is_active) {
|
|
return redirect()
|
|
->back()
|
|
->with('error', 'Cannot run an inactive automation. Enable it first.');
|
|
}
|
|
|
|
// Dispatch the job
|
|
RunMarketingAutomationJob::dispatch($automation->id);
|
|
|
|
return redirect()
|
|
->route('seller.business.marketing.automations.runs.index', [$business, $automation])
|
|
->with('success', "Automation \"{$automation->name}\" has been queued to run.");
|
|
}
|
|
|
|
public function destroy(Request $request, Business $business, MarketingAutomation $automation)
|
|
{
|
|
$this->authorizeForBusiness($business);
|
|
$this->ensureAutomationBelongsToBusiness($automation, $business);
|
|
|
|
$name = $automation->name;
|
|
$automation->delete();
|
|
|
|
return redirect()
|
|
->route('seller.business.marketing.automations.index', $business)
|
|
->with('success', "Automation \"{$name}\" has been deleted.");
|
|
}
|
|
|
|
protected function authorizeForBusiness(Business $business): void
|
|
{
|
|
$user = Auth::user();
|
|
|
|
// Check user has access to this business
|
|
if (! $user->businesses->contains($business->id) && ! $user->hasRole('Super Admin')) {
|
|
abort(403, 'Unauthorized access to this business.');
|
|
}
|
|
}
|
|
|
|
protected function ensureAutomationBelongsToBusiness(MarketingAutomation $automation, Business $business): void
|
|
{
|
|
if ($automation->business_id !== $business->id) {
|
|
abort(404);
|
|
}
|
|
}
|
|
}
|