Compare commits
1 Commits
cb495e18e3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
602fe582b0 |
@@ -18,20 +18,12 @@ class DashboardController extends Controller
|
||||
{
|
||||
$stats = $this->statsService->getDashboardStats();
|
||||
$dailyUsage = $this->statsService->getDailyUsageChart(30);
|
||||
$topUsers = $this->statsService->getTopUsers(5);
|
||||
$providerStats = $this->statsService->getUsageByProvider(30);
|
||||
$modelStats = $this->statsService->getUsageByModel(30);
|
||||
$costTrends = $this->statsService->getCostTrends(30);
|
||||
$errorStats = $this->statsService->getErrorStats(30);
|
||||
|
||||
return view('dashboard', compact(
|
||||
'stats',
|
||||
'dailyUsage',
|
||||
'topUsers',
|
||||
'providerStats',
|
||||
'modelStats',
|
||||
'costTrends',
|
||||
'errorStats'
|
||||
'providerStats'
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,11 @@ class GatewayUser extends Model implements Authenticatable
|
||||
return $this->hasMany(UsageLog::class, 'gateway_user_id', 'user_id');
|
||||
}
|
||||
|
||||
public function llmRequests()
|
||||
{
|
||||
return $this->hasMany(LlmRequest::class, 'gateway_user_id', 'user_id');
|
||||
}
|
||||
|
||||
// Scopes
|
||||
public function scopeActive($query)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
class LlmRequest extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'gateway_user_id',
|
||||
'provider',
|
||||
'model',
|
||||
'request_payload',
|
||||
@@ -41,9 +41,9 @@ class LlmRequest extends Model
|
||||
'http_status' => 'integer',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
public function gatewayUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
return $this->belongsTo(GatewayUser::class, 'gateway_user_id', 'user_id');
|
||||
}
|
||||
|
||||
public function isSuccess(): bool
|
||||
|
||||
@@ -69,12 +69,4 @@ class User extends Authenticatable
|
||||
{
|
||||
return $this->hasMany(UserProviderCredential::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's LLM requests
|
||||
*/
|
||||
public function llmRequests()
|
||||
{
|
||||
return $this->hasMany(LlmRequest::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\LlmRequest;
|
||||
use App\Models\User;
|
||||
use App\Models\GatewayUser;
|
||||
use App\Models\UserProviderCredential;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@@ -15,8 +15,9 @@ class StatisticsService
|
||||
public function getDashboardStats(): array
|
||||
{
|
||||
return [
|
||||
'total_users' => User::count(),
|
||||
'active_credentials' => UserProviderCredential::where('is_active', true)->count(),
|
||||
'total_gateway_users' => GatewayUser::count(),
|
||||
'active_gateway_users' => GatewayUser::where('blocked', false)->count(),
|
||||
'blocked_gateway_users' => GatewayUser::where('blocked', true)->count(),
|
||||
'total_requests_today' => LlmRequest::whereDate('created_at', today())->count(),
|
||||
'total_spend_today' => LlmRequest::whereDate('created_at', today())->sum('total_cost') ?? 0,
|
||||
'total_tokens_today' => LlmRequest::whereDate('created_at', today())->sum('total_tokens') ?? 0,
|
||||
@@ -73,11 +74,11 @@ class StatisticsService
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top users by spend
|
||||
* Get top gateway users by spend
|
||||
*/
|
||||
public function getTopUsers(int $limit = 10)
|
||||
{
|
||||
return User::select('users.*')
|
||||
return GatewayUser::select('gateway_users.*')
|
||||
->withCount('llmRequests')
|
||||
->withSum('llmRequests as total_cost', 'total_cost')
|
||||
->withSum('llmRequests as total_tokens', 'total_tokens')
|
||||
@@ -91,18 +92,18 @@ class StatisticsService
|
||||
*/
|
||||
public function getRecentActivity(int $limit = 20)
|
||||
{
|
||||
return LlmRequest::with('user')
|
||||
return LlmRequest::with('gatewayUser')
|
||||
->orderByDesc('created_at')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user statistics
|
||||
* Get gateway user statistics
|
||||
*/
|
||||
public function getUserStatistics(int $userId, int $days = 30)
|
||||
public function getGatewayUserStatistics(string $gatewayUserId, int $days = 30)
|
||||
{
|
||||
return LlmRequest::where('user_id', $userId)
|
||||
return LlmRequest::where('gateway_user_id', $gatewayUserId)
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', 'success')
|
||||
->selectRaw('
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Create Budget Template
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900">
|
||||
<form action="{{ route('budgets.store') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<!-- Budget Name (for display purposes) -->
|
||||
<div class="mb-4">
|
||||
<label for="budget_name" class="block text-sm font-medium text-gray-700">Budget Template Name</label>
|
||||
<input type="text" name="budget_name" id="budget_name"
|
||||
value="{{ old('budget_name') }}"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="e.g., Standard Monthly Budget"
|
||||
required>
|
||||
@error('budget_name')
|
||||
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Max Budget -->
|
||||
<div class="mb-4">
|
||||
<label for="max_budget" class="block text-sm font-medium text-gray-700">Maximum Budget ($)</label>
|
||||
<input type="number" name="max_budget" id="max_budget"
|
||||
value="{{ old('max_budget') }}"
|
||||
step="0.01" min="0"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="100.00"
|
||||
required>
|
||||
@error('max_budget')
|
||||
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Budget Type -->
|
||||
<div class="mb-4">
|
||||
<label for="budget_type" class="block text-sm font-medium text-gray-700 mb-2">Budget Duration</label>
|
||||
<select name="budget_type" id="budget_type"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
required>
|
||||
<option value="daily" {{ old('budget_type') == 'daily' ? 'selected' : '' }}>Daily (24 hours)</option>
|
||||
<option value="weekly" {{ old('budget_type') == 'weekly' ? 'selected' : '' }}>Weekly (7 days)</option>
|
||||
<option value="monthly" {{ old('budget_type') == 'monthly' ? 'selected' : '' }}>Monthly (30 days)</option>
|
||||
<option value="custom" {{ old('budget_type') == 'custom' ? 'selected' : '' }}>Custom Duration</option>
|
||||
<option value="unlimited" {{ old('budget_type') == 'unlimited' ? 'selected' : '' }}>Unlimited (No Reset)</option>
|
||||
</select>
|
||||
@error('budget_type')
|
||||
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Custom Duration (shown when custom is selected) -->
|
||||
<div class="mb-4" id="custom_duration_field" style="display: none;">
|
||||
<label for="custom_duration_days" class="block text-sm font-medium text-gray-700">Custom Duration (Days)</label>
|
||||
<input type="number" name="custom_duration_days" id="custom_duration_days"
|
||||
value="{{ old('custom_duration_days') }}"
|
||||
min="1"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="e.g., 14">
|
||||
@error('custom_duration_days')
|
||||
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="mb-6 p-4 bg-blue-50 rounded-lg">
|
||||
<h4 class="text-sm font-medium text-blue-900 mb-2">ℹ️ Budget Template Info</h4>
|
||||
<ul class="text-sm text-blue-700 space-y-1">
|
||||
<li>• Budget templates can be assigned to multiple users</li>
|
||||
<li>• Users will automatically reset when duration expires</li>
|
||||
<li>• "Unlimited" budgets never reset automatically</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<a href="{{ route('budgets.index') }}" class="text-gray-600 hover:text-gray-900">Cancel</a>
|
||||
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Create Budget Template
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Toggle custom duration field
|
||||
document.getElementById('budget_type').addEventListener('change', function() {
|
||||
const customField = document.getElementById('custom_duration_field');
|
||||
if (this.value === 'custom') {
|
||||
customField.style.display = 'block';
|
||||
} else {
|
||||
customField.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger on page load if custom was selected
|
||||
if (document.getElementById('budget_type').value === 'custom') {
|
||||
document.getElementById('custom_duration_field').style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
</x-app-layout>
|
||||
@@ -1,102 +0,0 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Edit Budget Template
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900">
|
||||
<form action="{{ route('budgets.update', $budget->budget_id) }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<!-- Budget ID (read-only) -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Budget ID</label>
|
||||
<input type="text" value="{{ $budget->budget_id }}"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 bg-gray-100 shadow-sm"
|
||||
disabled>
|
||||
</div>
|
||||
|
||||
<!-- Max Budget -->
|
||||
<div class="mb-4">
|
||||
<label for="max_budget" class="block text-sm font-medium text-gray-700">Maximum Budget ($)</label>
|
||||
<input type="number" name="max_budget" id="max_budget"
|
||||
value="{{ old('max_budget', $budget->max_budget) }}"
|
||||
step="0.01" min="0"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
required>
|
||||
@error('max_budget')
|
||||
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Budget Type -->
|
||||
<div class="mb-4">
|
||||
<label for="budget_type" class="block text-sm font-medium text-gray-700 mb-2">Budget Duration</label>
|
||||
<select name="budget_type" id="budget_type"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
required>
|
||||
<option value="daily" {{ old('budget_type', $budgetType) == 'daily' ? 'selected' : '' }}>Daily (24 hours)</option>
|
||||
<option value="weekly" {{ old('budget_type', $budgetType) == 'weekly' ? 'selected' : '' }}>Weekly (7 days)</option>
|
||||
<option value="monthly" {{ old('budget_type', $budgetType) == 'monthly' ? 'selected' : '' }}>Monthly (30 days)</option>
|
||||
<option value="custom" {{ old('budget_type', $budgetType) == 'custom' ? 'selected' : '' }}>Custom Duration</option>
|
||||
<option value="unlimited" {{ old('budget_type', $budgetType) == 'unlimited' ? 'selected' : '' }}>Unlimited (No Reset)</option>
|
||||
</select>
|
||||
@error('budget_type')
|
||||
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Custom Duration -->
|
||||
<div class="mb-4" id="custom_duration_field" style="display: {{ $budgetType == 'custom' ? 'block' : 'none' }};">
|
||||
<label for="custom_duration_days" class="block text-sm font-medium text-gray-700">Custom Duration (Days)</label>
|
||||
<input type="number" name="custom_duration_days" id="custom_duration_days"
|
||||
value="{{ old('custom_duration_days', $budget->budget_duration_sec ? floor($budget->budget_duration_sec / 86400) : '') }}"
|
||||
min="1"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="e.g., 14">
|
||||
@error('custom_duration_days')
|
||||
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Warning Box -->
|
||||
<div class="mb-6 p-4 bg-yellow-50 rounded-lg">
|
||||
<h4 class="text-sm font-medium text-yellow-900 mb-2">⚠️ Warning</h4>
|
||||
<p class="text-sm text-yellow-700">
|
||||
This budget is currently assigned to <strong>{{ $budget->gatewayUsers()->count() }} user(s)</strong>.
|
||||
Changes will affect all assigned users.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<a href="{{ route('budgets.show', $budget->budget_id) }}" class="text-gray-600 hover:text-gray-900">Cancel</a>
|
||||
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Update Budget
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Toggle custom duration field
|
||||
document.getElementById('budget_type').addEventListener('change', function() {
|
||||
const customField = document.getElementById('custom_duration_field');
|
||||
if (this.value === 'custom') {
|
||||
customField.style.display = 'block';
|
||||
} else {
|
||||
customField.style.display = 'none';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
</x-app-layout>
|
||||
@@ -1,98 +0,0 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Budget Templates
|
||||
</h2>
|
||||
<a href="{{ route('budgets.create') }}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Create Budget Template
|
||||
</a>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
@if(session('success'))
|
||||
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900">
|
||||
@if($budgets->count() > 0)
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Budget ID
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Max Budget
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Duration
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Assigned Users
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Created
|
||||
</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($budgets as $budget)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{{ $budget->budget_id }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<span class="font-semibold text-green-600">{{ $budget->max_budget_formatted }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $budget->duration_human }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{{ $budget->gateway_users_count }} users
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $budget->created_at->format('M d, Y') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<a href="{{ route('budgets.show', $budget->budget_id) }}" class="text-blue-600 hover:text-blue-900 mr-3">View</a>
|
||||
<a href="{{ route('budgets.edit', $budget->budget_id) }}" class="text-indigo-600 hover:text-indigo-900 mr-3">Edit</a>
|
||||
<form action="{{ route('budgets.destroy', $budget->budget_id) }}" method="POST" class="inline"
|
||||
onsubmit="return confirm('Are you sure? This budget will be deleted.');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="text-red-600 hover:text-red-900">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="mt-4">
|
||||
{{ $budgets->links() }}
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-center py-8">No budget templates found. Create your first budget template to get started.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -1,164 +0,0 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Budget Details
|
||||
</h2>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('budgets.edit', $budget->budget_id) }}"
|
||||
class="bg-indigo-500 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded">
|
||||
Edit Budget
|
||||
</a>
|
||||
<form action="{{ route('budgets.destroy', $budget->budget_id) }}" method="POST"
|
||||
onsubmit="return confirm('Are you sure? This will delete the budget.');" class="inline">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
@if(session('success'))
|
||||
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Budget Info Card -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">Budget Information</h3>
|
||||
<dl class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Budget ID</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 font-mono">{{ $budget->budget_id }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Maximum Budget</dt>
|
||||
<dd class="mt-1 text-2xl font-bold text-green-600">{{ $budget->max_budget_formatted }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Duration</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">{{ $budget->duration_human }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Assigned Users</dt>
|
||||
<dd class="mt-1 text-2xl font-bold text-blue-600">{{ $budget->gatewayUsers()->count() }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Created</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">{{ $budget->created_at->format('M d, Y H:i') }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Last Updated</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">{{ $budget->updated_at->format('M d, Y H:i') }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assigned Users Table -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">Assigned Users ({{ $budget->gatewayUsers()->count() }})</h3>
|
||||
|
||||
@if($budget->gatewayUsers()->count() > 0)
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">User ID</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Alias</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Current Spend</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Budget Started</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Next Reset</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@foreach($budget->gatewayUsers as $user)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
|
||||
{{ $user->user_id }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ $user->alias ?? '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<span class="font-semibold {{ $user->spend >= $budget->max_budget ? 'text-red-600' : 'text-green-600' }}">
|
||||
{{ $user->spend_formatted }}
|
||||
</span>
|
||||
<span class="text-gray-500">/ {{ $budget->max_budget_formatted }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $user->budget_started_at?->format('M d, Y') ?? '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $user->next_budget_reset_at?->format('M d, Y') ?? 'Never' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<a href="{{ route('gateway-users.show', $user->user_id) }}" class="text-blue-600 hover:text-blue-900">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<p class="text-gray-500 text-center py-8">No users assigned to this budget yet.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assign Users Form -->
|
||||
@if($availableUsers->count() > 0)
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">Assign Users to Budget</h3>
|
||||
|
||||
<form action="{{ route('budgets.assign-users', $budget->budget_id) }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Select Users</label>
|
||||
<div class="border rounded-lg p-4 max-h-64 overflow-y-auto">
|
||||
@foreach($availableUsers as $user)
|
||||
<div class="flex items-center mb-2">
|
||||
<input type="checkbox" name="user_ids[]" value="{{ $user->user_id }}"
|
||||
id="user_{{ $user->user_id }}"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
<label for="user_{{ $user->user_id }}" class="ml-2 text-sm text-gray-900 cursor-pointer">
|
||||
<span class="font-mono">{{ $user->user_id }}</span>
|
||||
@if($user->alias)
|
||||
<span class="text-gray-500">({{ $user->alias }})</span>
|
||||
@endif
|
||||
</label>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@error('user_ids')
|
||||
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Assign Selected Users
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-gray-50 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-center text-gray-500">
|
||||
All users are currently assigned to budgets. No available users to assign.
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -10,17 +10,17 @@
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- Total Users -->
|
||||
<!-- Total Gateway Users -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">Total Users</p>
|
||||
<p class="text-sm text-gray-600">Gateway Users</p>
|
||||
<p class="text-3xl font-bold text-gray-900">
|
||||
{{ number_format($stats['total_users']) }}
|
||||
{{ number_format($stats['total_gateway_users']) }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{{ $stats['active_credentials'] }} active credentials
|
||||
<p class="text-xs text-green-600 mt-1">
|
||||
{{ $stats['active_gateway_users'] }} active
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-blue-500">
|
||||
@@ -99,112 +99,58 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Trend Chart -->
|
||||
<!-- Daily Usage Chart -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
Usage Trend (Last 30 Days)
|
||||
</h3>
|
||||
<canvas id="usageChart" height="80"></canvas>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Daily Usage (Last 30 Days)</h3>
|
||||
<canvas id="dailyUsageChart" height="80"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Provider Stats & Top Users -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Provider Breakdown -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
Usage by Provider
|
||||
</h3>
|
||||
<canvas id="providerChart" height="250"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Users -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
Top Users by Spend
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
@forelse($topUsers as $user)
|
||||
<div class="flex items-center justify-between border-b border-gray-200 pb-3">
|
||||
<div class="flex-1">
|
||||
<p class="font-medium text-gray-900">
|
||||
{{ $user->name }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
{{ number_format($user->llm_requests_count ?? 0) }} requests
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="font-semibold text-green-600">
|
||||
${{ number_format($user->total_cost ?? 0, 2) }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
{{ number_format($user->total_tokens ?? 0) }} tokens
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-500 text-center py-4">
|
||||
No usage data yet
|
||||
</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Stats -->
|
||||
<!-- Provider Statistics -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
Most Used Models
|
||||
</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Provider Statistics (Last 30 Days)</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Model</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Provider</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Requests</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tokens</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cost</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Provider
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Requests
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Total Cost
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Total Tokens
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@forelse($modelStats as $model)
|
||||
@forelse($providerStats as $provider)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{{ $model->model }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium
|
||||
@if($model->provider == 'openai') bg-green-100 text-green-800
|
||||
@elseif($model->provider == 'anthropic') bg-purple-100 text-purple-800
|
||||
@elseif($model->provider == 'mistral') bg-blue-100 text-blue-800
|
||||
@elseif($model->provider == 'gemini') bg-yellow-100 text-yellow-800
|
||||
@else bg-gray-100 text-gray-800
|
||||
@endif">
|
||||
{{ ucfirst($model->provider) }}
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">
|
||||
{{ $provider->provider }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ number_format($model->count) }}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ number_format($provider->count) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ number_format($model->tokens ?? 0) }}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
${{ number_format($provider->total_cost, 2) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
${{ number_format($model->total_cost ?? 0, 4) }}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ number_format($provider->total_tokens) }}
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-4 text-center text-gray-500">
|
||||
No usage data yet
|
||||
<td colspan="4" class="px-6 py-4 text-center text-sm text-gray-500">
|
||||
No data available yet
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
@@ -218,36 +164,34 @@
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
// Usage Trend Chart
|
||||
const usageCtx = document.getElementById('usageChart').getContext('2d');
|
||||
new Chart(usageCtx, {
|
||||
// Daily Usage Chart
|
||||
const dailyUsageCtx = document.getElementById('dailyUsageChart').getContext('2d');
|
||||
const dailyUsageData = @json($dailyUsage);
|
||||
|
||||
new Chart(dailyUsageCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: @json($dailyUsage->pluck('date')),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Requests',
|
||||
data: @json($dailyUsage->pluck('requests')),
|
||||
borderColor: 'rgb(59, 130, 246)',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
tension: 0.4,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: 'Cost ($)',
|
||||
data: @json($dailyUsage->pluck('cost')),
|
||||
borderColor: 'rgb(16, 185, 129)',
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||
tension: 0.4,
|
||||
yAxisID: 'y1',
|
||||
}
|
||||
]
|
||||
labels: dailyUsageData.map(d => d.date),
|
||||
datasets: [{
|
||||
label: 'Requests',
|
||||
data: dailyUsageData.map(d => d.requests),
|
||||
borderColor: 'rgb(59, 130, 246)',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
tension: 0.1,
|
||||
yAxisID: 'y',
|
||||
}, {
|
||||
label: 'Cost ($)',
|
||||
data: dailyUsageData.map(d => d.cost),
|
||||
borderColor: 'rgb(34, 197, 94)',
|
||||
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
||||
tension: 0.1,
|
||||
yAxisID: 'y1',
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
@@ -277,51 +221,6 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Provider Breakdown Chart
|
||||
const providerCtx = document.getElementById('providerChart').getContext('2d');
|
||||
new Chart(providerCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: @json($providerStats->pluck('provider')->map(fn($p) => ucfirst($p))),
|
||||
datasets: [{
|
||||
data: @json($providerStats->pluck('count')),
|
||||
backgroundColor: [
|
||||
'rgba(34, 197, 94, 0.8)', // Green - OpenAI
|
||||
'rgba(168, 85, 247, 0.8)', // Purple - Anthropic
|
||||
'rgba(59, 130, 246, 0.8)', // Blue - Mistral
|
||||
'rgba(251, 191, 36, 0.8)', // Yellow - Gemini
|
||||
'rgba(236, 72, 153, 0.8)', // Pink - DeepSeek
|
||||
'rgba(249, 115, 22, 0.8)', // Orange
|
||||
],
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
padding: 15
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.label || '';
|
||||
let value = context.parsed || 0;
|
||||
let total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
let percentage = ((value / total) * 100).toFixed(1);
|
||||
return label + ': ' + value + ' requests (' + percentage + '%)';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
</x-app-layout>
|
||||
|
||||
@@ -35,28 +35,6 @@
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Budget -->
|
||||
<div class="mb-6">
|
||||
<label for="budget_id" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Budget Template (Optional)
|
||||
</label>
|
||||
<select name="budget_id"
|
||||
id="budget_id"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('budget_id') border-red-300 @enderror">
|
||||
<option value="">No Budget</option>
|
||||
@foreach($budgets as $budget)
|
||||
<option value="{{ $budget->budget_id }}" {{ old('budget_id') == $budget->budget_id ? 'selected' : '' }}>
|
||||
{{ $budget->budget_id }} - ${{ number_format($budget->max_budget, 2) }}
|
||||
({{ floor($budget->budget_duration_sec / 86400) }}d)
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-500">Assign a spending limit to this user</p>
|
||||
@error('budget_id')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="mb-6 bg-blue-50 border-l-4 border-blue-400 p-4">
|
||||
<div class="flex">
|
||||
|
||||
@@ -47,28 +47,6 @@
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Budget -->
|
||||
<div class="mb-6">
|
||||
<label for="budget_id" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Budget Template
|
||||
</label>
|
||||
<select name="budget_id"
|
||||
id="budget_id"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('budget_id') border-red-300 @enderror">
|
||||
<option value="">No Budget</option>
|
||||
@foreach($budgets as $budget)
|
||||
<option value="{{ $budget->budget_id }}"
|
||||
{{ old('budget_id', $user->budget_id) == $budget->budget_id ? 'selected' : '' }}>
|
||||
{{ $budget->budget_id }} - ${{ number_format($budget->max_budget, 2) }}
|
||||
({{ floor($budget->budget_duration_sec / 86400) }}d)
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('budget_id')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Current Spend (Read-only) -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
|
||||
@@ -150,10 +150,14 @@
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">
|
||||
<a href="{{ route('gateway-users.show', $key->user_id) }}"
|
||||
class="text-blue-600 hover:text-blue-900">
|
||||
{{ $key->gatewayUser->alias ?? $key->user_id }}
|
||||
</a>
|
||||
@if($key->user_id)
|
||||
<a href="{{ route('gateway-users.show', $key->user_id) }}"
|
||||
class="text-blue-600 hover:text-blue-900">
|
||||
{{ $key->gatewayUser->alias ?? $key->user_id }}
|
||||
</a>
|
||||
@else
|
||||
<span class="text-gray-400">No user</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
|
||||
@@ -56,10 +56,14 @@
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Associated User</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<a href="{{ route('gateway-users.show', $apiKey->user_id) }}"
|
||||
class="text-blue-600 hover:text-blue-900">
|
||||
{{ $apiKey->gatewayUser->alias ?? $apiKey->user_id }}
|
||||
</a>
|
||||
@if($apiKey->user_id)
|
||||
<a href="{{ route('gateway-users.show', $apiKey->user_id) }}"
|
||||
class="text-blue-600 hover:text-blue-900">
|
||||
{{ $apiKey->gatewayUser->alias ?? $apiKey->user_id }}
|
||||
</a>
|
||||
@else
|
||||
<span class="text-gray-400">No user assigned</span>
|
||||
@endif
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -39,9 +39,6 @@ new class extends Component
|
||||
<x-nav-link :href="route('keys.index')" :active="request()->routeIs('keys.*')" wire:navigate>
|
||||
{{ __('API Keys') }}
|
||||
</x-nav-link>
|
||||
<x-nav-link :href="route('budgets.index')" :active="request()->routeIs('budgets.*')" wire:navigate>
|
||||
{{ __('Budgets') }}
|
||||
</x-nav-link>
|
||||
<x-nav-link :href="route('usage-logs.index')" :active="request()->routeIs('usage-logs.*')" wire:navigate>
|
||||
{{ __('Usage Logs') }}
|
||||
</x-nav-link>
|
||||
@@ -51,9 +48,6 @@ new class extends Component
|
||||
<x-nav-link :href="route('admin.credentials.index')" :active="request()->routeIs('admin.credentials.*')" wire:navigate>
|
||||
{{ __('Credentials') }}
|
||||
</x-nav-link>
|
||||
<x-nav-link :href="route('admin.users.index')" :active="request()->routeIs('admin.users.*')" wire:navigate>
|
||||
{{ __('User Budgets') }}
|
||||
</x-nav-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -111,9 +105,6 @@ new class extends Component
|
||||
<x-responsive-nav-link :href="route('keys.index')" :active="request()->routeIs('keys.*')" wire:navigate>
|
||||
{{ __('API Keys') }}
|
||||
</x-responsive-nav-link>
|
||||
<x-responsive-nav-link :href="route('budgets.index')" :active="request()->routeIs('budgets.*')" wire:navigate>
|
||||
{{ __('Budgets') }}
|
||||
</x-responsive-nav-link>
|
||||
<x-responsive-nav-link :href="route('usage-logs.index')" :active="request()->routeIs('usage-logs.*')" wire:navigate>
|
||||
{{ __('Usage Logs') }}
|
||||
</x-responsive-nav-link>
|
||||
@@ -123,9 +114,6 @@ new class extends Component
|
||||
<x-responsive-nav-link :href="route('admin.credentials.index')" :active="request()->routeIs('admin.credentials.*')" wire:navigate>
|
||||
{{ __('Credentials') }}
|
||||
</x-responsive-nav-link>
|
||||
<x-responsive-nav-link :href="route('admin.users.index')" :active="request()->routeIs('admin.users.*')" wire:navigate>
|
||||
{{ __('User Budgets') }}
|
||||
</x-responsive-nav-link>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Settings Options -->
|
||||
|
||||
@@ -4,12 +4,9 @@ use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\DashboardController;
|
||||
use App\Http\Controllers\GatewayUserController;
|
||||
use App\Http\Controllers\ApiKeyController;
|
||||
use App\Http\Controllers\BudgetController;
|
||||
use App\Http\Controllers\UsageLogController;
|
||||
use App\Http\Controllers\ModelPricingController;
|
||||
use App\Http\Controllers\Admin\CredentialController;
|
||||
use App\Http\Controllers\Admin\UserBudgetController;
|
||||
use App\Http\Controllers\Admin\UserManagementController;
|
||||
|
||||
Route::view('/', 'welcome');
|
||||
|
||||
@@ -30,11 +27,6 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::post('keys/{id}/revoke', [ApiKeyController::class, 'revoke'])
|
||||
->name('keys.revoke');
|
||||
|
||||
// Budgets Management
|
||||
Route::resource('budgets', BudgetController::class);
|
||||
Route::post('budgets/{id}/assign-users', [BudgetController::class, 'assignUsers'])
|
||||
->name('budgets.assign-users');
|
||||
|
||||
// Usage Logs
|
||||
Route::get('usage-logs', [UsageLogController::class, 'index'])->name('usage-logs.index');
|
||||
Route::get('usage-logs/export', [UsageLogController::class, 'export'])->name('usage-logs.export');
|
||||
@@ -49,28 +41,12 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
|
||||
// Provider Credentials Management (Admin)
|
||||
Route::prefix('admin')->name('admin.')->group(function () {
|
||||
// User Management
|
||||
Route::get('users', [UserManagementController::class, 'index'])
|
||||
->name('users.index');
|
||||
|
||||
// Credentials
|
||||
Route::resource('credentials', CredentialController::class);
|
||||
Route::post('credentials/{credential}/test', [CredentialController::class, 'test'])
|
||||
->name('credentials.test');
|
||||
Route::post('credentials/{credential}/toggle', [CredentialController::class, 'toggle'])
|
||||
->name('credentials.toggle');
|
||||
|
||||
// User Budget & Rate Limit Management
|
||||
Route::get('users/{user}/budget', [UserBudgetController::class, 'show'])
|
||||
->name('users.budget.show');
|
||||
Route::put('users/{user}/budget', [UserBudgetController::class, 'updateBudget'])
|
||||
->name('users.budget.update');
|
||||
Route::put('users/{user}/rate-limit', [UserBudgetController::class, 'updateRateLimit'])
|
||||
->name('users.rate-limit.update');
|
||||
Route::post('users/{user}/rate-limit/reset', [UserBudgetController::class, 'resetRateLimit'])
|
||||
->name('users.rate-limit.reset');
|
||||
Route::post('users/{user}/budget/reset', [UserBudgetController::class, 'resetBudget'])
|
||||
->name('users.budget.reset');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user