Merge pull request 'fix: CRM controller fixes and missing migrations' (#125) from fix/buyer-crm-namespaces into develop
Reviewed-on: https://code.cannabrands.app/Cannabrands/hub/pulls/125
This commit is contained in:
@@ -6,10 +6,10 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\Brand;
|
||||
use App\Models\Buyer\BuyerBrandFollow;
|
||||
use App\Models\Buyer\BuyerProductBookmark;
|
||||
use App\Models\Crm\CrmThread;
|
||||
use App\Models\Seller\BrandAnnouncement;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Modules\Crm\Entities\CrmThread;
|
||||
|
||||
class BrandHubController extends Controller
|
||||
{
|
||||
|
||||
@@ -7,12 +7,12 @@ use App\Models\Buyer\BuyerAnalyticsCache;
|
||||
use App\Models\Buyer\BuyerBrandFollow;
|
||||
use App\Models\Buyer\BuyerQuoteApproval;
|
||||
use App\Models\Buyer\BuyerTask;
|
||||
use App\Models\Crm\CrmInvoice;
|
||||
use App\Models\Crm\CrmQuote;
|
||||
use App\Models\Crm\CrmThread;
|
||||
use App\Models\Order;
|
||||
use App\Models\Seller\BrandAnnouncement;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Modules\Crm\Entities\CrmInvoice;
|
||||
use Modules\Crm\Entities\CrmQuote;
|
||||
use Modules\Crm\Entities\CrmThread;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
|
||||
@@ -4,9 +4,9 @@ namespace App\Http\Controllers\Buyer\Crm;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Buyer\BuyerMessageSettings;
|
||||
use App\Models\Crm\CrmThread;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Modules\Crm\Entities\CrmThread;
|
||||
|
||||
class InboxController extends Controller
|
||||
{
|
||||
|
||||
@@ -5,9 +5,10 @@ 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;
|
||||
use Modules\Crm\Entities\CrmInvoice;
|
||||
|
||||
class InvoiceController extends Controller
|
||||
{
|
||||
@@ -115,7 +116,7 @@ class InvoiceController extends Controller
|
||||
}
|
||||
|
||||
// Get related thread if exists
|
||||
$thread = \Modules\Crm\Entities\CrmThread::where('buyer_business_id', $business->id)
|
||||
$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}%");
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
namespace App\Http\Controllers\Buyer\Crm;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Crm\CrmChannelMessage;
|
||||
use App\Models\Crm\CrmThread;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Modules\Crm\Entities\CrmMessage;
|
||||
use Modules\Crm\Entities\CrmThread;
|
||||
|
||||
class MessageController extends Controller
|
||||
{
|
||||
@@ -64,7 +64,7 @@ class MessageController extends Controller
|
||||
return back()->with('success', 'Message sent.');
|
||||
}
|
||||
|
||||
public function destroy(CrmThread $thread, CrmMessage $message)
|
||||
public function destroy(CrmThread $thread, CrmChannelMessage $message)
|
||||
{
|
||||
$business = Auth::user()->business;
|
||||
$user = Auth::user();
|
||||
@@ -88,7 +88,7 @@ class MessageController extends Controller
|
||||
return back()->with('success', 'Message deleted.');
|
||||
}
|
||||
|
||||
public function react(Request $request, CrmThread $thread, CrmMessage $message)
|
||||
public function react(Request $request, CrmThread $thread, CrmChannelMessage $message)
|
||||
{
|
||||
$business = Auth::user()->business;
|
||||
$user = Auth::user();
|
||||
|
||||
@@ -108,7 +108,7 @@ class OrderController extends Controller
|
||||
$deliveryEvents = BuyerDeliveryEvent::getTimelineForOrder($order->id);
|
||||
|
||||
// Get related thread if exists
|
||||
$thread = \Modules\Crm\Entities\CrmThread::where('order_id', $order->id)
|
||||
$thread = \App\Models\Crm\CrmThread::where('order_id', $order->id)
|
||||
->where('buyer_business_id', $business->id)
|
||||
->first();
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\Buyer\BuyerQuoteApproval;
|
||||
use App\Models\Buyer\BuyerSavedFilter;
|
||||
use App\Models\Buyer\BuyerTeamMember;
|
||||
use App\Models\Crm\CrmQuote;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Modules\Crm\Entities\CrmQuote;
|
||||
|
||||
class QuoteController extends Controller
|
||||
{
|
||||
|
||||
@@ -32,15 +32,15 @@ class CrmCalendarController extends Controller
|
||||
$startDate = $request->input('start', now()->startOfMonth());
|
||||
$endDate = $request->input('end', now()->endOfMonth());
|
||||
|
||||
$events = CrmSyncedEvent::whereIn('connection_id', $connections->pluck('id'))
|
||||
->whereBetween('start_time', [$startDate, $endDate])
|
||||
$events = CrmSyncedEvent::whereIn('calendar_connection_id', $connections->pluck('id'))
|
||||
->whereBetween('start_at', [$startDate, $endDate])
|
||||
->get()
|
||||
->map(fn ($e) => [
|
||||
'id' => $e->id,
|
||||
'title' => $e->title,
|
||||
'start' => $e->start_time->toIso8601String(),
|
||||
'end' => $e->end_time->toIso8601String(),
|
||||
'allDay' => $e->is_all_day,
|
||||
'start' => $e->start_at->toIso8601String(),
|
||||
'end' => $e->end_at?->toIso8601String(),
|
||||
'allDay' => $e->all_day,
|
||||
'color' => $e->connection->provider === 'google' ? '#4285f4' : '#0078d4',
|
||||
'extendedProps' => [
|
||||
'location' => $e->location,
|
||||
@@ -50,23 +50,23 @@ class CrmCalendarController extends Controller
|
||||
]);
|
||||
|
||||
// Get meeting bookings
|
||||
$bookings = \Modules\Crm\Entities\CrmMeetingBooking::whereHas('meetingLink', function ($q) use ($business, $user) {
|
||||
$bookings = \App\Models\Crm\CrmMeetingBooking::whereHas('meetingLink', function ($q) use ($business, $user) {
|
||||
$q->where('business_id', $business->id)
|
||||
->where('user_id', $user->id);
|
||||
})
|
||||
->whereBetween('start_time', [$startDate, $endDate])
|
||||
->whereBetween('start_at', [$startDate, $endDate])
|
||||
->with(['meetingLink', 'contact'])
|
||||
->get()
|
||||
->map(fn ($b) => [
|
||||
'id' => 'booking_'.$b->id,
|
||||
'title' => $b->meetingLink->name.' - '.$b->guest_name,
|
||||
'start' => $b->start_time->toIso8601String(),
|
||||
'end' => $b->end_time->toIso8601String(),
|
||||
'title' => $b->meetingLink->name.' - '.$b->booker_name,
|
||||
'start' => $b->start_at->toIso8601String(),
|
||||
'end' => $b->end_at->toIso8601String(),
|
||||
'color' => '#10b981',
|
||||
'extendedProps' => [
|
||||
'type' => 'booking',
|
||||
'contact_id' => $b->contact_id,
|
||||
'guest_email' => $b->guest_email,
|
||||
'booker_email' => $b->booker_email,
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -253,15 +253,15 @@ class CrmCalendarController extends Controller
|
||||
->where('user_id', $user->id)
|
||||
->pluck('id');
|
||||
|
||||
$events = CrmSyncedEvent::whereIn('connection_id', $connections)
|
||||
->whereBetween('start_time', [$validated['start'], $validated['end']])
|
||||
$events = CrmSyncedEvent::whereIn('calendar_connection_id', $connections)
|
||||
->whereBetween('start_at', [$validated['start'], $validated['end']])
|
||||
->get()
|
||||
->map(fn ($e) => [
|
||||
'id' => $e->id,
|
||||
'title' => $e->title,
|
||||
'start' => $e->start_time->toIso8601String(),
|
||||
'end' => $e->end_time->toIso8601String(),
|
||||
'allDay' => $e->is_all_day,
|
||||
'start' => $e->start_at->toIso8601String(),
|
||||
'end' => $e->end_at?->toIso8601String(),
|
||||
'allDay' => $e->all_day,
|
||||
]);
|
||||
|
||||
return response()->json($events);
|
||||
|
||||
@@ -16,13 +16,17 @@ class TaskController extends Controller
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$tasksQuery = CrmTask::where('business_id', $business->id)
|
||||
->with(['assignee', 'creator', 'related'])
|
||||
->orderBy('due_date');
|
||||
$tasksQuery = CrmTask::where('seller_business_id', $business->id)
|
||||
->with(['assignee', 'creator', 'contact', 'business'])
|
||||
->orderBy('due_at');
|
||||
|
||||
// Filter by status
|
||||
// Filter by status (completed vs incomplete)
|
||||
if ($request->filled('status')) {
|
||||
$tasksQuery->where('status', $request->status);
|
||||
if ($request->status === 'completed') {
|
||||
$tasksQuery->whereNotNull('completed_at');
|
||||
} elseif ($request->status === 'pending') {
|
||||
$tasksQuery->whereNull('completed_at');
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by assignee
|
||||
@@ -39,17 +43,17 @@ class TaskController extends Controller
|
||||
|
||||
// Get stats
|
||||
$stats = [
|
||||
'my_tasks' => CrmTask::where('business_id', $business->id)
|
||||
'my_tasks' => CrmTask::where('seller_business_id', $business->id)
|
||||
->where('assigned_to', $user->id)
|
||||
->where('status', '!=', 'completed')
|
||||
->whereNull('completed_at')
|
||||
->count(),
|
||||
'overdue' => CrmTask::where('business_id', $business->id)
|
||||
->where('status', '!=', 'completed')
|
||||
->where('due_date', '<', now())
|
||||
'overdue' => CrmTask::where('seller_business_id', $business->id)
|
||||
->whereNull('completed_at')
|
||||
->where('due_at', '<', now())
|
||||
->count(),
|
||||
'due_today' => CrmTask::where('business_id', $business->id)
|
||||
->where('status', '!=', 'completed')
|
||||
->whereDate('due_date', today())
|
||||
'due_today' => CrmTask::where('seller_business_id', $business->id)
|
||||
->whereNull('completed_at')
|
||||
->whereDate('due_at', today())
|
||||
->count(),
|
||||
];
|
||||
|
||||
@@ -71,19 +75,26 @@ class TaskController extends Controller
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'type' => 'required|in:call,email,meeting,task,follow_up',
|
||||
'priority' => 'required|in:low,medium,high,urgent',
|
||||
'due_date' => 'required|date',
|
||||
'details' => 'nullable|string',
|
||||
'type' => 'required|in:call,email,meeting,follow_up,demo,other',
|
||||
'priority' => 'required|in:low,normal,high,urgent',
|
||||
'due_at' => 'required|date',
|
||||
'assigned_to' => 'nullable|exists:users,id',
|
||||
'contact_id' => 'nullable|exists:contacts,id',
|
||||
'business_id' => 'nullable|exists:businesses,id',
|
||||
]);
|
||||
|
||||
$task = CrmTask::create([
|
||||
...$validated,
|
||||
'business_id' => $business->id,
|
||||
'title' => $validated['title'],
|
||||
'details' => $validated['details'] ?? null,
|
||||
'type' => $validated['type'],
|
||||
'priority' => $validated['priority'],
|
||||
'due_at' => $validated['due_at'],
|
||||
'contact_id' => $validated['contact_id'] ?? null,
|
||||
'business_id' => $validated['business_id'] ?? null,
|
||||
'seller_business_id' => $business->id,
|
||||
'created_by' => $request->user()->id,
|
||||
'assigned_to' => $validated['assigned_to'] ?? $request->user()->id,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
@@ -96,7 +107,7 @@ class TaskController extends Controller
|
||||
*/
|
||||
public function show(Request $request, Business $business, CrmTask $task)
|
||||
{
|
||||
$task->load(['assignee', 'creator', 'related']);
|
||||
$task->load(['assignee', 'creator', 'contact', 'business', 'opportunity', 'order']);
|
||||
|
||||
return view('seller.crm.tasks.show', compact('business', 'task'));
|
||||
}
|
||||
@@ -108,11 +119,10 @@ class TaskController extends Controller
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'sometimes|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'type' => 'sometimes|in:call,email,meeting,task,follow_up',
|
||||
'priority' => 'sometimes|in:low,medium,high,urgent',
|
||||
'status' => 'sometimes|in:pending,in_progress,completed,cancelled',
|
||||
'due_date' => 'sometimes|date',
|
||||
'details' => 'nullable|string',
|
||||
'type' => 'sometimes|in:call,email,meeting,follow_up,demo,other',
|
||||
'priority' => 'sometimes|in:low,normal,high,urgent',
|
||||
'due_at' => 'sometimes|date',
|
||||
'assigned_to' => 'nullable|exists:users,id',
|
||||
]);
|
||||
|
||||
@@ -140,10 +150,7 @@ class TaskController extends Controller
|
||||
*/
|
||||
public function complete(Request $request, Business $business, CrmTask $task)
|
||||
{
|
||||
$task->update([
|
||||
'status' => 'completed',
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
$task->markComplete($request->user());
|
||||
|
||||
return redirect()
|
||||
->back()
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
namespace App\Models\Buyer;
|
||||
|
||||
use App\Models\Business;
|
||||
use App\Models\Crm\CrmInvoice;
|
||||
use App\Models\Order;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Modules\Crm\Entities\CrmInvoice;
|
||||
|
||||
class BuyerInvoiceRecord extends Model
|
||||
{
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
namespace App\Models\Buyer;
|
||||
|
||||
use App\Models\Business;
|
||||
use App\Models\Crm\CrmQuote;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Modules\Crm\Entities\CrmQuote;
|
||||
|
||||
class BuyerQuoteApproval extends Model
|
||||
{
|
||||
|
||||
@@ -315,7 +315,7 @@ PROMPT;
|
||||
public function recommendProducts(int $businessId, int $accountId): array
|
||||
{
|
||||
// Get account's purchase history and interests
|
||||
$interests = \Modules\Crm\Entities\CrmProductInterest::forBusiness($businessId)
|
||||
$interests = \App\Models\Crm\CrmProductInterest::forBusiness($businessId)
|
||||
->forAccount($accountId)
|
||||
->highInterest(20)
|
||||
->with('product.category')
|
||||
|
||||
@@ -474,7 +474,7 @@ class CrmAutomationService
|
||||
|
||||
protected function findLeastLoadedAgent(int $businessId): ?int
|
||||
{
|
||||
return \Modules\Crm\Entities\CrmTeamRole::forBusiness($businessId)
|
||||
return \App\Models\Crm\CrmTeamRole::forBusiness($businessId)
|
||||
->assignable()
|
||||
->get()
|
||||
->filter(fn ($role) => $role->canTakeMoreThreads())
|
||||
|
||||
@@ -47,7 +47,7 @@ class PermissionService
|
||||
return false;
|
||||
}
|
||||
|
||||
$userPermissions = $businessUser->pivot->permissions ?? [];
|
||||
$userPermissions = $this->normalizePermissions($businessUser->pivot->permissions);
|
||||
|
||||
// Check permission (supports wildcards)
|
||||
return $this->hasPermissionInList($permission, $userPermissions);
|
||||
@@ -102,7 +102,7 @@ class PermissionService
|
||||
return false;
|
||||
}
|
||||
|
||||
$currentPermissions = $businessUser->pivot->permissions ?? [];
|
||||
$currentPermissions = $this->normalizePermissions($businessUser->pivot->permissions);
|
||||
$newPermissions = array_unique(array_merge($currentPermissions, $permissions));
|
||||
|
||||
// Update permissions
|
||||
@@ -147,7 +147,7 @@ class PermissionService
|
||||
return false;
|
||||
}
|
||||
|
||||
$currentPermissions = $businessUser->pivot->permissions ?? [];
|
||||
$currentPermissions = $this->normalizePermissions($businessUser->pivot->permissions);
|
||||
$newPermissions = array_diff($currentPermissions, $permissions);
|
||||
|
||||
// Update permissions
|
||||
@@ -197,7 +197,7 @@ class PermissionService
|
||||
return false;
|
||||
}
|
||||
|
||||
$currentPermissions = $businessUser->pivot->permissions ?? [];
|
||||
$currentPermissions = $this->normalizePermissions($businessUser->pivot->permissions);
|
||||
$currentRoleTemplate = $businessUser->pivot->role_template;
|
||||
|
||||
// Update permissions and role template
|
||||
@@ -261,7 +261,7 @@ class PermissionService
|
||||
if ($merge) {
|
||||
// Merge with existing permissions
|
||||
$businessUser = $user->businesses()->where('businesses.id', $business->id)->first();
|
||||
$currentPermissions = $businessUser?->pivot->permissions ?? [];
|
||||
$currentPermissions = $this->normalizePermissions($businessUser?->pivot->permissions);
|
||||
$finalPermissions = array_unique(array_merge($currentPermissions, $expandedPermissions));
|
||||
} else {
|
||||
// Replace existing permissions
|
||||
@@ -425,6 +425,22 @@ class PermissionService
|
||||
Cache::forget("permissions.{$userId}.{$businessId}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize permissions to array (handles JSON string from database)
|
||||
*/
|
||||
protected function normalizePermissions(mixed $permissions): array
|
||||
{
|
||||
if (is_array($permissions)) {
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
if (is_string($permissions)) {
|
||||
return json_decode($permissions, true) ?? [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's permissions with caching
|
||||
*/
|
||||
@@ -443,7 +459,7 @@ class PermissionService
|
||||
->where('businesses.id', $business->id)
|
||||
->first();
|
||||
|
||||
return $businessUser?->pivot->permissions ?? [];
|
||||
return $this->normalizePermissions($businessUser?->pivot->permissions);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* CRM Pipelines - Configurable deal stages for CRM Premium
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('crm_pipelines')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('crm_pipelines', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('business_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->json('stages')->nullable(); // Array of stage objects with name, probability, color
|
||||
$table->boolean('is_default')->default(false);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['business_id', 'is_active']);
|
||||
$table->index(['business_id', 'is_default']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('crm_pipelines');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* CRM Calendar Integration Tables - Premium calendar sync features
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// Calendar connections (OAuth tokens for Google/Outlook)
|
||||
if (! Schema::hasTable('crm_calendar_connections')) {
|
||||
Schema::create('crm_calendar_connections', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('business_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('provider'); // google, outlook
|
||||
$table->string('provider_user_id')->nullable();
|
||||
$table->string('email')->nullable();
|
||||
$table->text('access_token')->nullable();
|
||||
$table->text('refresh_token')->nullable();
|
||||
$table->timestamp('token_expires_at')->nullable();
|
||||
$table->json('scopes')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->boolean('sync_enabled')->default(true);
|
||||
$table->string('sync_direction')->default('both'); // inbound, outbound, both
|
||||
$table->string('default_calendar_id')->nullable();
|
||||
$table->timestamp('last_sync_at')->nullable();
|
||||
$table->string('sync_status')->default('idle'); // idle, syncing, error
|
||||
$table->text('sync_error')->nullable();
|
||||
$table->text('sync_token')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['business_id', 'user_id', 'provider']);
|
||||
$table->index(['business_id', 'is_active']);
|
||||
});
|
||||
}
|
||||
|
||||
// Synced events from external calendars
|
||||
if (! Schema::hasTable('crm_synced_events')) {
|
||||
Schema::create('crm_synced_events', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('calendar_connection_id')->constrained('crm_calendar_connections')->cascadeOnDelete();
|
||||
$table->foreignId('business_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('calendar_event_id')->nullable()->constrained('calendar_events')->nullOnDelete();
|
||||
$table->string('external_id');
|
||||
$table->string('external_calendar_id')->nullable();
|
||||
$table->string('external_link')->nullable();
|
||||
$table->string('title');
|
||||
$table->text('description')->nullable();
|
||||
$table->string('location')->nullable();
|
||||
$table->timestamp('start_at');
|
||||
$table->timestamp('end_at')->nullable();
|
||||
$table->boolean('all_day')->default(false);
|
||||
$table->string('timezone')->nullable();
|
||||
$table->boolean('is_recurring')->default(false);
|
||||
$table->string('recurrence_rule')->nullable();
|
||||
$table->string('recurring_event_id')->nullable();
|
||||
$table->string('status')->default('confirmed'); // confirmed, tentative, cancelled
|
||||
$table->json('attendees')->nullable();
|
||||
$table->string('etag')->nullable();
|
||||
$table->timestamp('external_updated_at')->nullable();
|
||||
$table->timestamp('synced_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['calendar_connection_id', 'external_id']);
|
||||
$table->index(['user_id', 'start_at']);
|
||||
$table->index(['business_id', 'start_at']);
|
||||
});
|
||||
}
|
||||
|
||||
// Meeting links (Calendly-style scheduling)
|
||||
if (! Schema::hasTable('crm_meeting_links')) {
|
||||
Schema::create('crm_meeting_links', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('business_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->text('description')->nullable();
|
||||
$table->json('duration_options')->nullable(); // [15, 30, 60]
|
||||
$table->integer('default_duration')->default(30);
|
||||
$table->json('availability')->nullable(); // Weekly schedule
|
||||
$table->string('timezone')->default('America/Phoenix');
|
||||
$table->integer('buffer_before')->default(0); // minutes
|
||||
$table->integer('buffer_after')->default(0);
|
||||
$table->integer('min_notice')->default(60); // minutes
|
||||
$table->integer('max_days_ahead')->default(60);
|
||||
$table->foreignId('calendar_connection_id')->nullable()->constrained('crm_calendar_connections')->nullOnDelete();
|
||||
$table->string('location_type')->default('google_meet'); // google_meet, zoom, phone, in_person, custom
|
||||
$table->string('custom_location')->nullable();
|
||||
$table->string('zoom_meeting_id')->nullable();
|
||||
$table->json('questions')->nullable(); // Custom form questions
|
||||
$table->text('confirmation_message')->nullable();
|
||||
$table->boolean('requires_confirmation')->default(false);
|
||||
$table->unsignedInteger('bookings_count')->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['business_id', 'is_active']);
|
||||
$table->index(['user_id', 'is_active']);
|
||||
});
|
||||
}
|
||||
|
||||
// Meeting bookings
|
||||
if (! Schema::hasTable('crm_meeting_bookings')) {
|
||||
Schema::create('crm_meeting_bookings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('meeting_link_id')->constrained('crm_meeting_links')->cascadeOnDelete();
|
||||
$table->foreignId('business_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete(); // Host
|
||||
$table->string('booker_name');
|
||||
$table->string('booker_email');
|
||||
$table->string('booker_phone')->nullable();
|
||||
$table->foreignId('contact_id')->nullable()->constrained('contacts')->nullOnDelete();
|
||||
$table->foreignId('account_id')->nullable()->constrained('businesses')->nullOnDelete();
|
||||
$table->timestamp('start_at');
|
||||
$table->timestamp('end_at');
|
||||
$table->integer('duration'); // minutes
|
||||
$table->string('timezone')->nullable();
|
||||
$table->string('location_type')->nullable();
|
||||
$table->string('location')->nullable();
|
||||
$table->string('meeting_id')->nullable(); // External meeting ID (Zoom, etc.)
|
||||
$table->json('answers')->nullable(); // Answers to custom questions
|
||||
$table->string('status')->default('scheduled'); // scheduled, confirmed, cancelled, completed, no_show
|
||||
$table->foreignId('calendar_event_id')->nullable()->constrained('calendar_events')->nullOnDelete();
|
||||
$table->string('external_event_id')->nullable();
|
||||
$table->boolean('reminder_sent')->default(false);
|
||||
$table->timestamp('reminder_sent_at')->nullable();
|
||||
$table->timestamp('cancelled_at')->nullable();
|
||||
$table->string('cancelled_by')->nullable(); // 'host' or 'booker' or email
|
||||
$table->text('cancellation_reason')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['meeting_link_id', 'status']);
|
||||
$table->index(['user_id', 'start_at']);
|
||||
$table->index(['business_id', 'start_at']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('crm_meeting_bookings');
|
||||
Schema::dropIfExists('crm_meeting_links');
|
||||
Schema::dropIfExists('crm_synced_events');
|
||||
Schema::dropIfExists('crm_calendar_connections');
|
||||
}
|
||||
};
|
||||
@@ -9,7 +9,7 @@
|
||||
sortBy: 'name',
|
||||
brandSegment: 'all',
|
||||
selectedBrand: null,
|
||||
brands: @js($brands->map(function($brand) use ($business) {
|
||||
brands: @js($brands->filter(fn($brand) => $brand->hashid)->map(function($brand) use ($business) {
|
||||
// Enhanced with relationship + performance data
|
||||
$isHouseBrand = rand(0, 100) > 60;
|
||||
$tier = $isHouseBrand ? 'house' : (['premium', 'partner', 'white-label'][rand(0, 2)]);
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
@forelse($brandOverview as $brand)
|
||||
@forelse($brandOverview->filter(fn($b) => $b['brand']?->hashid) as $brand)
|
||||
<div class="card bg-base-100 border border-base-200 shadow-sm">
|
||||
<div class="card-body p-5">
|
||||
{{-- Brand Header --}}
|
||||
@@ -406,7 +406,7 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@forelse($myBrandPerformance as $brandPerf)
|
||||
@forelse($myBrandPerformance->filter(fn($b) => $b['brand']?->hashid) as $brandPerf)
|
||||
<div class="card bg-base-100 border border-base-200 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex items-start gap-3 mb-3">
|
||||
|
||||
Reference in New Issue
Block a user