First Commit

This commit is contained in:
marco
2026-09-01 07:49:48 -03:00
commit 28b1044038
399 changed files with 41659 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Invoice;
use App\Models\SubscriptionPlan;
use App\Models\UserNotification;
use App\Models\UserSubscription;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
class BillingController extends Controller
{
public function index(): View
{
$user = Auth::user();
$activeSubscription = UserSubscription::where('user_id', $user->id)
->where('status', 'active')
->with('plan')
->first();
$subscriptionHistory = UserSubscription::where('user_id', $user->id)
->with('plan')
->orderByDesc('created_at')
->get();
$invoices = Invoice::where('user_id', $user->id)
->with('subscription.plan')
->orderByDesc('created_at')
->get();
$plans = SubscriptionPlan::where('is_active', true)->get();
$notifications = UserNotification::where('user_id', $user->id)
->where('is_read', false)
->orderByDesc('created_at')
->get();
return view('admin.billing.index', compact(
'activeSubscription', 'subscriptionHistory', 'invoices', 'plans', 'notifications'
));
}
public function subscribe(Request $request, SubscriptionPlan $plan): \Illuminate\Http\RedirectResponse
{
$user = Auth::user();
// Check if user already has active subscription
$existing = UserSubscription::where('user_id', $user->id)
->where('status', 'active')
->first();
if ($existing) {
return back()->with('error', __('You already have an active subscription.'));
}
$subscription = UserSubscription::create([
'user_id' => $user->id,
'subscription_plan_id' => $plan->id,
'start_date' => now(),
'end_date' => now()->addDays($plan->duration_days),
'status' => 'active',
'payment_approved' => false,
]);
// Create pending invoice
$lastInvoice = Invoice::orderByDesc('id')->first();
$nextNumber = $lastInvoice ? (int)filter_var($lastInvoice->invoice_number, FILTER_SANITIZE_NUMBER_INT) + 1 : 1001;
Invoice::create([
'user_id' => $user->id,
'user_subscription_id' => $subscription->id,
'invoice_number' => 'INV-' . $nextNumber . '/' . now()->year,
'amount' => $plan->price,
'currency' => $plan->currency,
'status' => 'pending',
]);
// Notify SuperAdmin
UserNotification::create([
'user_id' => 1, // SuperAdmin
'type' => 'admin_message',
'title' => __('New Subscription'),
'message' => __('User :name subscribed to plan :plan. Payment pending approval.', [
'name' => $user->name,
'plan' => $plan->name,
]),
]);
return back()->with('success', __('Subscription created successfully. Your payment is pending approval.'));
}
public function downloadInvoice(Invoice $invoice)
{
if ($invoice->user_id !== Auth::id()) {
abort(403);
}
if ($invoice->pdf_path && Storage::exists($invoice->pdf_path)) {
return Storage::download($invoice->pdf_path, "invoice-{$invoice->invoice_number}.pdf");
}
return back()->with('error', __('Invoice PDF not available yet.'));
}
public function notifications(): View
{
$user = Auth::user();
$notifications = UserNotification::where('user_id', $user->id)
->orderByDesc('created_at')
->paginate(20);
return view('admin.billing.notifications', compact('notifications'));
}
public function markNotificationRead(UserNotification $notification): \Illuminate\Http\RedirectResponse
{
if ($notification->user_id !== Auth::id()) {
abort(403);
}
$notification->markAsRead();
return back();
}
public function markAllNotificationsRead(): \Illuminate\Http\RedirectResponse
{
UserNotification::where('user_id', Auth::id())
->where('is_read', false)
->update(['is_read' => true, 'read_at' => now()]);
return back()->with('success', __('All notifications marked as read.'));
}
}
+367
View File
@@ -0,0 +1,367 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\CustomSubject;
use App\Models\CustomSubjectQuestion;
use App\Models\TokenUsage;
use App\Services\AIService;
use App\Services\PdfExtractionService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
/**
* Controller for managing user custom subjects created from PDFs.
*/
class CustomSubjectController extends Controller
{
/**
* Display a listing of the user's custom subjects.
*/
public function index(): View
{
/** @var \App\Models\User $user */
$user = Auth::user();
$customSubjects = CustomSubject::where('user_id', $user->id)
->orderByDesc('created_at')
->get();
return view('admin.custom-subjects.index', compact('customSubjects'));
}
/**
* Store a newly created custom subject from a PDF upload.
*/
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'pdf' => ['required', 'file', 'mimetypes:application/pdf', 'max:10240'],
]);
/** @var \App\Models\User $user */
$user = Auth::user();
$pdfFile = $request->file('pdf');
if (!$pdfFile->isValid()) {
return back()->with('error', __('Failed to upload PDF. Please try again.'));
}
$fileName = time() . '_' . uniqid() . '.pdf';
$storagePath = 'custom_subjects/' . $user->id;
$pdfPath = $pdfFile->storeAs($storagePath, $fileName, 'local');
$customSubject = CustomSubject::create([
'user_id' => $user->id,
'name' => $validated['name'],
'pdf_path' => $pdfPath,
'processing_status' => 'processing',
]);
// Extract text asynchronously (in real app, use a queue job)
$pdfExtractionService = new PdfExtractionService();
$fullPath = Storage::disk('local')->path($pdfPath);
$result = $pdfExtractionService->extract($fullPath);
if ($result['success']) {
$customSubject->update([
'extracted_content' => $result['content'],
'processing_status' => 'completed',
]);
} else {
$customSubject->update([
'processing_status' => 'failed',
'error_message' => $result['error'],
]);
}
if ($result['success']) {
return back()->with('success', __('Custom subject created successfully.'));
}
return back()->with('error', __('Failed to process PDF: :error', ['error' => $result['error']]));
}
/**
* Show the form for editing the specified custom subject.
*/
public function edit(CustomSubject $customSubject): View
{
/** @var \App\Models\User $user */
$user = Auth::user();
if ($customSubject->user_id !== $user->id) {
abort(403);
}
return view('admin.custom-subjects.edit', compact('customSubject'));
}
/**
* Update the specified custom subject.
*/
public function update(Request $request, CustomSubject $customSubject): \Illuminate\Http\RedirectResponse
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'is_active' => ['boolean'],
]);
/** @var \App\Models\User $user */
$user = Auth::user();
if ($customSubject->user_id !== $user->id) {
abort(403);
}
$customSubject->update([
'name' => $validated['name'],
'is_active' => $request->has('is_active'),
]);
return back()->with('success', __('Custom subject updated successfully.'));
}
/**
* Display the specified custom subject with Q&A interface.
*/
public function show(CustomSubject $customSubject): View
{
/** @var \App\Models\User $user */
$user = Auth::user();
if ($customSubject->user_id !== $user->id) {
abort(403);
}
$questions = $customSubject->questions()
->orderByDesc('created_at')
->get();
// Get AI settings from DB (unified for all subjects), fallback to config
$provider = \App\Models\Setting::where('key', 'ai_default_provider')->value('value');
$model = \App\Models\Setting::where('key', 'ai_default_model')->value('value');
if (!$provider) {
$provider = config('services.ai.default_provider', 'openrouter');
}
if (!$model) {
$model = config('services.ai.providers.' . $provider . '.model', 'inclusionai/ling-3.0-tiny:free');
}
return view('admin.custom-subjects.show', compact('customSubject', 'questions', 'provider', 'model'));
}
/**
* Ask a question about the custom subject and get an AI answer.
*/
public function ask(Request $request, CustomSubject $customSubject): \Illuminate\Http\RedirectResponse
{
/** @var \App\Models\User $user */
$user = Auth::user();
if ($customSubject->user_id !== $user->id) {
abort(403);
}
if ($customSubject->processing_status !== 'completed' || empty($customSubject->extracted_content)) {
return back()->with('error', __('This PDF is still being processed or has no content.'));
}
$validated = $request->validate([
'question' => ['required', 'string', 'max:2000'],
]);
$question = trim($validated['question']);
// Get AI settings from DB (unified for all subjects), fallback to config
$provider = \App\Models\Setting::where('key', 'ai_default_provider')->value('value');
$model = \App\Models\Setting::where('key', 'ai_default_model')->value('value');
if (!$provider) {
$provider = config('services.ai.default_provider', 'openrouter');
}
if (!$model) {
$model = config('services.ai.providers.' . $provider . '.model', 'inclusionai/ling-3.0-tiny:free');
}
$language = $user->locale ?? 'en';
$languageNames = [
'pt_BR' => 'Brazilian Portuguese',
'en' => 'English',
'es' => 'Spanish',
];
$languageName = $languageNames[$language] ?? 'English';
$systemPrompt = "You are an expert assistant analyzing the content of a document titled '{$customSubject->name}'. "
. "Your task is to answer questions based ONLY on the provided document content. "
. "If the answer is not found in the document, say so clearly. "
. "Do NOT make up information. "
. "CRITICAL LANGUAGE INSTRUCTION: You MUST respond ONLY in {$languageName} ({$language}). "
. "Do NOT use any other language.";
$userMessage = "Document content:\n\n{$customSubject->extracted_content}\n\n"
. "Question: {$question}\n\n"
. "CRITICAL: Answer EXCLUSIVELY in {$languageName}. Do NOT use English. Do NOT translate. "
. "The entire response must be ONLY in {$languageName}.";
$messages = [
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => $userMessage],
];
$aiService = new AIService($provider, $model);
$result = $aiService->chat($messages);
if (!$result['success']) {
return back()->with('error', __('Failed to get response: :error', [
'error' => $result['error'] ?? __('Unknown error'),
]));
}
$answer = $result['content'];
$inputTokens = $result['input_tokens'] ?? 0;
$outputTokens = $result['output_tokens'] ?? 0;
CustomSubjectQuestion::create([
'custom_subject_id' => $customSubject->id,
'user_id' => $user->id,
'question' => $question,
'answer' => $answer,
'provider' => $provider,
'model' => $model,
'input_tokens' => $inputTokens,
'output_tokens' => $outputTokens,
'asked_at' => now(),
]);
// Track token usage
if ($inputTokens > 0 || $outputTokens > 0) {
TokenUsage::create([
'user_id' => $user->id,
'input_tokens' => $inputTokens,
'output_tokens' => $outputTokens,
'total_tokens' => $inputTokens + $outputTokens,
'model' => $model,
'provider' => $provider,
]);
}
return back()->with('success', __('Question answered successfully!'));
}
/**
* Remove the specified custom subject.
*/
public function destroy(CustomSubject $customSubject): RedirectResponse
{
/** @var \App\Models\User $user */
$user = Auth::user();
if ($customSubject->user_id !== $user->id) {
abort(403);
}
// Delete PDF file from storage
if ($customSubject->pdf_path && Storage::disk('local')->exists($customSubject->pdf_path)) {
Storage::disk('local')->delete($customSubject->pdf_path);
}
$customSubject->delete();
return back()->with('success', __('Custom subject deleted successfully.'));
}
/**
* Remove the specified question from the custom subject.
*/
public function destroyQuestion(CustomSubject $customSubject, CustomSubjectQuestion $question): RedirectResponse
{
/** @var \App\Models\User $user */
$user = Auth::user();
if ($question->custom_subject_id !== $customSubject->id || $question->user_id !== $user->id) {
abort(403);
}
$question->delete();
return back()->with('success', __('Question deleted successfully.'));
}
/**
* Translate a question answer to the user's preferred language.
*/
public function translateQuestion(Request $request, CustomSubject $customSubject, CustomSubjectQuestion $question): RedirectResponse
{
/** @var \App\Models\User $user */
$user = Auth::user();
if ($question->custom_subject_id !== $customSubject->id || $question->user_id !== $user->id) {
abort(403);
}
if (!$question->answer) {
return back()->with('error', __('This question has no answer to translate.'));
}
$validated = $request->validate([
'target_language' => ['required', 'string', 'max:10'],
]);
$targetLanguage = $validated['target_language'];
// Get AI settings
$provider = \App\Models\Setting::where('key', 'ai_default_provider')->value('value');
$model = \App\Models\Setting::where('key', 'ai_default_model')->value('value');
if (!$provider) {
$provider = config('services.ai.default_provider', 'openrouter');
}
if (!$model) {
$model = config('services.ai.providers.' . $provider . '.model', 'inclusionai/ling-3.0-tiny:free');
}
$aiService = new AIService($provider, $model);
$result = $aiService->translate($question->answer, $targetLanguage, $customSubject->name);
if (!$result['success']) {
return back()->with('error', __('Failed to translate question: :error', [
'error' => $result['error'] ?? __('Unknown error'),
]));
}
// Update question with translated answer
$question->update([
'answer' => $result['content'],
]);
// Track token usage
$inputTokens = $result['input_tokens'] ?? 0;
$outputTokens = $result['output_tokens'] ?? 0;
if ($inputTokens > 0 || $outputTokens > 0) {
TokenUsage::create([
'user_id' => $user->id,
'input_tokens' => $inputTokens,
'output_tokens' => $outputTokens,
'total_tokens' => $inputTokens + $outputTokens,
'model' => $model,
'provider' => $provider,
]);
}
return back()->with('success', __('Question translated successfully!'));
}
}
+260
View File
@@ -0,0 +1,260 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Enums\MessageType;
use App\Http\Controllers\Controller;
use App\Models\Insight;
use App\Models\Subject;
use App\Models\TokenUsage;
use App\Services\AIService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
/**
* Controller for managing AI-generated insights.
*/
class InsightController extends Controller
{
/**
* The AI service instance.
*/
protected AIService $ai;
/**
* Create a new controller instance.
*/
public function __construct(AIService $ai)
{
$this->ai = $ai;
}
/**
* Display a listing of insights for the authenticated user.
*/
public function index(Request $request): View
{
/** @var \App\Models\User $user */
$user = Auth::user();
$subjectId = $request->input('subject_id');
/** @var \Illuminate\Pagination\LengthAwarePaginator $insights */
$insightsQuery = Insight::where('user_id', $user->id)
->with('subject');
if ($subjectId) {
$insightsQuery->where('subject_id', (int) $subjectId);
}
$insights = $insightsQuery->orderByDesc('created_at')
->paginate(20);
/** @var \Illuminate\Database\Eloquent\Collection<int, Subject> $subjects */
$subjects = Subject::where('is_active', true)->get();
// Token usage statistics
$totalInputTokens = TokenUsage::where('user_id', $user->id)->sum('input_tokens');
$totalOutputTokens = TokenUsage::where('user_id', $user->id)->sum('output_tokens');
$totalTokens = $totalInputTokens + $totalOutputTokens;
return view('admin.insights.index', compact(
'insights',
'subjects',
'subjectId',
'totalInputTokens',
'totalOutputTokens',
'totalTokens'
));
}
/**
* Generate a new AI insight for the specified subject.
*/
public function generate(Request $request): RedirectResponse
{
$validated = $request->validate([
'subject_id' => ['required', 'exists:subjects,id'],
'custom_prompt' => ['nullable', 'string', 'max:500'],
'message_type' => ['nullable', 'string', 'max:50'],
'language' => ['nullable', 'string', 'in:pt_BR,en,es'],
]);
/** @var \App\Models\User $user */
$user = Auth::user();
/** @var Subject|null $subject */
$subject = Subject::findOrFail((int) $validated['subject_id']);
$messageType = MessageType::tryFrom($validated['message_type'] ?? '') ?? MessageType::AiGenerated;
$language = $validated['language']
?? app()->getLocale()
?? 'en';
// Build the prompt with subject context
$prompt = $validated['custom_prompt']
?: $this->buildDefaultPrompt($subject, $messageType);
$context = ['language' => $language];
if ($subject->introduction) {
$context['introduction'] = $subject->introduction;
$prompt = "Context about {$subject->name}: {$subject->introduction}\n\n{$prompt}";
}
// Generate message via AI based on type
$result = $this->ai->generateInsight($subject->name, $prompt, $context, $messageType);
if (!$result['success']) {
return back()->with('error', __('Failed to generate message: :error', [
'error' => $result['error'] ?? __('Unknown error'),
]));
}
// Store the generated message
Insight::create([
'user_id' => $user->id,
'subject_id' => $subject->id,
'content' => $result['content'],
'type' => $messageType->value,
'input_tokens' => $result['input_tokens'],
'output_tokens' => $result['output_tokens'],
'sent_at' => now(),
]);
// Track token usage
TokenUsage::create([
'user_id' => $user->id,
'input_tokens' => $result['input_tokens'],
'output_tokens' => $result['output_tokens'],
'total_tokens' => $result['input_tokens'] + $result['output_tokens'],
'model' => config('services.openrouter.model', 'google/gemini-2.0-flash-exp:free'),
'provider' => 'openrouter',
]);
return back()->with('success', __('Message generated successfully!'));
}
/**
* Build a default prompt based on the message type.
*/
private function buildDefaultPrompt(Subject $subject, MessageType $messageType): string
{
return match ($messageType) {
MessageType::LanguageLesson => __('Teach me a practical language lesson about :subject with examples, practice sentences, pronunciation, grammar, and vocabulary.', ['subject' => $subject->name]),
MessageType::GameSimulator => __('Give me a strategic tip for :subject with actionable strategies, commands, and practical steps.', ['subject' => $subject->name]),
MessageType::TechnicalTeaching => __('Give me a technical teaching lesson about :subject covering components, techniques, repairs, schematics, and practical exercises.', ['subject' => $subject->name]),
default => __('Generate an insightful learning tip about :subject. Include practical advice and key concepts.', ['subject' => $subject->name]),
};
}
/**
* Store a manually created insight.
*/
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'subject_id' => ['required', 'exists:subjects,id'],
'content' => ['required', 'string', 'max:5000'],
]);
/** @var \App\Models\User $user */
$user = Auth::user();
Insight::create([
'user_id' => $user->id,
'subject_id' => (int) $validated['subject_id'],
'content' => $validated['content'],
'type' => 'manual',
'input_tokens' => 0,
'output_tokens' => 0,
'sent_at' => now(),
]);
return back()->with('success', __('Insight created successfully.'));
}
/**
* Remove the specified insight.
*/
public function destroy(Insight $insight): RedirectResponse
{
/** @var \App\Models\User $user */
$user = Auth::user();
if ($insight->user_id !== $user->id && !$user->hasRole('superadmin')) {
return back()->with('error', __('You do not have permission to delete this insight.'));
}
$insight->delete();
return back()->with('success', __('Insight deleted successfully.'));
}
/**
* Translate an insight to the user's preferred language.
*/
public function translate(Request $request, Insight $insight): RedirectResponse
{
/** @var \App\Models\User $user */
$user = Auth::user();
if ($insight->user_id !== $user->id && !$user->hasRole('superadmin')) {
return back()->with('error', __('You do not have permission to translate this insight.'));
}
$validated = $request->validate([
'target_language' => ['required', 'string', 'max:10'],
]);
$targetLanguage = $validated['target_language'];
// Get user's language preference for this subject
$userSubject = \App\Models\UserSubject::where('user_id', $insight->user_id)
->where('subject_id', $insight->subject_id)
->first();
$language = $userSubject->insight_language ?? $user->locale ?? 'en';
// Don't translate if already in the target language
if ($language === $targetLanguage) {
return back()->with('info', __('Insight is already in the selected language.'));
}
$result = $this->ai->translate($insight->content, $targetLanguage, $insight->subject->name);
if (!$result['success']) {
return back()->with('error', __('Failed to translate message: :error', [
'error' => $result['error'] ?? __('Unknown error'),
]));
}
// Create a new insight with translated content
Insight::create([
'user_id' => $insight->user_id,
'subject_id' => $insight->subject_id,
'content' => $result['content'],
'type' => $insight->type->value,
'input_tokens' => $result['input_tokens'],
'output_tokens' => $result['output_tokens'],
'sent_at' => now(),
]);
// Track token usage
TokenUsage::create([
'user_id' => $insight->user_id,
'input_tokens' => $result['input_tokens'],
'output_tokens' => $result['output_tokens'],
'total_tokens' => $result['input_tokens'] + $result['output_tokens'],
'model' => config('services.openrouter.model', 'google/gemini-2.0-flash-exp:free'),
'provider' => 'openrouter',
]);
return back()->with('success', __('Message translated successfully!'));
}
}
+170
View File
@@ -0,0 +1,170 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Subject;
use App\Models\UserInterest;
use App\Models\UserSubject;
use App\Services\AIService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
/**
* Manages user interests and AI-generated subject suggestions.
*/
class InterestController extends Controller
{
/**
* Display the user's interests and AI suggestions.
*/
public function index(): View
{
/** @var \App\Models\User $user */
$user = Auth::user();
$interests = $user->userInterests()->orderByDesc('created_at')->get();
$suggestions = session('suggestions', []);
return view('admin.interests.index', compact('interests', 'suggestions'));
}
/**
* Store a new user interest.
*/
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'interest' => ['required', 'string', 'max:255'],
]);
/** @var \App\Models\User $user */
$user = Auth::user();
$user->userInterests()->create([
'interest' => $validated['interest'],
]);
return back()->with('success', __('Interest added successfully.'));
}
/**
* Remove the specified user interest.
*/
public function destroy(UserInterest $userInterest): RedirectResponse
{
$this->authorize('delete', $userInterest);
$userInterest->delete();
return back()->with('success', __('Interest removed successfully.'));
}
/**
* Generate AI subject suggestions based on user interests.
*/
public function generateSuggestions(Request $request): RedirectResponse
{
/** @var \App\Models\User $user */
$user = Auth::user();
$interests = $user->userInterests()->pluck('interest')->toArray();
if (empty($interests)) {
return back()->with('error', __('Add at least one interest before generating suggestions.'));
}
$interestsList = implode(', ', $interests);
$prompt = "Com base nos interesses do usuário: {$interestsList}. Sugira de 3 a 5 temas de assuntos de aprendizado que seriam relevantes para este usuário. Para cada tema, forneça: 1) name (formato: 'Aprendizado e Treinamento de [Tema]'), 2) description (máximo 1000 caracteres), 3) introduction (máximo 5000 caracteres). Retorne APENAS um array JSON válido (sem texto adicional, sem markdown, sem code blocks) com objetos contendo esses campos. Todo o conteúdo deve estar em Português do Brasil.";
/** @var \App\Services\AIService $ai */
$ai = app(AIService::class);
$result = $ai->generateInsight('Sugestões de assuntos', $prompt, ['language' => 'pt_BR']);
if (!$result['success']) {
return back()->with('error', __('Failed to generate suggestions: :error', ['error' => $result['error'] ?? __('Unknown error')]));
}
$suggestions = $this->extractJsonFromAiResponse($result['content']);
if (!$suggestions || !is_array($suggestions)) {
return back()->with('error', __('Failed to parse AI suggestions. Please try again.'));
}
return back()->with('suggestions', $suggestions)->with('success', __('Suggestions generated successfully.'));
}
/**
* Create a subject automatically from a suggestion.
*/
public function createSubjectFromSuggestion(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string', 'max:1000'],
'introduction' => ['nullable', 'string', 'max:5000'],
]);
$existing = Subject::where('name', $request->name)->first();
if ($existing) {
return back()->with('error', __('A subject with this name already exists.'));
}
$subject = Subject::create([
'user_id' => Auth::id(),
'name' => $request->name,
'description' => $request->description,
'introduction' => $request->introduction,
'is_system' => false,
'is_active' => true,
]);
UserSubject::firstOrCreate(
[
'user_id' => Auth::id(),
'subject_id' => $subject->id,
],
[
'frequency' => 'daily',
'preferred_times' => ['09:00'],
'insight_language' => Auth::user()->locale ?? 'en',
'delivery_channels' => ['dashboard'],
'is_active' => true,
]
);
return back()->with('success', __('Subject created successfully.'));
}
/**
* Extract JSON from AI response, handling code blocks and plain text.
*/
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;
}
}
+511
View File
@@ -0,0 +1,511 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Enums\MessageType;
use App\Http\Controllers\Controller;
use App\Models\InsightSchedule;
use App\Models\Subject;
use App\Models\UserSubject;
use App\Services\AIService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
/**
* Controller for managing user subjects and subscriptions.
*/
class SubjectController extends Controller
{
/**
* Display a listing of available subjects and user subscriptions.
*/
public function index(Request $request): View
{
$user = Auth::user();
/** @var \Illuminate\Database\Eloquent\Collection<int, Subject> $systemSubjects */
$systemSubjects = Subject::where('is_system', true)
->where('is_active', true)
->with(['userSubjects' => function ($query) use ($user) {
$query->where('user_id', $user->id);
}])
->get();
/** @var \Illuminate\Database\Eloquent\Collection<int, UserSubject> $mySubjects */
$mySubjects = UserSubject::where('user_id', $user->id)
->whereHas('subject', function ($query) {
$query->where('user_id', '!=', null)
->where('is_system', false);
})
->with('subject')
->get();
return view('admin.subjects.index', compact('systemSubjects', 'mySubjects'));
}
/**
* Store a newly created custom subject.
*/
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string', 'max:1000'],
'introduction' => ['nullable', 'string', 'max:5000'],
'message_type' => ['nullable', 'string', 'max:50'],
'custom_instructions' => ['nullable', 'string', 'max:5000'],
]);
/** @var \App\Models\User $user */
$user = Auth::user();
/** @var Subject $subject */
$subject = Subject::create([
'user_id' => $user->id,
'name' => $validated['name'],
'description' => $validated['description'] ?? null,
'introduction' => $validated['introduction'] ?? null,
'message_type' => $validated['message_type'] ?? null,
'custom_instructions' => $validated['custom_instructions'] ?? null,
'is_system' => false,
'is_active' => true,
]);
// Auto-subscribe the user to their own subject
UserSubject::firstOrCreate(
[
'user_id' => $user->id,
'subject_id' => $subject->id,
],
[
'frequency' => 'daily',
'preferred_times' => ['09:00'],
'insight_language' => $user->locale ?? 'en',
'delivery_channels' => ['dashboard'],
'is_active' => true,
]
);
return back()->with('success', __('Subject created successfully.'));
}
/**
* Generate a new custom subject using AI.
*/
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.";
$ai = app(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,
];
}
$user = Auth::user();
$subject = Subject::create([
'user_id' => $user->id,
'name' => $data['name'],
'description' => $data['description'] ?? null,
'introduction' => $data['introduction'] ?? null,
'is_system' => false,
'is_active' => true,
]);
UserSubject::firstOrCreate(
[
'user_id' => $user->id,
'subject_id' => $subject->id,
],
[
'frequency' => 'daily',
'preferred_times' => ['09:00'],
'insight_language' => $user->locale ?? 'en',
'delivery_channels' => ['dashboard'],
'is_active' => true,
]
);
return back()->with('success', __('Subject created successfully.'));
}
/**
* Extract JSON from AI response, handling code blocks and plain text.
*/
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;
}
/**
* Subscribe to a subject with scheduling preferences.
*/
public function subscribe(Request $request, Subject $subject): RedirectResponse
{
$validated = $request->validate([
'frequency' => ['required', 'string', 'in:daily,weekly,biweekly,monthly'],
'preferred_times' => ['required', 'array', 'min:1'],
'preferred_times.*' => ['required', 'string', 'date_format:H:i'],
'weekdays' => ['nullable', 'array'],
'weekdays.*' => ['integer', 'between:0,6'],
'insight_language' => ['required', 'string', 'in:pt_BR,en,es'],
'delivery_channels' => ['nullable', 'array'],
'delivery_channels.*' => ['string', 'in:dashboard,telegram'],
]);
/** @var \App\Models\User $user */
$user = Auth::user();
$channels = $validated['delivery_channels'] ?? ['dashboard'];
if (!in_array('dashboard', $channels, true)) {
$channels[] = 'dashboard'; // Dashboard is always enabled
}
UserSubject::updateOrCreate(
[
'user_id' => $user->id,
'subject_id' => $subject->id,
],
[
'frequency' => $validated['frequency'],
'preferred_times' => $validated['preferred_times'],
'weekdays' => $validated['weekdays'] ?? null,
'insight_language' => $validated['insight_language'],
'delivery_channels' => $channels,
'is_active' => true,
]
);
return back()->with('success', __('Subscribed to subject successfully.'));
}
/**
* Update the specified subject.
*/
public function update(Request $request, Subject $subject): RedirectResponse
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string', 'max:1000'],
'message_type' => ['nullable', 'string', 'max:50'],
'custom_instructions' => ['nullable', 'string', 'max:5000'],
'is_active' => ['boolean'],
]);
$subject->update([
'name' => $validated['name'],
'description' => $validated['description'] ?? $subject->description,
'message_type' => $validated['message_type'] ?? $subject->message_type,
'custom_instructions' => $validated['custom_instructions'] ?? $subject->custom_instructions,
'is_active' => $validated['is_active'] ?? $subject->is_active,
]);
return back()->with('success', __('Subject updated successfully.'));
}
/**
* Toggle the active status of a user's subject subscription.
*/
public function toggle(Request $request, UserSubject $userSubject): RedirectResponse
{
$validated = $request->validate([
'is_active' => ['required', 'boolean'],
]);
$userSubject->update([
'is_active' => $validated['is_active'],
]);
if (!$validated['is_active']) {
InsightSchedule::where('user_id', $userSubject->user_id)
->where('subject_id', $userSubject->subject_id)
->delete();
}
$status = $validated['is_active'] ? __('activated') : __('deactivated');
return back()->with('success', __("Subject {$status} successfully."));
}
/**
* Update the schedule for a user's subject subscription.
*/
public function updateSchedule(Request $request, UserSubject $userSubject): RedirectResponse
{
$validated = $request->validate([
'frequency' => ['required', 'string', 'in:daily,weekly,biweekly,monthly'],
'preferred_times' => ['required', 'array', 'min:1'],
'preferred_times.*' => ['required', 'string', 'date_format:H:i'],
'weekdays' => ['nullable', 'array'],
'weekdays.*' => ['integer', 'between:0,6'],
'insight_language' => ['required', 'string', 'in:pt_BR,en,es'],
'delivery_channels' => ['nullable', 'array'],
'delivery_channels.*' => ['string', 'in:dashboard,telegram'],
]);
$channels = $validated['delivery_channels'] ?? ['dashboard'];
if (!in_array('dashboard', $channels, true)) {
$channels[] = 'dashboard';
}
$userSubject->update([
'frequency' => $validated['frequency'],
'preferred_times' => $validated['preferred_times'],
'weekdays' => $validated['weekdays'] ?? $userSubject->weekdays,
'insight_language' => $validated['insight_language'],
'delivery_channels' => $channels,
]);
return back()->with('success', __('Schedule updated successfully.'));
}
/**
* Remove the specified subject.
*/
public function destroy(Subject $subject): RedirectResponse
{
/** @var \App\Models\User $user */
$user = Auth::user();
// Only allow deletion if the user owns the subject or is SuperAdmin
if ($subject->user_id !== $user->id && !$user->hasRole('superadmin')) {
return back()->with('error', __('You do not have permission to delete this subject.'));
}
$subject->delete();
return back()->with('success', __('Subject deleted successfully.'));
}
/**
* Unsubscribe from a subject.
*/
public function unsubscribe(UserSubject $userSubject): RedirectResponse
{
$userSubject->delete();
return back()->with('success', __('Unsubscribed from subject successfully.'));
}
/**
* Send an immediate AI insight for a subject.
*/
public function sendInsight(Request $request, Subject $subject): RedirectResponse
{
/** @var \App\Models\User $user */
$user = Auth::user();
$ai = app(AIService::class);
$messageType = MessageType::tryFrom($request->input('message_type', '')) ?? MessageType::AiGenerated;
if ($messageType === MessageType::AiGenerated && $subject->message_type) {
$messageType = MessageType::tryFrom($subject->message_type) ?? MessageType::AiGenerated;
}
// Get the user's language preference for this subject
$userSubject = UserSubject::where('user_id', $user->id)
->where('subject_id', $subject->id)
->first();
$language = $userSubject->insight_language ?? $user->locale ?? 'en';
$prompt = match ($messageType) {
MessageType::LanguageLesson => __('Teach me a practical language lesson about :subject with examples, practice sentences, pronunciation, grammar, and vocabulary.', ['subject' => $subject->name]),
MessageType::GameSimulator => __('Give me a strategic tip for :subject with actionable strategies, commands, and practical steps.', ['subject' => $subject->name]),
MessageType::TechnicalTeaching => __('Give me a technical teaching lesson about :subject covering components, techniques, repairs, schematics, and practical exercises.', ['subject' => $subject->name]),
default => __('Generate an insightful learning tip about :subject. Include practical advice and key concepts.', ['subject' => $subject->name]),
};
$context = ['language' => $language];
if ($subject->introduction) {
$context['introduction'] = $subject->introduction;
$prompt = __('Context about :subject: :introduction', [
'subject' => $subject->name,
'introduction' => $subject->introduction,
]) . "\n\n" . $prompt;
}
$result = $ai->generateInsight($subject->name, $prompt, $context, $messageType);
if (!$result['success']) {
return back()->with('error', __('Failed to generate message: :error', [
'error' => $result['error'] ?? __('Unknown error'),
]));
}
\App\Models\Insight::create([
'user_id' => $user->id,
'subject_id' => $subject->id,
'content' => $result['content'],
'type' => $messageType->value,
'input_tokens' => $result['input_tokens'],
'output_tokens' => $result['output_tokens'],
'sent_at' => now(),
]);
\App\Models\TokenUsage::create([
'user_id' => $user->id,
'input_tokens' => $result['input_tokens'],
'output_tokens' => $result['output_tokens'],
'total_tokens' => $result['input_tokens'] + $result['output_tokens'],
'model' => config('services.openrouter.model', 'google/gemini-2.0-flash-exp:free'),
'provider' => 'openrouter',
]);
// Send to Telegram if user has a bot configured
$this->sendToTelegram($user, $subject->id, $result['content']);
return back()->with('success', __('Message generated and sent successfully!'));
}
/**
* Send an AI-generated introduction for a subject as an insight.
* Always generates a fresh introduction via OpenRouter AI.
*/
public function sendIntroduction(Request $request, Subject $subject): RedirectResponse
{
/** @var \App\Models\User $user */
$user = Auth::user();
$ai = app(AIService::class);
$userSubject = UserSubject::where('user_id', $user->id)
->where('subject_id', $subject->id)
->first();
$language = $userSubject->insight_language ?? $user->locale ?? 'en';
$prompt = "Create a comprehensive and engaging introduction about the subject: {$subject->name}. "
. "Explain what it is, why it's important to learn, its key concepts, "
. "and what the learner will gain from studying it. "
. "Be informative, inspiring, and practical.";
$context = ['language' => $language];
if ($subject->introduction) {
$context['existing_introduction'] = $subject->introduction;
$prompt .= "\n\nHere is some context about the subject: {$subject->introduction}";
}
$result = $ai->generateInsight($subject->name, $prompt, $context, MessageType::Introduction);
if (!$result['success']) {
return back()->with('error', __('Failed to generate introduction: :error', [
'error' => $result['error'] ?? __('Unknown error'),
]));
}
$subject->update(['introduction' => $result['content']]);
\App\Models\Insight::create([
'user_id' => $user->id,
'subject_id' => $subject->id,
'content' => $result['content'],
'type' => MessageType::Introduction->value,
'input_tokens' => $result['input_tokens'],
'output_tokens' => $result['output_tokens'],
'sent_at' => now(),
]);
\App\Models\TokenUsage::create([
'user_id' => $user->id,
'input_tokens' => $result['input_tokens'],
'output_tokens' => $result['output_tokens'],
'total_tokens' => $result['input_tokens'] + $result['output_tokens'],
'model' => config('services.openrouter.model'),
'provider' => 'openrouter',
]);
// Send to Telegram if user has a bot configured
$this->sendToTelegram($user, $subject->id, $result['content']);
return back()->with('success', __('Introduction generated by AI and sent successfully!'));
}
/**
* Send insight content to the user's Telegram bot if configured and enabled.
*/
private function sendToTelegram(\App\Models\User $user, int $subjectId, string $content): void
{
try {
// Check if user has Telegram enabled for this subject
$userSubject = \App\Models\UserSubject::where('user_id', $user->id)
->where('subject_id', $subjectId)
->first();
// If no user subject found or telegram channel not enabled, skip
if (!$userSubject || !$userSubject->hasChannel('telegram')) {
return;
}
/** @var \App\Models\TelegramUser|null $telegramUser */
$telegramUser = $user->telegramUsers()->where('is_active', true)->first();
if (!$telegramUser || !$telegramUser->isFullyConfigured()) {
return;
}
$subject = \App\Models\Subject::find($subjectId);
$subjectName = $subject ? $subject->name : __('Subject');
$telegramService = new \App\Services\TelegramService($telegramUser->bot_token);
// Strip markdown for Telegram (it has its own formatting)
$plainText = \App\Services\MarkdownService::strip($content);
$message = "📚 *{$subjectName}*\n\n{$plainText}";
$telegramService->sendMessage($telegramUser->chat_id, $message);
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Failed to send insight to Telegram', [
'user_id' => $user->id,
'error' => $e->getMessage(),
]);
}
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\TelegramUser;
use App\Services\TelegramService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
/**
* Controller for managing Telegram bot integration.
* Each user can configure their own Telegram bot for receiving insights.
*/
class TelegramController extends Controller
{
/**
* Display the Telegram configuration page.
*/
public function index(): View
{
/** @var \App\Models\User $user */
$user = Auth::user();
/** @var TelegramUser|null $telegramUser */
$telegramUser = $user->telegramUsers()->first();
return view('admin.telegram.index', compact('telegramUser'));
}
/**
* Connect/configure the user's Telegram bot.
*/
public function connect(Request $request): RedirectResponse
{
$validated = $request->validate([
'bot_token' => ['required', 'string', 'max:255'],
'bot_username' => ['nullable', 'string', 'max:255'],
'chat_id' => ['required', 'string', 'max:255'],
'telegram_username' => ['nullable', 'string', 'max:255'],
]);
/** @var \App\Models\User $user */
$user = Auth::user();
// Test the bot connection before saving
$telegramService = new TelegramService($validated['bot_token']);
$testResult = $telegramService->sendMessage(
$validated['chat_id'],
'🤖 *Annunk Bot Connected!*' . "\n\n"
. '✅ Your Telegram bot has been successfully connected to Annunk.'
. "\n\n" . 'You will now receive learning insights here.'
);
if (!$testResult) {
return back()->with('error', __(
'Failed to connect. Please check your Bot Token and Chat ID. '
. 'Make sure you have started a conversation with your bot first.'
))->withInput();
}
TelegramUser::updateOrCreate(
['user_id' => $user->id],
[
'telegram_user_id' => $validated['chat_id'],
'bot_token' => $validated['bot_token'],
'bot_username' => $validated['bot_username'] ?? null,
'chat_id' => $validated['chat_id'],
'telegram_username' => $validated['telegram_username'] ?? null,
'is_active' => true,
]
);
return back()->with('success', __('Telegram bot connected successfully! A test message was sent.'));
}
/**
* Send a test message to verify the bot is working.
*/
public function test(): RedirectResponse
{
/** @var \App\Models\User $user */
$user = Auth::user();
/** @var TelegramUser|null $telegramUser */
$telegramUser = $user->telegramUsers()->first();
if (!$telegramUser || !$telegramUser->isFullyConfigured()) {
return back()->with('error', __('Please configure your Telegram bot first.'));
}
$telegramService = new TelegramService($telegramUser->bot_token);
$result = $telegramService->sendMessage(
$telegramUser->chat_id,
'🧪 *Annunk Test Message*' . "\n\n"
. '✅ Your bot is working correctly!'
. "\n\n" . 'You will receive learning insights according to your schedule.'
);
if ($result) {
return back()->with('success', __('Test message sent successfully! Check your Telegram.'));
}
return back()->with('error', __('Failed to send test message. Please check your bot configuration.'));
}
/**
* Disconnect the user's Telegram bot.
*/
public function disconnect(): RedirectResponse
{
/** @var \App\Models\User $user */
$user = Auth::user();
$user->telegramUsers()->delete();
return back()->with('success', __('Telegram bot disconnected successfully.'));
}
}
@@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Validation\MathCaptchaRule;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AuthenticatedSessionController extends Controller
{
public function create(): View
{
$this->generateMathCaptcha();
return view("auth.login");
}
public function store(Request $request): RedirectResponse
{
$request->validate([
"email" => ["required", "string", "email"],
"password" => ["required", "string"],
"math_captcha" => ["required", "string", new MathCaptchaRule()],
]);
if (!Auth::attempt($request->only("email", "password"), $request->boolean("remember"))) {
return back()->withErrors([
"email" => __("auth.failed"),
])->onlyInput("email");
}
$request->session()->regenerate();
return redirect()->intended(route("dashboard", absolute: false));
}
public function destroy(Request $request): RedirectResponse
{
Auth::guard("web")->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect("/");
}
private function generateMathCaptcha(): void
{
$num1 = random_int(1, 10);
$num2 = random_int(1, 10);
$answer = $num1 + $num2;
session([
"math_captcha_answer" => $answer,
"math_captcha_question" => __("What is :num1 + :num2?", ["num1" => $num1, "num2" => $num2]),
]);
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Validation\MathCaptchaRule;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class RegisteredUserController extends Controller
{
public function create(): View
{
$this->generateMathCaptcha();
return view("auth.register");
}
public function store(Request $request): RedirectResponse
{
$request->validate([
"name" => ["required", "string", "max:255"],
"email" => ["required", "string", "lowercase", "email", "max:255", "unique:".User::class],
"password" => ["required", "confirmed", Rules\Password::defaults()],
"math_captcha" => ["required", "string", new MathCaptchaRule()],
]);
$user = User::create([
"name" => $request->name,
"email" => $request->email,
"password" => Hash::make($request->password),
"is_approved" => false,
]);
event(new Registered($user));
Auth::login($user);
return redirect(route("dashboard", absolute: false));
}
private function generateMathCaptcha(): void
{
$num1 = random_int(1, 10);
$num2 = random_int(1, 10);
$answer = $num1 + $num2;
session([
"math_captcha_answer" => $answer,
"math_captcha_question" => __("What is :num1 + :num2?", ["num1" => $num1, "num2" => $num2]),
]);
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Validation\ContentFilterRule;
use App\Models\ChatMessage;
use App\Models\TokenUsage;
use App\Services\AIService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule as ValidationRule;
use Illuminate\View\View;
/**
* Controller for the AI Chat feature.
* Allows users to chat with registered AI models.
*/
class ChatController extends Controller
{
/**
* Display the AI chat interface.
*/
public function index(): View
{
/** @var \App\Models\User $user */
$user = Auth::user();
// Get available AI providers from config
$providers = config('services.ai.providers', []);
// Get user's chat history
$chatHistory = ChatMessage::where('user_id', $user->id)
->orderBy('created_at', 'asc')
->take(50)
->get();
// Get chat default provider/model from DB, fallback to config
$defaultProvider = \App\Models\Setting::where('key', 'chat_default_provider')->value('value');
$defaultModel = \App\Models\Setting::where('key', 'chat_default_model')->value('value');
if (!$defaultProvider) {
$defaultProvider = config('services.ai.default_provider', 'openrouter');
}
if (!$defaultModel) {
$defaultModel = config('services.ai.providers.'.$defaultProvider.'.model', 'inclusionai/ling-3.0-tiny:free');
}
return view('dashboard.chat', compact(
'providers',
'chatHistory',
'defaultProvider',
'defaultModel'
));
}
/**
* Send a message to the AI and get a response.
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
{
/** @var \App\Models\User $user */
$user = Auth::user();
$validated = $request->validate([
'message' => ['required', 'string', 'max:4000', new ContentFilterRule()],
]);
$message = trim($validated['message']);
// Use chat default provider/model from DB
$defaultProvider = \App\Models\Setting::where('key', 'chat_default_provider')->value('value');
$defaultModel = \App\Models\Setting::where('key', 'chat_default_model')->value('value');
if (!$defaultProvider) {
$defaultProvider = config('services.ai.default_provider', 'openrouter');
}
if (!$defaultModel) {
$defaultModel = config('services.ai.providers.'.$defaultProvider.'.model', 'inclusionai/ling-3.0-tiny:free');
}
// Build conversation history for context
$recentMessages = ChatMessage::where('user_id', $user->id)
->orderByDesc('created_at')
->take(10)
->get()
->reverse()
->values();
$messages = [];
foreach ($recentMessages as $chat) {
$messages[] = ['role' => 'user', 'content' => $chat->message];
if ($chat->response) {
$messages[] = ['role' => 'assistant', 'content' => $chat->response];
}
}
$messages[] = ['role' => 'user', 'content' => $message];
// Initialize AI service with selected provider and model
$aiService = new AIService($defaultProvider, $defaultModel);
// Use the chat completion endpoint directly
$result = $aiService->chat($messages);
if (!$result['success']) {
return back()->with('error', __('Failed to get response: :error', [
'error' => $result['error'] ?? __('Unknown error'),
]));
}
$response = $result['content'];
$inputTokens = $result['input_tokens'] ?? 0;
$outputTokens = $result['output_tokens'] ?? 0;
$model = $defaultModel;
$provider = $defaultProvider;
// Save chat message
ChatMessage::create([
'user_id' => $user->id,
'message' => $message,
'response' => $response,
'provider' => $provider,
'model' => $model,
'input_tokens' => $inputTokens,
'output_tokens' => $outputTokens,
'sent_at' => now(),
]);
// Track token usage
if ($inputTokens > 0 || $outputTokens > 0) {
TokenUsage::create([
'user_id' => $user->id,
'input_tokens' => $inputTokens,
'output_tokens' => $outputTokens,
'total_tokens' => $inputTokens + $outputTokens,
'model' => $model,
'provider' => $provider,
]);
}
return back()->with('success', __('Message sent successfully!'));
}
/**
* Clear chat history.
*/
public function destroy(Request $request): \Illuminate\Http\RedirectResponse
{
/** @var \App\Models\User $user */
$user = Auth::user();
ChatMessage::where('user_id', $user->id)->delete();
return back()->with('success', __('Chat history cleared.'));
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
/**
* Base controller for Annunk application.
*/
class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
}
+154
View File
@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Insight;
use App\Models\Subject;
use App\Models\TokenUsage;
use App\Models\UserSubject;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
/**
* Controller for the main user dashboard.
* Displays insights, statistics, token usage, and charts.
*/
class DashboardController extends Controller
{
/**
* Display the user dashboard with insights and statistics.
*/
public function index(Request $request): View
{
/** @var \App\Models\User $user */
$user = Auth::user();
// Basic statistics
$subjectsCount = UserSubject::where('user_id', $user->id)->count();
$activeSubjectsCount = UserSubject::where('user_id', $user->id)
->where('is_active', true)
->count();
$insightsCount = Insight::where('user_id', $user->id)->count();
// Token usage statistics
$totalInputTokens = TokenUsage::where('user_id', $user->id)->sum('input_tokens');
$totalOutputTokens = TokenUsage::where('user_id', $user->id)->sum('output_tokens');
$totalTokens = $totalInputTokens + $totalOutputTokens;
// Recent insights (last 10)
/** @var \Illuminate\Database\Eloquent\Collection<int, Insight> $recentInsights */
$recentInsights = Insight::where('user_id', $user->id)
->with('subject')
->orderByDesc('created_at')
->take(10)
->get();
// Active subjects with schedules
/** @var \Illuminate\Database\Eloquent\Collection<int, UserSubject> $activeSubjects */
$activeSubjects = UserSubject::where('user_id', $user->id)
->where('is_active', true)
->with('subject')
->get();
// Token usage chart data (last 30 days)
$tokenChartData = $this->getTokenChartData($user->id);
// Insights per subject chart data
$subjectChartData = $this->getSubjectChartData($user->id);
// Weekly activity data
$weeklyActivity = $this->getWeeklyActivity($user->id);
return view('dashboard.index', compact(
'subjectsCount',
'activeSubjectsCount',
'insightsCount',
'totalInputTokens',
'totalOutputTokens',
'totalTokens',
'recentInsights',
'activeSubjects',
'tokenChartData',
'subjectChartData',
'weeklyActivity'
));
}
/**
* Get token usage data for charts (last 30 days).
*
* @return array<string, mixed>
*/
private function getTokenChartData(int $userId): array
{
$days = 30;
$data = [];
for ($i = $days - 1; $i >= 0; $i--) {
$date = now()->subDays($i)->format('Y-m-d');
$dailyUsage = TokenUsage::where('user_id', $userId)
->whereDate('created_at', $date)
->selectRaw('COALESCE(SUM(input_tokens), 0) as input_tokens')
->selectRaw('COALESCE(SUM(output_tokens), 0) as output_tokens')
->first();
$data['labels'][] = now()->subDays($i)->format('d/m');
$data['input'][] = (int) ($dailyUsage->input_tokens ?? 0);
$data['output'][] = (int) ($dailyUsage->output_tokens ?? 0);
}
return $data;
}
/**
* Get insights per subject for chart data.
*
* @return array<string, mixed>
*/
private function getSubjectChartData(int $userId): array
{
$insightsBySubject = Insight::where('user_id', $userId)
->selectRaw('subject_id, COUNT(*) as total')
->groupBy('subject_id')
->with('subject')
->get();
$data = [
'labels' => [],
'values' => [],
];
foreach ($insightsBySubject as $item) {
$data['labels'][] = $item->subject?->name ?? __('Unknown');
$data['values'][] = (int) $item->total;
}
return $data;
}
/**
* Get weekly activity data (insights per day of week).
*
* @return array<string, mixed>
*/
private function getWeeklyActivity(int $userId): array
{
$daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
$data = [
'labels' => $daysOfWeek,
'values' => [],
];
foreach (range(0, 6) as $day) {
$count = Insight::where('user_id', $userId)
->whereRaw('DAYOFWEEK(created_at) = ?', [$day + 1])
->count();
$data['values'][] = $count;
}
return $data;
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
class LanguageController extends Controller
{
public function switch(string $locale): \Illuminate\Http\RedirectResponse
{
$supportedLocales = ['pt_BR', 'en', 'es'];
if (!in_array($locale, $supportedLocales, true)) {
$locale = config('app.locale', 'pt_BR');
}
App::setLocale($locale);
session(['locale' => $locale]);
if (Auth::check()) {
Auth::user()->update(['locale' => $locale]);
}
return redirect()->back();
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace App\Http\Controllers;
use App\Models\UserConsent;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LegalController extends Controller
{
public function terms()
{
$currentVersion = config('legal.terms_version', '1.0.0');
return view('legal.terms', compact('currentVersion'));
}
public function privacy()
{
$currentVersion = config('legal.privacy_version', '1.0.0');
return view('legal.privacy', compact('currentVersion'));
}
public function cookies()
{
$currentVersion = config('legal.cookie_version', '1.0.0');
return view('legal.cookies', compact('currentVersion'));
}
public function accept(Request $request)
{
$request->validate([
'terms_version' => 'required|string',
'privacy_version' => 'required|string',
'cookie_version' => 'required|string',
]);
$user = Auth::user();
UserConsent::create([
'user_id' => $user->id,
'terms_version' => $request->terms_version,
'privacy_version' => $request->privacy_version,
'cookie_version' => $request->cookie_version,
'accepted_at' => now(),
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
return redirect()->intended('/dashboard')->with('success', __('legal.accepted_successfully'));
}
public function history()
{
$user = Auth::user();
$consents = $user->consents()->orderByDesc('accepted_at')->paginate(20);
return view('legal.history', compact('consents'));
}
public function acceptCookieBanner(Request $request)
{
$request->validate([
'terms_version' => 'required|string',
'privacy_version' => 'required|string',
'cookie_version' => 'required|string',
]);
$user = Auth::user();
UserConsent::create([
'user_id' => $user->id,
'terms_version' => $request->terms_version,
'privacy_version' => $request->privacy_version,
'cookie_version' => $request->cookie_version,
'accepted_at' => now(),
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
return response()->json(['success' => true]);
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\UserNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class NotificationController extends Controller
{
public function index(): \Illuminate\View\View
{
$notifications = UserNotification::where('user_id', Auth::id())
->orderByDesc('created_at')
->paginate(20);
return view('notifications.index', compact('notifications'));
}
public function markAsRead(UserNotification $notification): \Illuminate\Http\RedirectResponse
{
$this->authorizeNotification($notification);
$notification->markAsRead();
return back()->with('success', __('Notification marked as read.'));
}
public function markAllAsRead(): \Illuminate\Http\RedirectResponse
{
UserNotification::where('user_id', Auth::id())
->where('is_read', false)
->update(['is_read' => true, 'read_at' => now()]);
return back()->with('success', __('All notifications marked as read.'));
}
private function authorizeNotification(UserNotification $notification): void
{
if ($notification->user_id !== Auth::id()) {
abort(403);
}
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class ProfileController extends Controller
{
public function edit()
{
return view('profile.edit', [
'user' => Auth::user(),
]);
}
public function update(Request $request)
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255'],
'phone' => ['nullable', 'string', 'max:20'],
'locale' => ['nullable', 'string', 'max:10'],
'timezone' => ['required', 'string', 'max:50', 'timezone'],
'password' => ['nullable', 'string', 'min:8', 'confirmed'],
]);
$user = Auth::user();
$user->update($request->only('name', 'email', 'phone', 'locale', 'timezone'));
if ($request->filled('password')) {
$user->password = Hash::make($request->password);
$user->save();
}
return back()->with('success', 'Profile updated successfully.');
}
public function destroy(Request $request)
{
$request->validate([
'password' => ['required', 'current_password'],
]);
$user = Auth::user();
Auth::logout();
$user->delete();
return redirect()->route('home');
}
}
+153
View File
@@ -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
View File
@@ -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
View File
@@ -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]));
}
}