First Commit
This commit is contained in:
+153
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\SuperAdmin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\SubscriptionPlan;
|
||||
use App\Models\User;
|
||||
use App\Models\UserNotification;
|
||||
use App\Models\UserSubscription;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class BillingController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$stats = [
|
||||
'total_plans' => SubscriptionPlan::count(),
|
||||
'active_plans' => SubscriptionPlan::where('is_active', true)->count(),
|
||||
'active_subscriptions' => UserSubscription::where('status', 'active')->count(),
|
||||
'total_revenue' => Invoice::where('status', 'paid')->sum('amount'),
|
||||
'pending_invoices' => Invoice::where('status', 'pending')->count(),
|
||||
'expiring_soon' => UserSubscription::where('status', 'active')
|
||||
->where('end_date', '<=', now()->addDays(7))
|
||||
->count(),
|
||||
];
|
||||
|
||||
$plans = SubscriptionPlan::withCount('userSubscriptions')->get();
|
||||
$subscriptions = UserSubscription::with(['user', 'plan'])
|
||||
->orderByDesc('created_at')
|
||||
->paginate(20);
|
||||
|
||||
return view('superadmin.billing.index', compact('stats', 'plans', 'subscriptions'));
|
||||
}
|
||||
|
||||
public function storePlan(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'price' => ['required', 'numeric', 'min:0'],
|
||||
'currency' => ['required', 'string', 'size:3'],
|
||||
'duration_days' => ['required', 'integer', 'min:1'],
|
||||
'token_limit' => ['nullable', 'integer', 'min:0'],
|
||||
]);
|
||||
|
||||
SubscriptionPlan::create($validated + ['is_active' => true]);
|
||||
|
||||
return back()->with('success', __('Plan created successfully.'));
|
||||
}
|
||||
|
||||
public function updatePlan(Request $request, SubscriptionPlan $plan): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'price' => ['required', 'numeric', 'min:0'],
|
||||
'currency' => ['required', 'string', 'size:3'],
|
||||
'duration_days' => ['required', 'integer', 'min:1'],
|
||||
'token_limit' => ['nullable', 'integer', 'min:0'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
$plan->update($validated);
|
||||
|
||||
return back()->with('success', __('Plan updated successfully.'));
|
||||
}
|
||||
|
||||
public function approvePayment(UserSubscription $subscription): RedirectResponse
|
||||
{
|
||||
$subscription->update(['payment_approved' => true, 'status' => 'active']);
|
||||
|
||||
// Create invoice
|
||||
$lastInvoice = Invoice::orderByDesc('id')->first();
|
||||
$nextNumber = $lastInvoice ? (int)filter_var($lastInvoice->invoice_number, FILTER_SANITIZE_NUMBER_INT) + 1 : 1001;
|
||||
|
||||
$invoice = Invoice::create([
|
||||
'user_id' => $subscription->user_id,
|
||||
'user_subscription_id' => $subscription->id,
|
||||
'invoice_number' => 'INV-' . $nextNumber . '/' . now()->year,
|
||||
'amount' => $subscription->plan->price,
|
||||
'currency' => $subscription->plan->currency,
|
||||
'status' => 'paid',
|
||||
'payment_method' => 'manual',
|
||||
'paid_at' => now(),
|
||||
]);
|
||||
|
||||
// Generate PDF
|
||||
$this->generateInvoicePdf($invoice);
|
||||
|
||||
// Notify user
|
||||
UserNotification::create([
|
||||
'user_id' => $subscription->user_id,
|
||||
'type' => 'payment_received',
|
||||
'title' => __('Payment Approved'),
|
||||
'message' => __('Your payment of :amount has been approved. Your subscription is active until :date.', [
|
||||
'amount' => 'R$ ' . number_format($subscription->plan->price, 2, ',', '.'),
|
||||
'date' => $subscription->end_date->format('d/m/Y'),
|
||||
]),
|
||||
]);
|
||||
|
||||
return back()->with('success', __('Payment approved. Invoice generated and user notified.'));
|
||||
}
|
||||
|
||||
public function cancelSubscription(UserSubscription $subscription): RedirectResponse
|
||||
{
|
||||
$subscription->update(['status' => 'cancelled', 'cancelled_at' => now()]);
|
||||
|
||||
UserNotification::create([
|
||||
'user_id' => $subscription->user_id,
|
||||
'type' => 'admin_message',
|
||||
'title' => __('Subscription Cancelled'),
|
||||
'message' => __('Your subscription has been cancelled by the administrator.'),
|
||||
]);
|
||||
|
||||
return back()->with('success', __('Subscription cancelled.'));
|
||||
}
|
||||
|
||||
public function invoices(): View
|
||||
{
|
||||
$invoices = Invoice::with(['user', 'subscription.plan'])
|
||||
->orderByDesc('created_at')
|
||||
->paginate(20);
|
||||
|
||||
return view('superadmin.billing.invoices', compact('invoices'));
|
||||
}
|
||||
|
||||
public function downloadInvoice(Invoice $invoice)
|
||||
{
|
||||
if ($invoice->pdf_path && Storage::exists($invoice->pdf_path)) {
|
||||
return Storage::download($invoice->pdf_path, "invoice-{$invoice->invoice_number}.pdf");
|
||||
}
|
||||
|
||||
// Generate on the fly
|
||||
$this->generateInvoicePdf($invoice);
|
||||
|
||||
return Storage::download($invoice->pdf_path, "invoice-{$invoice->invoice_number}.pdf");
|
||||
}
|
||||
|
||||
private function generateInvoicePdf(Invoice $invoice): void
|
||||
{
|
||||
$pdf = Pdf::loadView('pdf.invoice', compact('invoice'));
|
||||
$path = "invoices/invoice-{$invoice->invoice_number}.pdf";
|
||||
Storage::put($path, $pdf->output());
|
||||
$invoice->update(['pdf_path' => $path]);
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\SuperAdmin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Subject;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class SystemController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$stats = [
|
||||
"total_users" => \App\Models\User::count(),
|
||||
"active_users" => \App\Models\User::where("is_active", true)->count(),
|
||||
"pending_users" => \App\Models\User::where("is_approved", false)->count(),
|
||||
"total_subjects" => Subject::count(),
|
||||
"total_insights" => \App\Models\Insight::count(),
|
||||
];
|
||||
|
||||
$subjects = Subject::all();
|
||||
|
||||
$aiDefaultProvider = \App\Models\Setting::where('key', 'ai_default_provider')->value('value');
|
||||
$aiDefaultModel = \App\Models\Setting::where('key', 'ai_default_model')->value('value');
|
||||
|
||||
if (!$aiDefaultProvider) {
|
||||
$aiDefaultProvider = config('services.ai.default_provider', 'openrouter');
|
||||
}
|
||||
|
||||
if (!$aiDefaultModel) {
|
||||
$aiDefaultModel = config('services.ai.providers.'.$aiDefaultProvider.'.model', 'inclusionai/ling-3.0-tiny:free');
|
||||
}
|
||||
|
||||
$chatDefaultProvider = \App\Models\Setting::where('key', 'chat_default_provider')->value('value');
|
||||
$chatDefaultModel = \App\Models\Setting::where('key', 'chat_default_model')->value('value');
|
||||
|
||||
if (!$chatDefaultProvider) {
|
||||
$chatDefaultProvider = config('services.ai.default_provider', 'openrouter');
|
||||
}
|
||||
|
||||
if (!$chatDefaultModel) {
|
||||
$chatDefaultModel = config('services.ai.providers.'.$chatDefaultProvider.'.model', 'inclusionai/ling-3.0-tiny:free');
|
||||
}
|
||||
|
||||
return view("superadmin.system.index", compact(
|
||||
"stats",
|
||||
"subjects",
|
||||
"aiDefaultProvider",
|
||||
"aiDefaultModel",
|
||||
"chatDefaultProvider",
|
||||
"chatDefaultModel"
|
||||
));
|
||||
}
|
||||
|
||||
public function updateAiSettings(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
"ai_default_provider" => ["required", "string", "max:50"],
|
||||
"ai_default_model" => ["required", "string", "max:255"],
|
||||
]);
|
||||
|
||||
$provider = $request->input("ai_default_provider");
|
||||
$model = $request->input("ai_default_model");
|
||||
|
||||
\App\Models\Setting::updateOrCreate(
|
||||
["key" => "ai_default_provider"],
|
||||
["value" => $provider, "type" => "string"]
|
||||
);
|
||||
|
||||
\App\Models\Setting::updateOrCreate(
|
||||
["key" => "ai_default_model"],
|
||||
["value" => $model, "type" => "string"]
|
||||
);
|
||||
|
||||
return back()->with("success", __("AI settings updated successfully."));
|
||||
}
|
||||
|
||||
public function updateChatSettings(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
"chat_default_provider" => ["required", "string", "max:50"],
|
||||
"chat_default_model" => ["required", "string", "max:255"],
|
||||
]);
|
||||
|
||||
$provider = $request->input("chat_default_provider");
|
||||
$model = $request->input("chat_default_model");
|
||||
|
||||
\App\Models\Setting::updateOrCreate(
|
||||
["key" => "chat_default_provider"],
|
||||
["value" => $provider, "type" => "string"]
|
||||
);
|
||||
|
||||
\App\Models\Setting::updateOrCreate(
|
||||
["key" => "chat_default_model"],
|
||||
["value" => $model, "type" => "string"]
|
||||
);
|
||||
|
||||
return back()->with("success", __("Chat AI settings updated successfully."));
|
||||
}
|
||||
|
||||
public function createSubject(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
"name" => ["required", "string", "max:255"],
|
||||
"description" => ["nullable", "string"],
|
||||
"introduction" => ["nullable", "string"],
|
||||
]);
|
||||
|
||||
Subject::create([
|
||||
"user_id" => null,
|
||||
"name" => $request->name,
|
||||
"description" => $request->description,
|
||||
"introduction" => $request->introduction,
|
||||
"is_system" => true,
|
||||
"is_active" => true,
|
||||
]);
|
||||
|
||||
return back()->with("success", "System subject created successfully.");
|
||||
}
|
||||
|
||||
public function updateSubject(Request $request, Subject $subject)
|
||||
{
|
||||
$request->validate([
|
||||
"name" => ["required", "string", "max:255"],
|
||||
"description" => ["nullable", "string"],
|
||||
"introduction" => ["nullable", "string"],
|
||||
"is_active" => ["boolean"],
|
||||
]);
|
||||
|
||||
$subject->update($request->only("name", "description", "introduction", "is_active"));
|
||||
|
||||
return back()->with("success", "System subject updated successfully.");
|
||||
}
|
||||
|
||||
public function destroySubject(Subject $subject)
|
||||
{
|
||||
$subject->delete();
|
||||
|
||||
return back()->with("success", "System subject deleted successfully.");
|
||||
}
|
||||
|
||||
public function generateAiSubject(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
"theme" => ["required", "string", "max:255"],
|
||||
]);
|
||||
|
||||
$theme = $request->input("theme");
|
||||
|
||||
$prompt = "Crie um assunto de aprendizado e treinamento sobre o tema: {$theme}. Retorne APENAS um objeto JSON válido (sem texto adicional, sem markdown, sem code blocks) com os campos: name (string, máximo 255 caracteres, formato: 'Aprendizado e Treinamento de [Tema]'), description (string, máximo 1000 caracteres), introduction (string, máximo 5000 caracteres). Todo o conteúdo deve estar em Português do Brasil. O assunto deve ser educativo, prático e adequado para geração de insights de aprendizagem pela IA.";
|
||||
|
||||
/** @var \App\Services\AIService $ai */
|
||||
$ai = app(\App\Services\AIService::class);
|
||||
$result = $ai->generateInsight($theme, $prompt, ["language" => "pt_BR"]);
|
||||
|
||||
if (!$result["success"]) {
|
||||
return back()->with("error", __("Failed to generate subject: :error", ["error" => $result["error"] ?? __("Unknown error")]));
|
||||
}
|
||||
|
||||
$content = $result["content"];
|
||||
|
||||
$data = $this->extractJsonFromAiResponse($content);
|
||||
|
||||
if (!$data || !isset($data["name"])) {
|
||||
$data = [
|
||||
"name" => "Aprendizado e Treinamento de {$theme}",
|
||||
"description" => $theme,
|
||||
"introduction" => $content,
|
||||
];
|
||||
}
|
||||
|
||||
Subject::create([
|
||||
"user_id" => null,
|
||||
"name" => $data["name"],
|
||||
"description" => $data["description"] ?? null,
|
||||
"introduction" => $data["introduction"] ?? null,
|
||||
"is_system" => true,
|
||||
"is_active" => true,
|
||||
]);
|
||||
|
||||
return back()->with("success", __("System subject generated successfully."));
|
||||
}
|
||||
|
||||
private function extractJsonFromAiResponse(string $content): ?array
|
||||
{
|
||||
$content = trim($content);
|
||||
|
||||
if (str_starts_with($content, "```")) {
|
||||
$content = preg_replace("/^```(?:json)?\s*/", "", $content);
|
||||
$content = preg_replace("/\s*```$/", "", $content);
|
||||
}
|
||||
|
||||
$data = json_decode($content, true);
|
||||
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($data)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if (preg_match("/\{.*\}/s", $content, $matches)) {
|
||||
$data = json_decode($matches[0], true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($data)) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\SuperAdmin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* Controller for SuperAdmin user management.
|
||||
* Handles listing, approving, editing, pausing, and managing users.
|
||||
*/
|
||||
class UserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of all users.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = User::where('id', '!=', Auth::id());
|
||||
|
||||
// Filter by status
|
||||
if ($request->has('status')) {
|
||||
match ($request->status) {
|
||||
'pending' => $query->where('is_approved', false),
|
||||
'approved' => $query->where('is_approved', true),
|
||||
'paused' => $query->where('is_active', false),
|
||||
'verified' => $query->whereNotNull('email_verified_at'),
|
||||
'unverified' => $query->whereNull('email_verified_at'),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
// Search by name or email
|
||||
if ($search = $request->input('search')) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$users = $query->orderByDesc('created_at')->paginate(20);
|
||||
|
||||
// Stats
|
||||
$stats = [
|
||||
'total' => User::count(),
|
||||
'pending' => User::where('is_approved', false)->count(),
|
||||
'active' => User::where('is_active', true)->count(),
|
||||
'paused' => User::where('is_active', false)->count(),
|
||||
'unverified' => User::whereNull('email_verified_at')->count(),
|
||||
];
|
||||
|
||||
return view('superadmin.users.index', compact('users', 'stats'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve a user account.
|
||||
*/
|
||||
public function approve(User $user): RedirectResponse
|
||||
{
|
||||
$user->update(['is_approved' => true]);
|
||||
|
||||
return back()->with('success', __('User :name approved successfully.', ['name' => $user->name]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject/unapprove a user account.
|
||||
*/
|
||||
public function reject(User $user): RedirectResponse
|
||||
{
|
||||
$user->update(['is_approved' => false]);
|
||||
|
||||
return back()->with('success', __('User :name rejected successfully.', ['name' => $user->name]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle user active status (pause/activate).
|
||||
*/
|
||||
public function toggleActive(User $user): RedirectResponse
|
||||
{
|
||||
$user->update(['is_active' => !$user->is_active]);
|
||||
|
||||
$status = $user->is_active ? __('activated') : __('paused');
|
||||
|
||||
return back()->with('success', __("User :name {$status}.", ['name' => $user->name]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark user email as verified without requiring email confirmation.
|
||||
*/
|
||||
public function verifyEmail(User $user): RedirectResponse
|
||||
{
|
||||
if ($user->hasVerifiedEmail()) {
|
||||
return back()->with('info', __('User :name email is already verified.', ['name' => $user->name]));
|
||||
}
|
||||
|
||||
$user->markEmailAsVerified();
|
||||
|
||||
return back()->with('success', __('User :name email verified successfully.', ['name' => $user->name]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the edit form for a user.
|
||||
*/
|
||||
public function edit(User $user): View
|
||||
{
|
||||
return view('superadmin.users.edit', compact('user'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user details.
|
||||
*/
|
||||
public function update(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,' . $user->id],
|
||||
'role' => ['required', 'string', 'in:admin,superadmin'],
|
||||
'token_limit' => ['nullable', 'integer', 'min:0'],
|
||||
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
|
||||
]);
|
||||
|
||||
$user->update([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'token_limit' => $validated['token_limit'] !== null && $validated['token_limit'] !== ''
|
||||
? (int) $validated['token_limit']
|
||||
: null,
|
||||
]);
|
||||
|
||||
if (!empty($validated['password'])) {
|
||||
$user->update(['password' => Hash::make($validated['password'])]);
|
||||
}
|
||||
|
||||
// Sync role
|
||||
$user->syncRoles([$validated['role']]);
|
||||
|
||||
return redirect()->route('superadmin.users.index')
|
||||
->with('success', __('User :name updated successfully.', ['name' => $user->name]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user account.
|
||||
*/
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
$name = $user->name;
|
||||
$user->delete();
|
||||
|
||||
return back()->with('success', __('User :name deleted successfully.', ['name' => $name]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user