feat(cms): implement complete SysSiteCMS content management system

Initial implementation of the HomeSysPanel CMS built with Laravel 13.0.
Includes complete admin panel, content management, user authentication,
modular architecture for products/services/chatbot, and marketing tools.

Key features:
- User authentication with email verification and password reset
- Role-based access control (Admin/User with granular permissions)
- Content management (pages, news, FAQ, downloads, galleries, banners)
- Contact management system with subjects and responses
- Marketing tools (email campaigns, visitor analytics, social sharing)
- Modular architecture (Products, Services, Chatbot modules)
- AI Chatbot integration with OpenRouter.ai and LlamaCpp support
- Visitor tracking with geolocation
- SEO optimization with meta tags and Open Graph
- Responsive design with Bootstrap 5
- Dark mode support
- Maintenance mode
- Feature request system
This commit is contained in:
marco
2026-09-07 19:06:53 -03:00
commit 38295e7e88
279 changed files with 33691 additions and 0 deletions
@@ -0,0 +1,277 @@
<?php
namespace App\Modules\Chatbot\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Modules\Chatbot\Models\ChatbotKnowledge;
use App\Modules\Chatbot\Models\ChatbotRule;
use App\Modules\Chatbot\Models\ChatbotConversation;
use App\Models\Setting;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Notification;
use App\Notifications\FeatureRequestNotification;
class ChatbotController extends Controller
{
/**
* Display the knowledge base listing.
*/
public function index(): \Illuminate\View\View
{
$knowledgeEntries = ChatbotKnowledge::orderBy('category')
->orderBy('title')
->paginate(20);
$categories = ChatbotKnowledge::getCategories();
return view('chatbot::admin.index', compact('knowledgeEntries', 'categories'));
}
/**
* Show the form for creating a new knowledge entry.
*/
public function create(): \Illuminate\View\View
{
$categories = ChatbotKnowledge::getCategories();
return view('chatbot::admin.create', compact('categories'));
}
/**
* Store a newly created knowledge entry.
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
'restrictions' => 'nullable|string',
'category' => 'required|string|in:' . implode(',', array_keys(ChatbotKnowledge::getCategories())),
]);
$validated['active'] = $request->has('active');
ChatbotKnowledge::create($validated);
return redirect()
->route('admin.chatbot.index')
->with('success', 'Informação adicionada à base de conhecimento com sucesso!');
}
/**
* Show the form for editing a knowledge entry.
*/
public function edit(ChatbotKnowledge $chatbot_knowledge): \Illuminate\View\View
{
$entry = $chatbot_knowledge;
$categories = ChatbotKnowledge::getCategories();
return view('chatbot::admin.edit', compact('entry', 'categories'));
}
/**
* Update a knowledge entry.
*/
public function update(Request $request, ChatbotKnowledge $chatbot_knowledge): \Illuminate\Http\RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
'restrictions' => 'nullable|string',
'category' => 'required|string|in:' . implode(',', array_keys(ChatbotKnowledge::getCategories())),
]);
$validated['active'] = $request->has('active');
$chatbot_knowledge->update($validated);
return redirect()
->route('admin.chatbot.index')
->with('success', 'Informação atualizada com sucesso!');
}
/**
* Delete a knowledge entry.
*/
public function destroy(ChatbotKnowledge $chatbot_knowledge): \Illuminate\Http\RedirectResponse
{
$chatbot_knowledge->delete();
return redirect()
->route('admin.chatbot.index')
->with('success', 'Informação removida da base de conhecimento.');
}
/**
* Display conversations listing with visitor contacts.
*/
public function conversations(): \Illuminate\View\View
{
$conversations = ChatbotConversation::with('messages')
->whereNotNull('visitor_name')
->orderBy('created_at', 'desc')
->paginate(20);
return view('chatbot::admin.conversations', compact('conversations'));
}
/**
* Display a specific conversation with all messages.
*/
public function showConversation(ChatbotConversation $conversation): \Illuminate\View\View
{
$conversation->load('messages');
return view('chatbot::admin.conversation-show', compact('conversation'));
}
/**
* Delete a conversation.
*/
public function destroyConversation(ChatbotConversation $conversation): \Illuminate\Http\RedirectResponse
{
$conversation->delete();
return redirect()
->route('admin.chatbot.conversations')
->with('success', 'Conversa removida com sucesso.');
}
/**
* Display chatbot settings.
*/
public function settings(): \Illuminate\View\View
{
$settings = [
'chatbot_enabled' => Setting::get('chatbot_enabled', 'off'),
'chatbot_provider' => Setting::get('chatbot_provider', 'openrouter'),
'chatbot_openrouter_api_key' => Setting::get('chatbot_openrouter_api_key'),
'chatbot_openrouter_model' => Setting::get('chatbot_openrouter_model', 'openai/gpt-3.5-turbo'),
'chatbot_llamacpp_base_url' => Setting::get('chatbot_llamacpp_base_url', 'http://172.17.0.1:8080'),
'chatbot_llamacpp_model' => Setting::get('chatbot_llamacpp_model', ''),
'chatbot_welcome_message' => Setting::get('chatbot_welcome_message', 'Olá! Como posso ajudar você hoje?'),
'chatbot_custom_instructions' => Setting::get('chatbot_custom_instructions'),
'chatbot_max_tokens' => Setting::get('chatbot_max_tokens', '500'),
'chatbot_temperature' => Setting::get('chatbot_temperature', '0.7'),
];
return view('chatbot::admin.settings', compact('settings'));
}
/**
* Update chatbot settings.
*/
public function updateSettings(Request $request): \Illuminate\Http\RedirectResponse
{
$validated = $request->validate([
'chatbot_enabled' => 'nullable|in:on,off',
'chatbot_provider' => 'required|in:openrouter,llamacpp',
'chatbot_openrouter_api_key' => 'nullable|string|max:255',
'chatbot_openrouter_model' => 'nullable|string|max:255',
'chatbot_llamacpp_base_url' => 'nullable|string|max:255',
'chatbot_llamacpp_model' => 'nullable|string|max:255',
'chatbot_welcome_message' => 'required|string|max:500',
'chatbot_custom_instructions' => 'nullable|string',
'chatbot_max_tokens' => 'required|integer|min:50|max:2000',
'chatbot_temperature' => 'required|numeric|min:0|max:2',
]);
foreach ($validated as $key => $value) {
if ($key === 'chatbot_enabled') {
Setting::set($key, $value ?? 'off', 'chatbot');
} else {
Setting::set($key, $value, 'chatbot');
}
}
// Ensure enabled is set properly when checkbox is not checked
if (!$request->has('chatbot_enabled')) {
Setting::set('chatbot_enabled', 'off', 'chatbot');
}
return redirect()
->route('admin.chatbot.settings')
->with('success', 'Configurações do chatbot atualizadas com sucesso!');
}
/**
* Display the chatbot rules listing.
*/
public function rulesIndex(): \Illuminate\View\View
{
$rules = ChatbotRule::orderBy('order')
->orderBy('id')
->paginate(30);
return view('chatbot::admin.rules.index', compact('rules'));
}
/**
* Show the form for creating a new chatbot rule.
*/
public function rulesCreate(): \Illuminate\View\View
{
return view('chatbot::admin.rules.create');
}
/**
* Store a newly created chatbot rule.
*/
public function rulesStore(Request $request): \Illuminate\Http\RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string|max:2000',
'order' => 'nullable|integer|min:0|max:9999',
]);
$validated['active'] = $request->has('active');
$validated['order'] = $validated['order'] ?? 0;
ChatbotRule::create($validated);
return redirect()
->route('admin.chatbot.rules.index')
->with('success', 'Regra criada com sucesso! A IA passará a segui-la nas próximas conversas.');
}
/**
* Show the form for editing a chatbot rule.
*/
public function rulesEdit(ChatbotRule $rule): \Illuminate\View\View
{
return view('chatbot::admin.rules.edit', compact('rule'));
}
/**
* Update a chatbot rule.
*/
public function rulesUpdate(Request $request, ChatbotRule $rule): \Illuminate\Http\RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string|max:2000',
'order' => 'nullable|integer|min:0|max:9999',
]);
$validated['active'] = $request->has('active');
$validated['order'] = $validated['order'] ?? 0;
$rule->update($validated);
return redirect()
->route('admin.chatbot.rules.index')
->with('success', 'Regra atualizada com sucesso!');
}
/**
* Delete a chatbot rule.
*/
public function rulesDestroy(ChatbotRule $rule): \Illuminate\Http\RedirectResponse
{
$rule->delete();
return redirect()
->route('admin.chatbot.rules.index')
->with('success', 'Regra removida com sucesso.');
}
}
@@ -0,0 +1,259 @@
<?php
namespace App\Modules\Chatbot\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use App\Models\User;
use App\Modules\Chatbot\Models\ChatbotConversation;
use App\Modules\Chatbot\Models\ChatbotMessage;
use App\Modules\Chatbot\Notifications\ChatbotConversationNotification;
use App\Modules\Chatbot\Services\OpenRouterService;
use App\Modules\Chatbot\Services\LlamaCppService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Validator;
class ChatbotApiController extends Controller
{
/**
* Register visitor information before starting the chat.
*/
public function registerVisitor(Request $request): JsonResponse
{
// Check if chatbot is enabled
if (Setting::get('chatbot_enabled', 'off') !== 'on') {
return response()->json([
'success' => false,
'message' => 'O assistente virtual está indisponível no momento.',
], 403);
}
$validator = Validator::make($request->all(), [
'session_id' => 'required|string|max:255|regex:/^[a-zA-Z0-9\-_]+$/',
'name' => 'required|string|max:255',
'phone' => 'required|string|max:30',
'city' => 'required|string|max:255',
], [
'session_id.regex' => 'ID de sessão inválido.',
'name.required' => 'Por favor, informe seu nome.',
'phone.required' => 'Por favor, informe seu telefone.',
'city.required' => 'Por favor, informe sua cidade.',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Preencha todos os campos obrigatórios.',
'errors' => $validator->errors(),
], 422);
}
$sessionId = $request->input('session_id');
// Get or create conversation and save visitor info
$conversation = ChatbotConversation::getOrCreateBySession($sessionId);
$conversation->update([
'visitor_name' => trim($request->input('name')),
'visitor_phone' => trim($request->input('phone')),
'visitor_city' => trim($request->input('city')),
]);
// Send notification to admins
$adminTypes = \App\Models\UserType::where('is_admin', true)->pluck('name')->toArray();
$admins = User::whereIn('user_type', $adminTypes)->get();
if ($admins->isNotEmpty()) {
Notification::send($admins, new ChatbotConversationNotification($conversation));
}
return response()->json([
'success' => true,
'message' => 'Dados registrados com sucesso.',
]);
}
/**
* Handle an incoming chat message from the public widget.
*/
public function sendMessage(Request $request): JsonResponse
{
// Check if chatbot is enabled
if (Setting::get('chatbot_enabled', 'off') !== 'on') {
return response()->json([
'success' => false,
'message' => 'O assistente virtual está indisponível no momento.',
], 403);
}
$validator = Validator::make($request->all(), [
'message' => 'required|string|max:1000',
'session_id' => 'required|string|max:255|regex:/^[a-zA-Z0-9\-_]+$/',
], [
'session_id.regex' => 'ID de sessão inválido.',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Mensagem inválida.',
'errors' => $validator->errors(),
], 422);
}
$sessionId = $request->input('session_id');
$userMessage = trim($request->input('message'));
// Get or create conversation
$conversation = ChatbotConversation::getOrCreateBySession($sessionId);
// Check if conversation is ended
if ($conversation->isEnded()) {
return response()->json([
'success' => false,
'message' => 'Esta conversa foi finalizada. Inicie uma nova conversa.',
'conversation_ended' => true,
], 403);
}
// Save user message
ChatbotMessage::create([
'conversation_id' => $conversation->id,
'role' => 'user',
'content' => $userMessage,
]);
// Get conversation context
$context = $conversation->getContextForApi(20);
// Call the configured AI provider (OpenRouter or llama.cpp)
$provider = Setting::get('chatbot_provider', 'openrouter');
if ($provider === 'llamacpp') {
$aiService = new LlamaCppService();
} else {
$aiService = new OpenRouterService();
}
$result = $aiService->chat($context, $conversation);
// Save assistant message with token usage
ChatbotMessage::create([
'conversation_id' => $conversation->id,
'role' => 'assistant',
'content' => $result['content'],
'tokens_used' => $result['tokens_used'],
]);
return response()->json([
'success' => true,
'message' => $result['content'],
]);
}
/**
* End a conversation.
*/
public function endConversation(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'session_id' => 'required|string|max:255|regex:/^[a-zA-Z0-9\-_]+$/',
], [
'session_id.regex' => 'ID de sessão inválido.',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Sessão inválida.',
], 422);
}
$sessionId = $request->input('session_id');
$conversation = ChatbotConversation::where('session_id', $sessionId)->first();
if (!$conversation) {
return response()->json([
'success' => false,
'message' => 'Conversa não encontrada.',
], 404);
}
if ($conversation->isEnded()) {
return response()->json([
'success' => true,
'message' => 'Conversa já foi finalizada.',
]);
}
$conversation->update([
'status' => 'ended',
'ended_at' => now(),
]);
return response()->json([
'success' => true,
'message' => 'Conversa finalizada com sucesso.',
]);
}
/**
* Submit a rating for a conversation.
*/
public function rateConversation(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'session_id' => 'required|string|max:255|regex:/^[a-zA-Z0-9\-_]+$/',
'rating' => 'required|integer|min:1|max:5',
'comment' => 'nullable|string|max:500',
], [
'session_id.regex' => 'ID de sessão inválido.',
'rating.required' => 'Por favor, selecione uma avaliação.',
'rating.min' => 'A avaliação mínima é 1 estrela.',
'rating.max' => 'A avaliação máxima é 5 estrelas.',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Avaliação inválida.',
'errors' => $validator->errors(),
], 422);
}
$sessionId = $request->input('session_id');
$conversation = ChatbotConversation::where('session_id', $sessionId)->first();
if (!$conversation) {
return response()->json([
'success' => false,
'message' => 'Conversa não encontrada.',
], 404);
}
$conversation->update([
'rating' => $request->input('rating'),
'rating_comment' => $request->input('comment'),
]);
return response()->json([
'success' => true,
'message' => 'Avaliação registrada. Obrigado pelo feedback!',
]);
}
/**
* Get the initial welcome message and chatbot status.
*/
public function getStatus(): JsonResponse
{
$enabled = Setting::get('chatbot_enabled', 'off') === 'on';
$welcomeMessage = Setting::get('chatbot_welcome_message', 'Olá! Como posso ajudar você hoje?');
return response()->json([
'enabled' => $enabled,
'welcome_message' => $welcomeMessage,
]);
}
}