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
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Banner;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class BannerController extends Controller
{
public function index()
{
$banners = Banner::orderBy('order')->paginate(10);
return view('admin.banners.index', compact('banners'));
}
public function create()
{
return view('admin.banners.create');
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'image' => 'required|image|max:2048|mimes:jpeg,png,jpg,gif,webp',
'link' => 'nullable|string|max:500|regex:/^https?:\/\/.+\..+$/i',
'button_text' => 'nullable|string|max:50',
'order' => 'required|integer',
'active' => 'required|boolean',
], [
'image.mimes' => 'A imagem deve ser do tipo: JPEG, PNG, JPG, GIF ou WebP.',
'link.regex' => 'O link deve começar com http:// ou https:// e ter um formato válido.',
]);
$data = $request->all();
if ($request->hasFile('image')) {
$data['image'] = $request->file('image')->store('banners', 'public');
}
Banner::create($data);
return redirect()->route('admin.marketing.banners.index')->with('success', 'Banner criado com sucesso.');
}
public function edit(Banner $banner)
{
return view('admin.banners.edit', compact('banner'));
}
public function update(Request $request, Banner $banner)
{
$request->validate([
'title' => 'required|string|max:255',
'image' => 'nullable|image|max:2048|mimes:jpeg,png,jpg,gif,webp',
'link' => 'nullable|string|max:500|regex:/^https?:\/\/.+\..+$/i',
'button_text' => 'nullable|string|max:50',
'order' => 'required|integer',
'active' => 'required|boolean',
], [
'image.mimes' => 'A imagem deve ser do tipo: JPEG, PNG, JPG, GIF ou WebP.',
'link.regex' => 'O link deve começar com http:// ou https:// e ter um formato válido.',
]);
$data = $request->all();
if ($request->hasFile('image')) {
if ($banner->image) {
Storage::disk('public')->delete($banner->image);
}
$data['image'] = $request->file('image')->store('banners', 'public');
}
$banner->update($data);
return redirect()->route('admin.marketing.banners.index')->with('success', 'Banner atualizado com sucesso.');
}
public function destroy(Banner $banner)
{
if ($banner->image) {
Storage::disk('public')->delete($banner->image);
}
$banner->delete();
return redirect()->route('admin.marketing.banners.index')->with('success', 'Banner excluído com sucesso.');
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ContactMessage;
use App\Models\ContactResponse;
use App\Mail\ContactReplyMail;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Auth;
class ContactController extends Controller
{
public function index()
{
$contacts = ContactMessage::with('subject')->latest()->paginate(10);
return view('admin.contacts.index', compact('contacts'));
}
public function show(ContactMessage $contact)
{
$contact->update(['read' => true]);
$contact->load('responses.user', 'subject');
return view('admin.contacts.show', compact('contact'));
}
public function reply(Request $request, ContactMessage $contact)
{
$request->validate([
'message' => 'required|string|min:10',
]);
// 1. Salvar no Banco de Dados
ContactResponse::create([
'contact_message_id' => $contact->id,
'user_id' => Auth::id(),
'message' => $request->message,
]);
// 2. Enviar E-mail
Mail::to($contact->email)->send(new ContactReplyMail($contact, $request->message));
return redirect()->route('admin.contacts.show', $contact)->with('success', 'Resposta enviada e registrada com sucesso!');
}
public function destroy(ContactMessage $contact)
{
$contact->delete();
return redirect()->route('admin.contacts.index')->with('success', 'Mensagem excluída com sucesso.');
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Download;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class DownloadController extends Controller
{
public function index()
{
$downloads = Download::latest()->paginate(10);
return view('admin.downloads.index', compact('downloads'));
}
public function create()
{
return view('admin.downloads.create');
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'file' => 'required|file|max:20480|mimes:pdf,doc,docx,zip,rar,txt,xls,xlsx', // Máx 20MB
'file_type' => 'required|string|in:Driver,Manual,Software,Outro',
], [
'file.mimes' => 'O arquivo deve ser do tipo: PDF, DOC, DOCX, ZIP, RAR, TXT, XLS ou XLSX.',
]);
$path = $request->file('file')->store('downloads', 'public');
Download::create([
'title' => $request->title,
'description' => $request->description,
'file_path' => $path,
'file_type' => $request->file_type,
'active' => true,
]);
return redirect()->route('admin.downloads.index')->with('success', 'Download cadastrado com sucesso.');
}
public function edit(Download $download)
{
return view('admin.downloads.edit', compact('download'));
}
public function update(Request $request, Download $download)
{
$request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'file' => 'nullable|file|max:20480|mimes:pdf,doc,docx,zip,rar,txt,xls,xlsx',
'file_type' => 'required|string|in:Driver,Manual,Software,Outro',
'active' => 'required|boolean',
], [
'file.mimes' => 'O arquivo deve ser do tipo: PDF, DOC, DOCX, ZIP, RAR, TXT, XLS ou XLSX.',
]);
$data = [
'title' => $request->title,
'description' => $request->description,
'file_type' => $request->file_type,
'active' => $request->active,
];
if ($request->hasFile('file')) {
// Remove o arquivo antigo
Storage::disk('public')->delete($download->file_path);
$data['file_path'] = $request->file('file')->store('downloads', 'public');
}
$download->update($data);
return redirect()->route('admin.downloads.index')->with('success', 'Download atualizado com sucesso.');
}
public function destroy(Download $download)
{
Storage::disk('public')->delete($download->file_path);
$download->delete();
return redirect()->route('admin.downloads.index')->with('success', 'Download excluído com sucesso.');
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Faq;
use Illuminate\Http\Request;
class FaqController extends Controller
{
public function index()
{
$faqs = Faq::orderBy('order')->latest()->paginate(10);
return view('admin.faqs.index', compact('faqs'));
}
public function create()
{
return view('admin.faqs.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'question' => 'required|string|max:255',
'answer' => 'required|string',
'order' => 'required|integer',
]);
$validated['active'] = true;
Faq::create($validated);
return redirect()->route('admin.faqs.index')->with('success', 'FAQ criado com sucesso.');
}
public function edit(Faq $faq)
{
return view('admin.faqs.edit', compact('faq'));
}
public function update(Request $request, Faq $faq)
{
$validated = $request->validate([
'question' => 'required|string|max:255',
'answer' => 'required|string',
'order' => 'required|integer',
'active' => 'required|boolean',
]);
$faq->update($validated);
return redirect()->route('admin.faqs.index')->with('success', 'FAQ atualizado com sucesso.');
}
public function destroy(Faq $faq)
{
$faq->delete();
return redirect()->route('admin.faqs.index')->with('success', 'FAQ excluído com sucesso.');
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\GalleryCategory;
use Illuminate\Http\Request;
class GalleryCategoryController extends Controller
{
public function index()
{
$categories = GalleryCategory::withCount('galleries')->latest()->paginate(10);
return view('admin.gallery-categories.index', compact('categories'));
}
public function create()
{
return view('admin.gallery-categories.create');
}
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'slug' => 'required|string|max:255|unique:gallery_categories',
'description' => 'nullable|string',
]);
GalleryCategory::create([
'name' => $request->name,
'slug' => $request->slug,
'description' => $request->description,
'active' => true,
]);
return redirect()->route('admin.gallery-categories.index')->with('success', 'Categoria criada com sucesso.');
}
public function edit(GalleryCategory $galleryCategory)
{
return view('admin.gallery-categories.edit', compact('galleryCategory'));
}
public function update(Request $request, GalleryCategory $galleryCategory)
{
$request->validate([
'name' => 'required|string|max:255',
'slug' => 'required|string|max:255|unique:gallery_categories,slug,' . $galleryCategory->id,
'description' => 'nullable|string',
'active' => 'required|boolean',
]);
$galleryCategory->update($request->all());
return redirect()->route('admin.gallery-categories.index')->with('success', 'Categoria atualizada com sucesso.');
}
public function destroy(GalleryCategory $galleryCategory)
{
$galleryCategory->delete();
return redirect()->route('admin.gallery-categories.index')->with('success', 'Categoria excluída com sucesso.');
}
}
+151
View File
@@ -0,0 +1,151 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Gallery;
use App\Models\GalleryCategory;
use App\Models\GalleryImage;
use App\Models\User;
use App\Notifications\GalleryActionNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
class GalleryController extends Controller
{
public function index()
{
$galleries = Gallery::withCount('images')->latest()->paginate(10);
return view('admin.galleries.index', compact('galleries'));
}
public function create()
{
$categories = GalleryCategory::where('active', true)->orderBy('name')->get();
return view('admin.galleries.create', compact('categories'));
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'slug' => 'required|string|max:255|unique:galleries',
'description' => 'nullable|string',
'category_id' => 'nullable|exists:gallery_categories,id',
]);
$gallery = Gallery::create([
'title' => $request->title,
'slug' => $request->slug,
'description' => $request->description,
'category_id' => $request->category_id,
'active' => true,
]);
/** @var \App\Models\User $user */
$user = Auth::user();
$admins = User::where('user_type', 'Admin')->where('id', '!=', $user->id)->get();
Notification::send($admins, new GalleryActionNotification($gallery, $user, 'created'));
return redirect()->route('admin.galleries.index')->with('success', 'Galeria criada com sucesso.');
}
public function edit(Gallery $gallery)
{
$gallery->load('images');
$categories = GalleryCategory::where('active', true)->orderBy('name')->get();
return view('admin.galleries.edit', compact('gallery', 'categories'));
}
public function update(Request $request, Gallery $gallery)
{
$request->validate([
'title' => 'required|string|max:255',
'slug' => 'required|string|max:255|unique:galleries,slug,' . $gallery->id,
'description' => 'nullable|string',
'active' => 'required|boolean',
'category_id' => 'nullable|exists:gallery_categories,id',
]);
$gallery->update($request->all());
/** @var \App\Models\User $user */
$user = Auth::user();
$admins = User::where('user_type', 'Admin')->where('id', '!=', $user->id)->get();
Notification::send($admins, new GalleryActionNotification($gallery, $user, 'updated'));
return redirect()->route('admin.galleries.index')->with('success', 'Galeria atualizada com sucesso.');
}
public function destroy(Gallery $gallery)
{
foreach ($gallery->images as $image) {
Storage::disk('public')->delete($image->file_path);
}
$gallery->delete();
return redirect()->route('admin.galleries.index')->with('success', 'Galeria e suas imagens excluídas.');
}
// Gerenciamento de Imagens
public function uploadImage(Request $request, Gallery $gallery)
{
$request->validate([
'images' => 'required|array|min:1|max:20',
'images.*' => 'image|max:5120|mimes:jpeg,png,jpg,gif,webp', // 5MB each
], [
'images.max' => 'Máximo de 20 imagens permitidas.',
'images.*.mimes' => 'As imagens devem ser do tipo: JPEG, PNG, JPG, GIF ou WebP.',
]);
$order = $gallery->images()->count() + 1;
$count = 0;
foreach ($request->file('images') as $file) {
$path = $file->store('galleries/' . $gallery->id, 'public');
GalleryImage::create([
'gallery_id' => $gallery->id,
'file_path' => $path,
'order' => $order++,
]);
$count++;
}
/** @var \App\Models\User $user */
$user = Auth::user();
$admins = User::where('user_type', 'Admin')->where('id', '!=', $user->id)->get();
Notification::send($admins, new GalleryActionNotification($gallery, $user, 'photo_added'));
$message = $count === 1 ? 'Imagem adicionada à galeria.' : "{$count} imagens adicionadas à galeria.";
return redirect()->back()->with('success', $message);
}
public function updateImage(Request $request, GalleryImage $image)
{
$request->validate([
'description' => 'nullable|string|max:255',
'order' => 'required|integer',
]);
$image->update($request->only('description', 'order'));
return redirect()->back()->with('success', 'Imagem atualizada.');
}
public function deleteImage(GalleryImage $image)
{
$gallery = $image->gallery;
Storage::disk('public')->delete($image->file_path);
$image->delete();
/** @var \App\Models\User $user */
$user = Auth::user();
$admins = User::where('user_type', 'Admin')->where('id', '!=', $user->id)->get();
Notification::send($admins, new GalleryActionNotification($gallery, $user, 'photo_deleted'));
return redirect()->back()->with('success', 'Imagem removida.');
}
}
+138
View File
@@ -0,0 +1,138 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Setting;
use App\Models\VisitorLog;
use App\Mail\PromotionMail;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
class MarketingController extends Controller
{
public function index()
{
return view('admin.marketing.index');
}
public function promoteProducts()
{
if (!module_enabled('products') || !class_exists(\App\Modules\Products\Models\Product::class)) {
return redirect()->route('admin.marketing.index')->with('error', 'O módulo de Produtos está desativado.');
}
$products = \App\Modules\Products\Models\Product::where('active', true)->with('category')->get();
return view('admin.marketing.promote_products', compact('products'));
}
public function promoteServices()
{
if (!module_enabled('services') || !class_exists(\App\Modules\Services\Models\Service::class)) {
return redirect()->route('admin.marketing.index')->with('error', 'O módulo de Serviços está desativado.');
}
$services = \App\Modules\Services\Models\Service::where('active', true)->get();
return view('admin.marketing.promote_services', compact('services'));
}
public function analytics()
{
// Grouping by Country
$countries = VisitorLog::select('country', DB::raw('count(*) as total'))
->groupBy('country')
->orderBy('total', 'desc')
->limit(10)
->get();
// Grouping by City
$cities = VisitorLog::select('city', 'region', DB::raw('count(*) as total'))
->groupBy('city', 'region')
->orderBy('total', 'desc')
->limit(10)
->get();
// Data for last 7 days access chart
$last7Days = collect(range(6, 0))->map(function ($days) {
$date = now()->subDays($days)->format('Y-m-d');
return [
'date' => now()->subDays($days)->format('d/m'),
'total' => VisitorLog::whereDate('created_at', $date)->count()
];
});
return view('admin.marketing.analytics', compact('countries', 'cities', 'last7Days'));
}
public function emailMarketing()
{
$products = collect();
$services = collect();
if (module_enabled('products') && class_exists(\App\Modules\Products\Models\Product::class)) {
$products = \App\Modules\Products\Models\Product::where('active', true)->get();
}
if (module_enabled('services') && class_exists(\App\Modules\Services\Models\Service::class)) {
$services = \App\Modules\Services\Models\Service::where('active', true)->get();
}
$userCount = User::where('user_type', 'User')->count();
return view('admin.marketing.email', compact('products', 'services', 'userCount'));
}
public function sendEmailMarketing(Request $request)
{
$request->validate([
'subject' => 'required|string|max:255',
'message' => 'required|string',
'button_text' => 'nullable|string|max:50',
'button_url' => 'nullable|string|max:500|regex:/^https?:\/\/.+\..+$/i',
], [
'button_url.regex' => 'O link do botão deve começar com http:// ou https:// e ter um formato válido.',
]);
$users = User::where('user_type', 'User')->get();
$company = (object)[
'name' => Setting::get('company_name', config('app.name')),
];
$productImage = null;
if ($request->product_id && module_enabled('products') && class_exists(\App\Modules\Products\Models\Product::class)) {
$product = \App\Modules\Products\Models\Product::find($request->product_id);
if ($product && $product->image) {
$productImage = asset('storage/' . $product->image);
}
} elseif ($request->service_id && module_enabled('services') && class_exists(\App\Modules\Services\Models\Service::class)) {
$service = \App\Modules\Services\Models\Service::find($request->service_id);
if ($service && $service->image) {
$productImage = asset('storage/' . $service->image);
}
}
foreach ($users as $user) {
// Check if user wants to receive emails (preference)
$prefs = $user->preferences ?? [];
if ($prefs['notifications_email'] ?? true) {
try {
Mail::to($user->email)->send(new PromotionMail(
$request->subject,
$request->message,
$request->button_text,
$request->button_url,
$productImage,
$company
));
} catch (\Exception $e) {
Log::error("Erro ao enviar e-mail de marketing para {$user->email}: " . $e->getMessage());
}
}
}
return redirect()->route('admin.marketing.email')->with('success', 'E-mail de marketing enviado para ' . $users->count() . ' clientes!');
}
}
+203
View File
@@ -0,0 +1,203 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\News;
use App\Models\NewsImage;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\NewsActionNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
class NewsController extends Controller
{
public function index()
{
$newsList = News::withCount('images')->latest()->paginate(10);
$newsEnabled = Setting::get('news_enabled', 'on') === 'on';
return view('admin.news.index', compact('newsList', 'newsEnabled'));
}
public function toggleVisibility()
{
$current = Setting::get('news_enabled', 'on');
$newValue = $current === 'on' ? 'off' : 'on';
Setting::set('news_enabled', $newValue, 'news');
$status = $newValue === 'on' ? 'ativado' : 'desativado';
return redirect()->route('admin.news.index')->with('success', "Módulo de Notícias {$status} com sucesso.");
}
public function create()
{
return view('admin.news.create');
}
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'slug' => 'required|string|max:255|unique:news',
'summary' => 'nullable|string|max:500',
'content' => 'nullable|string',
'author' => 'nullable|string|max:255',
'featured_image' => 'nullable|image|max:5120|mimes:jpeg,png,jpg,gif,webp',
'published_at' => 'nullable|date',
'seo_title' => 'nullable|string|max:255',
'seo_description' => 'nullable|string|max:500',
'seo_keywords' => 'nullable|string|max:255',
], [
'featured_image.mimes' => 'A imagem deve ser do tipo: JPEG, PNG, JPG, GIF ou WebP.',
]);
$data = $request->except('featured_image');
$data['active'] = true;
$data['user_id'] = Auth::id();
if ($request->hasFile('featured_image')) {
$data['featured_image'] = $request->file('featured_image')->store('news/featured', 'public');
}
$news = News::create($data);
/** @var \App\Models\User $user */
$user = Auth::user();
$admins = User::where('user_type', 'Admin')->where('id', '!=', $user->id)->get();
Notification::send($admins, new NewsActionNotification($news, $user, 'created'));
return redirect()->route('admin.news.index')->with('success', 'Notícia criada com sucesso.');
}
public function edit(News $news)
{
$news->load('images');
return view('admin.news.edit', compact('news'));
}
public function update(Request $request, News $news)
{
$request->validate([
'title' => 'required|string|max:255',
'slug' => 'required|string|max:255|unique:news,slug,' . $news->id,
'summary' => 'nullable|string|max:500',
'content' => 'nullable|string',
'author' => 'nullable|string|max:255',
'featured_image' => 'nullable|image|max:5120',
'active' => 'required|boolean',
'published_at' => 'nullable|date',
'seo_title' => 'nullable|string|max:255',
'seo_description' => 'nullable|string|max:500',
'seo_keywords' => 'nullable|string|max:255',
]);
$data = $request->except('featured_image');
if ($request->hasFile('featured_image')) {
// Delete old featured image
if ($news->featured_image) {
Storage::disk('public')->delete($news->featured_image);
}
$data['featured_image'] = $request->file('featured_image')->store('news/featured', 'public');
}
$news->update($data);
/** @var \App\Models\User $user */
$user = Auth::user();
$admins = User::where('user_type', 'Admin')->where('id', '!=', $user->id)->get();
Notification::send($admins, new NewsActionNotification($news, $user, 'updated'));
return redirect()->route('admin.news.index')->with('success', 'Notícia atualizada com sucesso.');
}
public function destroy(News $news)
{
// Delete featured image
if ($news->featured_image) {
Storage::disk('public')->delete($news->featured_image);
}
// Delete all additional images
foreach ($news->images as $image) {
Storage::disk('public')->delete($image->file_path);
}
$news->delete();
return redirect()->route('admin.news.index')->with('success', 'Notícia excluída com sucesso.');
}
// Featured Image Management
public function removeFeaturedImage(News $news)
{
if ($news->featured_image) {
Storage::disk('public')->delete($news->featured_image);
$news->update(['featured_image' => null]);
}
return redirect()->back()->with('success', 'Imagem principal removida.');
}
// Additional Images Management
public function uploadImage(Request $request, News $news)
{
$request->validate([
'images' => 'required|array|min:1|max:10',
'images.*' => 'image|max:5120|mimes:jpeg,png,jpg,gif,webp', // 5MB each
], [
'images.max' => 'Máximo de 10 imagens permitidas.',
'images.*.mimes' => 'As imagens devem ser do tipo: JPEG, PNG, JPG, GIF ou WebP.',
]);
$order = $news->images()->count() + 1;
$count = 0;
foreach ($request->file('images') as $file) {
$path = $file->store('news/' . $news->id, 'public');
NewsImage::create([
'news_id' => $news->id,
'file_path' => $path,
'order' => $order++,
]);
$count++;
}
/** @var \App\Models\User $user */
$user = Auth::user();
$admins = User::where('user_type', 'Admin')->where('id', '!=', $user->id)->get();
Notification::send($admins, new NewsActionNotification($news, $user, 'photo_added'));
$message = $count === 1 ? 'Imagem adicionada à notícia.' : "{$count} imagens adicionadas à notícia.";
return redirect()->back()->with('success', $message);
}
public function updateImage(Request $request, NewsImage $image)
{
$request->validate([
'description' => 'nullable|string|max:255',
'order' => 'required|integer',
]);
$image->update($request->only('description', 'order'));
return redirect()->back()->with('success', 'Imagem atualizada.');
}
public function deleteImage(NewsImage $image)
{
$news = $image->news;
Storage::disk('public')->delete($image->file_path);
$image->delete();
/** @var \App\Models\User $user */
$user = Auth::user();
$admins = User::where('user_type', 'Admin')->where('id', '!=', $user->id)->get();
Notification::send($admins, new NewsActionNotification($news, $user, 'photo_deleted'));
return redirect()->back()->with('success', 'Imagem removida.');
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Page;
use App\Models\User;
use App\Notifications\PageActionNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
class PageController extends Controller
{
public function index()
{
$pages = Page::latest()->paginate(10);
return view('admin.pages.index', compact('pages'));
}
public function create()
{
return view('admin.pages.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'slug' => 'required|string|max:255|unique:pages',
'content' => 'nullable|string',
'meta_description' => 'nullable|string|max:255',
'meta_keywords' => 'nullable|string|max:255',
'og_title' => 'nullable|string|max:255',
'og_description' => 'nullable|string|max:255',
'og_image' => 'nullable|string|max:255',
'canonical_url' => 'nullable|string|max:255',
'active' => 'required|boolean',
]);
$page = Page::create($validated);
/** @var \App\Models\User $user */
$user = Auth::user();
// Notificar outros administradores sobre a nova página
$admins = User::where('user_type', 'Admin')->where('id', '!=', $user->id)->get();
Notification::send($admins, new PageActionNotification($page, $user, 'created'));
return redirect()->route('admin.pages.index')->with('success', 'Página criada com sucesso.');
}
public function edit(Page $page)
{
return view('admin.pages.edit', compact('page'));
}
public function update(Request $request, Page $page)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'slug' => 'required|string|max:255|unique:pages,slug,' . $page->id,
'content' => 'nullable|string',
'meta_description' => 'nullable|string|max:255',
'meta_keywords' => 'nullable|string|max:255',
'og_title' => 'nullable|string|max:255',
'og_description' => 'nullable|string|max:255',
'og_image' => 'nullable|string|max:255',
'canonical_url' => 'nullable|string|max:255',
'active' => 'required|boolean',
]);
$page->update($validated);
/** @var \App\Models\User $user */
$user = Auth::user();
// Notificar outros administradores sobre a atualização
$admins = User::where('user_type', 'Admin')->where('id', '!=', $user->id)->get();
Notification::send($admins, new PageActionNotification($page, $user, 'updated'));
return redirect()->route('admin.pages.index')->with('success', 'Página atualizada com sucesso.');
}
public function destroy(Page $page)
{
$page->delete();
return redirect()->route('admin.pages.index')->with('success', 'Página excluída com sucesso.');
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
class SettingController extends Controller
{
public function index()
{
$settings = Setting::all()->groupBy('group');
return view('admin.settings.index', compact('settings'));
}
public function update(Request $request)
{
$settings = $request->except('_token');
foreach ($settings as $key => $value) {
$group = 'general';
if (str_starts_with($key, 'seo_')) $group = 'seo';
if (str_starts_with($key, 'scripts_')) $group = 'scripts';
if (str_starts_with($key, 'company_')) $group = 'company';
if (str_starts_with($key, 'dashboard_')) $group = 'dashboard';
if (str_starts_with($key, 'marketing_')) $group = 'marketing';
Setting::set($key, $value, $group);
}
// Limpa o cache para garantir que as novas configurações sejam aplicadas
Artisan::call('cache:clear');
Artisan::call('view:clear');
return redirect()->back()->with('success', 'Configurações globais atualizadas com sucesso.');
}
public function erpAd()
{
return view('admin.erp.ad');
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ContactSubject;
use Illuminate\Http\Request;
class SubjectController extends Controller
{
public function index()
{
$subjects = ContactSubject::latest()->paginate(10);
return view('admin.subjects.index', compact('subjects'));
}
public function create()
{
return view('admin.subjects.create');
}
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255|unique:contact_subjects',
]);
ContactSubject::create($request->all());
return redirect()->route('admin.subjects.index')->with('success', 'Assunto criado com sucesso.');
}
public function edit(ContactSubject $subject)
{
return view('admin.subjects.edit', compact('subject'));
}
public function update(Request $request, ContactSubject $subject)
{
$request->validate([
'name' => 'required|string|max:255|unique:contact_subjects,name,' . $subject->id,
'active' => 'required|boolean',
]);
$subject->update($request->all());
return redirect()->route('admin.subjects.index')->with('success', 'Assunto atualizado com sucesso.');
}
public function destroy(ContactSubject $subject)
{
if ($subject->messages()->count() > 0) {
return redirect()->route('admin.subjects.index')->with('error', 'Não é possível excluir um assunto que possui mensagens vinculadas.');
}
$subject->delete();
return redirect()->route('admin.subjects.index')->with('success', 'Assunto excluído com sucesso.');
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\UserType;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function index()
{
$users = User::latest()->paginate(10);
$availablePermissions = User::getAvailablePermissions();
$userTypes = UserType::getAllForSelect();
return view('admin.users.index', compact('users', 'availablePermissions', 'userTypes'));
}
public function create()
{
$availablePermissions = User::getAvailablePermissions();
$userTypes = UserType::getAllForSelect();
return view('admin.users.create', compact('availablePermissions', 'userTypes'));
}
public function store(Request $request)
{
$validTypes = UserType::pluck('name')->toArray();
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8',
'user_type' => 'required|string|in:' . implode(',', $validTypes),
'company' => 'nullable|string|max:255',
'website' => 'nullable|string|max:255|regex:/^https?:\/\/.+\..+$/i',
'whatsapp' => 'nullable|string|max:20',
'permissions' => 'nullable|array',
'permissions.*' => 'string|in:' . implode(',', array_keys(User::getAvailablePermissions())),
], [
'website.regex' => 'O site deve começar com http:// ou https:// e ter um formato válido.',
]);
$data = $request->all();
$data['password'] = \Illuminate\Support\Facades\Hash::make($request->password);
$data['email_verified_at'] = now();
// Handle permissions: admin types get null (all access), others get selected permissions
$selectedType = UserType::where('name', $data['user_type'])->first();
if ($selectedType && $selectedType->is_admin) {
$data['permissions'] = null;
} else {
$data['permissions'] = $request->has('permissions') ? $request->input('permissions') : [];
}
User::create($data);
return redirect()->route('admin.users.index')->with('success', 'Usuário criado com sucesso.');
}
public function edit(User $user)
{
$availablePermissions = User::getAvailablePermissions();
$userTypes = UserType::getAllForSelect();
return view('admin.users.edit', compact('user', 'availablePermissions', 'userTypes'));
}
public function update(Request $request, User $user)
{
$validTypes = UserType::pluck('name')->toArray();
$request->validate([
'name' => 'required|string|max:255',
'user_type' => 'required|string|in:' . implode(',', $validTypes),
'company' => 'nullable|string|max:255',
'website' => 'nullable|string|max:255|regex:/^https?:\/\/.+\..+$/i',
'whatsapp' => 'nullable|string|max:20',
'password' => 'nullable|string|min:8|confirmed',
'permissions' => 'nullable|array',
'permissions.*' => 'string|in:' . implode(',', array_keys(User::getAvailablePermissions())),
], [
'website.regex' => 'O site deve começar com http:// ou https:// e ter um formato válido.',
]);
$data = $request->except('password');
if ($request->filled('password')) {
$data['password'] = \Illuminate\Support\Facades\Hash::make($request->password);
}
// Handle permissions: admin types get null (all access), others get selected permissions
$selectedType = UserType::where('name', $data['user_type'])->first();
if ($selectedType && $selectedType->is_admin) {
$data['permissions'] = null;
} else {
$data['permissions'] = $request->has('permissions') ? $request->input('permissions') : [];
}
$user->update($data);
return redirect()->route('admin.users.index')->with('success', 'Usuário atualizado com sucesso.');
}
public function destroy(User $user)
{
if ($user->id === \Illuminate\Support\Facades\Auth::id()) {
return redirect()->route('admin.users.index')->with('error', 'Você não pode excluir sua própria conta.');
}
$user->delete();
return redirect()->route('admin.users.index')->with('success', 'Usuário excluído com sucesso.');
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\UserType;
use App\Models\User;
use Illuminate\Http\Request;
class UserTypeController extends Controller
{
/**
* Display a listing of user types.
*/
public function index()
{
$userTypes = UserType::orderBy('name')->get();
$userTypes->each(function ($type) {
$type->users_count = User::where('user_type', $type->name)->count();
});
return view('admin.user-types.index', compact('userTypes'));
}
/**
* Show the form for creating a new user type.
*/
public function create()
{
return view('admin.user-types.create');
}
/**
* Store a newly created user type.
*/
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255|unique:user_types,name',
'description' => 'nullable|string|max:500',
'is_admin' => 'nullable|boolean',
]);
UserType::create([
'name' => $request->name,
'description' => $request->description,
'is_admin' => $request->has('is_admin'),
'is_protected' => false,
]);
return redirect()->route('admin.user-types.index')->with('success', 'Tipo de usuário criado com sucesso.');
}
/**
* Show the form for editing the specified user type.
*/
public function edit(UserType $userType)
{
return view('admin.user-types.edit', compact('userType'));
}
/**
* Update the specified user type.
*/
public function update(Request $request, UserType $userType)
{
$request->validate([
'name' => 'required|string|max:255|unique:user_types,name,' . $userType->id,
'description' => 'nullable|string|max:500',
'is_admin' => 'nullable|boolean',
]);
$oldName = $userType->name;
$userType->update([
'name' => $request->name,
'description' => $request->description,
'is_admin' => $request->has('is_admin'),
]);
// Update all users with the old type name to the new type name
if ($oldName !== $request->name) {
User::where('user_type', $oldName)->update(['user_type' => $request->name]);
}
return redirect()->route('admin.user-types.index')->with('success', 'Tipo de usuário atualizado com sucesso.');
}
/**
* Remove the specified user type.
*/
public function destroy(UserType $userType)
{
if ($userType->is_protected) {
return redirect()->route('admin.user-types.index')->with('error', 'Este tipo de usuário é protegido e não pode ser excluído.');
}
$userCount = User::where('user_type', $userType->name)->count();
if ($userCount > 0) {
return redirect()->route('admin.user-types.index')->with('error', "Existem {$userCount} usuário(s) com este tipo. Altere o tipo desses usuários antes de excluir.");
}
$userType->delete();
return redirect()->route('admin.user-types.index')->with('success', 'Tipo de usuário excluído com sucesso.');
}
}