Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cda896ebe0 | ||
|
|
c439194c18 | ||
|
|
bf7c0d7d61 |
6 changed files with 211 additions and 206 deletions
|
|
@ -10,93 +10,56 @@ return new class extends Migration
|
|||
{
|
||||
/**
|
||||
* Agentic module tables - AI agents, tasks, sessions.
|
||||
*
|
||||
* Guarded with hasTable() so this migration is idempotent and
|
||||
* can coexist with the consolidated app-level migration.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
// 1. Agent API Keys
|
||||
Schema::create('agent_api_keys', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->uuid('uuid')->unique();
|
||||
$table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('key_hash', 64)->unique();
|
||||
$table->string('key_prefix', 12);
|
||||
$table->json('allowed_agents')->nullable();
|
||||
$table->json('rate_limits')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->unsignedBigInteger('usage_count')->default(0);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
if (! Schema::hasTable('agent_api_keys')) {
|
||||
Schema::create('agent_api_keys', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('workspace_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('key');
|
||||
$table->json('permissions')->nullable();
|
||||
$table->unsignedInteger('rate_limit')->nullable();
|
||||
$table->unsignedBigInteger('call_count')->default(0);
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamp('revoked_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['workspace_id', 'is_active']);
|
||||
$table->index('key_prefix');
|
||||
});
|
||||
$table->index('workspace_id');
|
||||
$table->index('key');
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Agent Tasks
|
||||
Schema::create('agent_tasks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->uuid('uuid')->unique();
|
||||
$table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->foreignId('api_key_id')->nullable()->constrained('agent_api_keys')->nullOnDelete();
|
||||
$table->string('agent_type');
|
||||
$table->string('status', 32)->default('pending');
|
||||
$table->text('prompt');
|
||||
$table->json('context')->nullable();
|
||||
$table->json('result')->nullable();
|
||||
$table->json('tool_calls')->nullable();
|
||||
$table->unsignedInteger('input_tokens')->default(0);
|
||||
$table->unsignedInteger('output_tokens')->default(0);
|
||||
$table->decimal('cost', 10, 6)->default(0);
|
||||
$table->unsignedInteger('duration_ms')->nullable();
|
||||
$table->text('error_message')->nullable();
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->timestamps();
|
||||
if (! Schema::hasTable('agent_sessions')) {
|
||||
Schema::create('agent_sessions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('workspace_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->foreignId('agent_api_key_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('session_id')->unique();
|
||||
$table->string('agent_type')->nullable();
|
||||
$table->string('status')->default('active');
|
||||
$table->json('context_summary')->nullable();
|
||||
$table->json('work_log')->nullable();
|
||||
$table->json('artifacts')->nullable();
|
||||
$table->json('handoff_notes')->nullable();
|
||||
$table->text('final_summary')->nullable();
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('last_active_at')->nullable();
|
||||
$table->timestamp('ended_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['workspace_id', 'status']);
|
||||
$table->index(['agent_type', 'status']);
|
||||
$table->index('created_at');
|
||||
});
|
||||
|
||||
// 3. Agent Sessions
|
||||
Schema::create('agent_sessions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->uuid('uuid')->unique();
|
||||
$table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('agent_type');
|
||||
$table->string('status', 32)->default('active');
|
||||
$table->json('context')->nullable();
|
||||
$table->json('memory')->nullable();
|
||||
$table->unsignedInteger('message_count')->default(0);
|
||||
$table->unsignedInteger('total_tokens')->default(0);
|
||||
$table->decimal('total_cost', 10, 6)->default(0);
|
||||
$table->timestamp('last_activity_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['workspace_id', 'status']);
|
||||
$table->index(['agent_type', 'status']);
|
||||
$table->index('last_activity_at');
|
||||
});
|
||||
|
||||
// 4. Agent Messages
|
||||
Schema::create('agent_messages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('session_id')->constrained('agent_sessions')->cascadeOnDelete();
|
||||
$table->string('role', 32);
|
||||
$table->longText('content');
|
||||
$table->json('tool_calls')->nullable();
|
||||
$table->json('tool_results')->nullable();
|
||||
$table->unsignedInteger('tokens')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['session_id', 'created_at']);
|
||||
});
|
||||
$table->index('workspace_id');
|
||||
$table->index('status');
|
||||
$table->index('agent_type');
|
||||
});
|
||||
}
|
||||
|
||||
Schema::enableForeignKeyConstraints();
|
||||
}
|
||||
|
|
@ -104,9 +67,7 @@ return new class extends Migration
|
|||
public function down(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
Schema::dropIfExists('agent_messages');
|
||||
Schema::dropIfExists('agent_sessions');
|
||||
Schema::dropIfExists('agent_tasks');
|
||||
Schema::dropIfExists('agent_api_keys');
|
||||
Schema::enableForeignKeyConstraints();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,17 +13,43 @@ return new class extends Migration
|
|||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('agent_api_keys')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('agent_api_keys', function (Blueprint $table) {
|
||||
$table->boolean('ip_restriction_enabled')->default(false)->after('rate_limits');
|
||||
$table->json('ip_whitelist')->nullable()->after('ip_restriction_enabled');
|
||||
$table->string('last_used_ip', 45)->nullable()->after('last_used_at');
|
||||
if (! Schema::hasColumn('agent_api_keys', 'ip_restriction_enabled')) {
|
||||
$table->boolean('ip_restriction_enabled')->default(false);
|
||||
}
|
||||
if (! Schema::hasColumn('agent_api_keys', 'ip_whitelist')) {
|
||||
$table->json('ip_whitelist')->nullable();
|
||||
}
|
||||
if (! Schema::hasColumn('agent_api_keys', 'last_used_ip')) {
|
||||
$table->string('last_used_ip', 45)->nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('agent_api_keys')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('agent_api_keys', function (Blueprint $table) {
|
||||
$table->dropColumn(['ip_restriction_enabled', 'ip_whitelist', 'last_used_ip']);
|
||||
$cols = [];
|
||||
if (Schema::hasColumn('agent_api_keys', 'ip_restriction_enabled')) {
|
||||
$cols[] = 'ip_restriction_enabled';
|
||||
}
|
||||
if (Schema::hasColumn('agent_api_keys', 'ip_whitelist')) {
|
||||
$cols[] = 'ip_whitelist';
|
||||
}
|
||||
if (Schema::hasColumn('agent_api_keys', 'last_used_ip')) {
|
||||
$cols[] = 'last_used_ip';
|
||||
}
|
||||
if ($cols) {
|
||||
$table->dropColumn($cols);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,82 +11,76 @@ return new class extends Migration
|
|||
/**
|
||||
* Create agent plans, phases, and workspace states tables.
|
||||
*
|
||||
* These tables support the structured work plan system that enables
|
||||
* multi-agent handoff and context recovery across sessions.
|
||||
* Guarded with hasTable() so this migration is idempotent and
|
||||
* can coexist with the consolidated app-level migration.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
// 1. Agent Plans - structured work plans with phases
|
||||
Schema::create('agent_plans', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('workspace_id')->constrained('workspaces')->cascadeOnDelete();
|
||||
$table->string('slug')->unique();
|
||||
$table->string('title');
|
||||
$table->text('description')->nullable();
|
||||
$table->longText('context')->nullable();
|
||||
$table->json('phases')->nullable(); // Deprecated: use agent_phases table
|
||||
$table->string('status', 32)->default('draft');
|
||||
$table->string('current_phase')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->string('source_file')->nullable();
|
||||
$table->timestamps();
|
||||
if (! Schema::hasTable('agent_plans')) {
|
||||
Schema::create('agent_plans', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('workspace_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('slug')->unique();
|
||||
$table->string('title');
|
||||
$table->text('description')->nullable();
|
||||
$table->longText('context')->nullable();
|
||||
$table->json('phases')->nullable();
|
||||
$table->string('status', 32)->default('draft');
|
||||
$table->string('current_phase')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->string('source_file')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['workspace_id', 'status']);
|
||||
$table->index('slug');
|
||||
});
|
||||
$table->index(['workspace_id', 'status']);
|
||||
$table->index('slug');
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Agent Phases - individual phases within a plan
|
||||
Schema::create('agent_phases', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('agent_plan_id')->constrained('agent_plans')->cascadeOnDelete();
|
||||
$table->unsignedInteger('order')->default(0);
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->json('tasks')->nullable();
|
||||
$table->json('dependencies')->nullable();
|
||||
$table->string('status', 32)->default('pending');
|
||||
$table->json('completion_criteria')->nullable();
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
if (! Schema::hasTable('agent_phases')) {
|
||||
Schema::create('agent_phases', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('agent_plan_id')->constrained('agent_plans')->cascadeOnDelete();
|
||||
$table->unsignedInteger('order')->default(0);
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->json('tasks')->nullable();
|
||||
$table->json('dependencies')->nullable();
|
||||
$table->string('status', 32)->default('pending');
|
||||
$table->json('completion_criteria')->nullable();
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['agent_plan_id', 'order']);
|
||||
$table->index(['agent_plan_id', 'status']);
|
||||
});
|
||||
$table->index(['agent_plan_id', 'order']);
|
||||
$table->index(['agent_plan_id', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Agent Workspace States - shared context between sessions
|
||||
Schema::create('agent_workspace_states', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('agent_plan_id')->constrained('agent_plans')->cascadeOnDelete();
|
||||
$table->string('key');
|
||||
$table->json('value')->nullable();
|
||||
$table->string('type', 32)->default('json');
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamps();
|
||||
if (! Schema::hasTable('agent_workspace_states')) {
|
||||
Schema::create('agent_workspace_states', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('agent_plan_id')->constrained('agent_plans')->cascadeOnDelete();
|
||||
$table->string('key');
|
||||
$table->json('value')->nullable();
|
||||
$table->string('type', 32)->default('json');
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['agent_plan_id', 'key']);
|
||||
$table->index('key');
|
||||
});
|
||||
$table->unique(['agent_plan_id', 'key']);
|
||||
$table->index('key');
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Update agent_sessions to add agent_plan_id foreign key
|
||||
// Note: Only run if agent_sessions table exists (from earlier migration)
|
||||
// Add agent_plan_id to agent_sessions if table exists
|
||||
if (Schema::hasTable('agent_sessions') && ! Schema::hasColumn('agent_sessions', 'agent_plan_id')) {
|
||||
Schema::table('agent_sessions', function (Blueprint $table) {
|
||||
$table->foreignId('agent_plan_id')
|
||||
->nullable()
|
||||
->after('workspace_id')
|
||||
->constrained('agent_plans')
|
||||
->nullOnDelete();
|
||||
$table->json('context_summary')->nullable()->after('context');
|
||||
$table->json('work_log')->nullable()->after('context_summary');
|
||||
$table->json('artifacts')->nullable()->after('work_log');
|
||||
$table->json('handoff_notes')->nullable()->after('artifacts');
|
||||
$table->text('final_summary')->nullable()->after('handoff_notes');
|
||||
$table->timestamp('started_at')->nullable()->after('final_summary');
|
||||
$table->timestamp('ended_at')->nullable()->after('started_at');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -97,20 +91,10 @@ return new class extends Migration
|
|||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
// Remove agent_sessions additions if table exists
|
||||
if (Schema::hasTable('agent_sessions') && Schema::hasColumn('agent_sessions', 'agent_plan_id')) {
|
||||
Schema::table('agent_sessions', function (Blueprint $table) {
|
||||
$table->dropForeign(['agent_plan_id']);
|
||||
$table->dropColumn([
|
||||
'agent_plan_id',
|
||||
'context_summary',
|
||||
'work_log',
|
||||
'artifacts',
|
||||
'handoff_notes',
|
||||
'final_summary',
|
||||
'started_at',
|
||||
'ended_at',
|
||||
]);
|
||||
$table->dropColumn('agent_plan_id');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ class AgentPlan extends Model
|
|||
];
|
||||
|
||||
protected $casts = [
|
||||
'context' => 'array',
|
||||
'phases' => 'array',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -58,14 +58,14 @@ class Boot extends ServiceProvider implements ServiceDefinition
|
|||
/**
|
||||
* Admin menu items for this service.
|
||||
*
|
||||
* Agentic is positioned in the dashboard group (not services)
|
||||
* as it's a cross-cutting AI capability, not a standalone product.
|
||||
* Agentic has its own top-level group right after Dashboard —
|
||||
* this is the primary capability of the platform.
|
||||
*/
|
||||
public function adminMenuItems(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'group' => 'dashboard',
|
||||
'group' => 'agents',
|
||||
'priority' => 5,
|
||||
'entitlement' => 'core.srv.agentic',
|
||||
'item' => fn () => [
|
||||
|
|
|
|||
|
|
@ -28,19 +28,34 @@ class Dashboard extends Component
|
|||
public function stats(): array
|
||||
{
|
||||
return $this->cacheWithLock('admin.agents.dashboard.stats', 60, function () {
|
||||
$activePlans = AgentPlan::active()->count();
|
||||
$totalPlans = AgentPlan::notArchived()->count();
|
||||
$activeSessions = AgentSession::active()->count();
|
||||
$todaySessions = AgentSession::whereDate('started_at', today())->count();
|
||||
try {
|
||||
$activePlans = AgentPlan::active()->count();
|
||||
$totalPlans = AgentPlan::notArchived()->count();
|
||||
} catch (\Throwable) {
|
||||
$activePlans = 0;
|
||||
$totalPlans = 0;
|
||||
}
|
||||
|
||||
// Tool call stats for last 7 days
|
||||
$toolStats = McpToolCallStat::last7Days()
|
||||
->selectRaw('SUM(call_count) as total_calls')
|
||||
->selectRaw('SUM(success_count) as total_success')
|
||||
->first();
|
||||
try {
|
||||
$activeSessions = AgentSession::active()->count();
|
||||
$todaySessions = AgentSession::whereDate('started_at', today())->count();
|
||||
} catch (\Throwable) {
|
||||
$activeSessions = 0;
|
||||
$todaySessions = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
$toolStats = McpToolCallStat::last7Days()
|
||||
->selectRaw('SUM(call_count) as total_calls')
|
||||
->selectRaw('SUM(success_count) as total_success')
|
||||
->first();
|
||||
$totalCalls = $toolStats->total_calls ?? 0;
|
||||
$totalSuccess = $toolStats->total_success ?? 0;
|
||||
} catch (\Throwable) {
|
||||
$totalCalls = 0;
|
||||
$totalSuccess = 0;
|
||||
}
|
||||
|
||||
$totalCalls = $toolStats->total_calls ?? 0;
|
||||
$totalSuccess = $toolStats->total_success ?? 0;
|
||||
$successRate = $totalCalls > 0 ? round(($totalSuccess / $totalCalls) * 100, 1) : 0;
|
||||
|
||||
return [
|
||||
|
|
@ -124,40 +139,46 @@ class Dashboard extends Component
|
|||
return $this->cacheWithLock('admin.agents.dashboard.activity', 30, function () {
|
||||
$activities = [];
|
||||
|
||||
// Recent plan updates
|
||||
$plans = AgentPlan::with('workspace')
|
||||
->latest('updated_at')
|
||||
->take(5)
|
||||
->get();
|
||||
try {
|
||||
$plans = AgentPlan::with('workspace')
|
||||
->latest('updated_at')
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
foreach ($plans as $plan) {
|
||||
$activities[] = [
|
||||
'type' => 'plan',
|
||||
'icon' => 'clipboard-list',
|
||||
'title' => "Plan \"{$plan->title}\"",
|
||||
'description' => "Status: {$plan->status}",
|
||||
'workspace' => $plan->workspace?->name ?? 'Unknown',
|
||||
'time' => $plan->updated_at,
|
||||
'link' => route('hub.agents.plans.show', $plan->slug),
|
||||
];
|
||||
foreach ($plans as $plan) {
|
||||
$activities[] = [
|
||||
'type' => 'plan',
|
||||
'icon' => 'clipboard-list',
|
||||
'title' => "Plan \"{$plan->title}\"",
|
||||
'description' => "Status: {$plan->status}",
|
||||
'workspace' => $plan->workspace?->name ?? 'Unknown',
|
||||
'time' => $plan->updated_at,
|
||||
'link' => route('hub.agents.plans.show', $plan->slug),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Table may not exist yet
|
||||
}
|
||||
|
||||
// Recent sessions
|
||||
$sessions = AgentSession::with(['plan', 'workspace'])
|
||||
->latest('last_active_at')
|
||||
->take(5)
|
||||
->get();
|
||||
try {
|
||||
$sessions = AgentSession::with(['plan', 'workspace'])
|
||||
->latest('last_active_at')
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
$activities[] = [
|
||||
'type' => 'session',
|
||||
'icon' => 'play',
|
||||
'title' => "Session {$session->session_id}",
|
||||
'description' => $session->plan?->title ?? 'No plan',
|
||||
'workspace' => $session->workspace?->name ?? 'Unknown',
|
||||
'time' => $session->last_active_at ?? $session->created_at,
|
||||
'link' => route('hub.agents.sessions.show', $session->id),
|
||||
];
|
||||
foreach ($sessions as $session) {
|
||||
$activities[] = [
|
||||
'type' => 'session',
|
||||
'icon' => 'play',
|
||||
'title' => "Session {$session->session_id}",
|
||||
'description' => $session->plan?->title ?? 'No plan',
|
||||
'workspace' => $session->workspace?->name ?? 'Unknown',
|
||||
'time' => $session->last_active_at ?? $session->created_at,
|
||||
'link' => route('hub.agents.sessions.show', $session->id),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Table may not exist yet
|
||||
}
|
||||
|
||||
// Sort by time descending
|
||||
|
|
@ -171,7 +192,11 @@ class Dashboard extends Component
|
|||
public function topTools(): \Illuminate\Support\Collection
|
||||
{
|
||||
return $this->cacheWithLock('admin.agents.dashboard.toptools', 300, function () {
|
||||
return McpToolCallStat::getTopTools(days: 7, limit: 5);
|
||||
try {
|
||||
return McpToolCallStat::getTopTools(days: 7, limit: 5);
|
||||
} catch (\Throwable) {
|
||||
return collect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +204,11 @@ class Dashboard extends Component
|
|||
public function dailyTrend(): \Illuminate\Support\Collection
|
||||
{
|
||||
return $this->cacheWithLock('admin.agents.dashboard.dailytrend', 300, function () {
|
||||
return McpToolCallStat::getDailyTrend(days: 7);
|
||||
try {
|
||||
return McpToolCallStat::getDailyTrend(days: 7);
|
||||
} catch (\Throwable) {
|
||||
return collect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -187,11 +216,15 @@ class Dashboard extends Component
|
|||
public function blockedPlans(): int
|
||||
{
|
||||
return (int) $this->cacheWithLock('admin.agents.dashboard.blocked', 60, function () {
|
||||
return AgentPlan::active()
|
||||
->whereHas('agentPhases', function ($query) {
|
||||
$query->where('status', 'blocked');
|
||||
})
|
||||
->count();
|
||||
try {
|
||||
return AgentPlan::active()
|
||||
->whereHas('agentPhases', function ($query) {
|
||||
$query->where('status', 'blocked');
|
||||
})
|
||||
->count();
|
||||
} catch (\Throwable) {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue