First Commit

This commit is contained in:
marco
2026-09-01 10:46:29 -03:00
commit bf96b77e9c
10000 changed files with 3508600 additions and 0 deletions
@@ -0,0 +1,19 @@
<?php
namespace Modules\AffiliatesAdmin\Middleware;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
class AffiliateAdminMiddleware
{
public function handle(Request $request, callable $next): Response
{
if (!isset($_SESSION['user_id']) || !in_array($_SESSION['user_role'], ['Super Admin', 'Admin'])) {
return new RedirectResponse('/login');
}
return $next($request);
}
}
@@ -0,0 +1,20 @@
<?php
namespace Modules\AffiliatesAdmin\Providers;
use Kernel\Application;
use Modules\AffiliatesAdmin\Middleware\AffiliateAdminMiddleware;
class AffiliatesAdminServiceProvider
{
public function register(): void
{
// The ModuleLoader handles loading routes, views, and translations
// based on module.json. No need to call them here.
}
public function boot(Application $app): void
{
$app->addProtectedMiddleware(new AffiliateAdminMiddleware());
}
}
@@ -0,0 +1,419 @@
<?php
namespace Modules\AffiliatesAdmin\Controllers;
use App\Controllers\BaseAdminController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Modules\AffiliatesAdmin\Models\Affiliate;
use Modules\AffiliatesAdmin\Models\AffiliateCommission;
use Modules\AffiliatesAdmin\Models\AffiliateBanner;
use Modules\AffiliatesAdmin\Models\AffiliateClick;
use Modules\Auth\Models\User;
class AffiliatesAdminController extends BaseAdminController
{
/**
* Check if user has permission.
*
* @param string $permission
* @return void
*/
protected function checkPermission(string $permission)
{
$user = $this->getCurrentUser();
if (!$user || !$user->hasPermission($permission)) {
$this->addFlash('error', 'Você não tem permissão para acessar esta área.');
$this->redirect('admin/dashboard')->send();
exit;
}
}
/**
* Display a listing of the affiliates.
*
* @param Request $request
* @return Response
*/
public function index(Request $request): Response
{
$this->checkPermission('affiliatesadmin.view');
$affiliates = Affiliate::all();
return $this->renderAdminView('@affiliatesadmin/index.twig', ['affiliates' => $affiliates]);
}
/**
* Show the form for creating a new affiliate.
*
* @param Request $request
* @return Response
*/
public function create(Request $request): Response
{
$this->checkPermission('affiliatesadmin.manage');
return $this->renderAdminView('@affiliatesadmin/form.twig');
}
/**
* Store a newly created affiliate in storage.
*
* @param Request $request
* @return Response
*/
public function store(Request $request): Response
{
$this->checkPermission('affiliatesadmin.manage');
$data = $request->request->all();
if (empty($data['affiliates_name']) || empty($data['affiliates_email'])) {
$this->addFlash('error', __('affiliatesadmin.validation_name_email_required'));
return $this->redirect('/admin/affiliatesadmin/create');
}
if (!filter_var($data['affiliates_email'], FILTER_VALIDATE_EMAIL)) {
$this->addFlash('error', __('affiliatesadmin.validation_invalid_email'));
return $this->redirect('/admin/affiliatesadmin/create');
}
if (Affiliate::where('affiliates_email', $data['affiliates_email'])->exists()) {
$this->addFlash('error', __('affiliatesadmin.validation_email_exists'));
return $this->redirect('/admin/affiliatesadmin/create');
}
// Check if user already exists in the system
$existingUser = User::where('users_email', $data['affiliates_email'])->first();
if (!$existingUser && empty($data['password'])) {
$this->addFlash('error', 'A senha é obrigatória para criar um novo usuário no sistema.');
return $this->redirect('/admin/affiliatesadmin/create');
}
// Create the Affiliate record
Affiliate::create($data);
if ($existingUser) {
// Only change role if user is a standard 'user'
// Prevent downgrading admins or super_admins
if (in_array($existingUser->users_role, ['user', ''])) {
$existingUser->users_role = 'member';
$existingUser->save();
$this->addFlash('success', __('affiliatesadmin.affiliate_created_successfully') . ' O usuário existente foi promovido para "member".');
} else {
$this->addFlash('success', __('affiliatesadmin.affiliate_created_successfully') . ' O usuário existente manteve seu nível de acesso privilegiado: ' . $existingUser->users_role);
}
} else {
// Create a new System User
$user = new User();
$user->users_name = $data['affiliates_name'];
$user->users_email = $data['affiliates_email'];
$user->users_password = password_hash($data['password'], PASSWORD_DEFAULT);
$user->users_role = 'member';
$user->users_status = 'active';
$user->save();
$this->addFlash('success', __('affiliatesadmin.affiliate_created_successfully') . ' Um novo usuário com nível "member" foi criado.');
}
return $this->redirect('/admin/affiliatesadmin');
}
/**
* Display the specified affiliate.
*
* @param Request $request
* @param int $id
* @return Response
*/
public function show(Request $request, int $id): Response
{
$this->checkPermission('affiliatesadmin.view');
$affiliate = Affiliate::findOrFail($id);
return $this->renderAdminView('@affiliatesadmin/show.twig', ['affiliate' => $affiliate]);
}
/**
* Show the form for editing the specified affiliate.
*
* @param Request $request
* @param int $id
* @return Response
*/
public function edit(Request $request, int $id): Response
{
$this->checkPermission('affiliatesadmin.manage');
$affiliate = Affiliate::findOrFail($id);
return $this->renderAdminView('@affiliatesadmin/form.twig', ['affiliate' => $affiliate]);
}
/**
* Update the specified affiliate in storage.
*
* @param Request $request
* @param int $id
* @return Response
*/
public function update(Request $request, int $id): Response
{
$this->checkPermission('affiliatesadmin.manage');
$affiliate = Affiliate::findOrFail($id);
$data = $request->request->all();
if (empty($data['affiliates_name']) || empty($data['affiliates_email'])) {
$this->addFlash('error', __('affiliatesadmin.validation_name_email_required'));
return $this->redirect('/admin/affiliatesadmin/' . $id . '/edit');
}
if (!filter_var($data['affiliates_email'], FILTER_VALIDATE_EMAIL)) {
$this->addFlash('error', __('affiliatesadmin.validation_invalid_email'));
return $this->redirect('/admin/affiliatesadmin/' . $id . '/edit');
}
if (Affiliate::where('affiliates_email', $data['affiliates_email'])->where('affiliates_id', '!=', $id)->exists()) {
$this->addFlash('error', __('affiliatesadmin.validation_email_exists'));
return $this->redirect('/admin/affiliatesadmin/' . $id . '/edit');
}
$affiliate->update($data);
$this->addFlash('success', __('affiliatesadmin.affiliate_updated_successfully'));
return $this->redirect('/admin/affiliatesadmin');
}
/**
* Remove the specified affiliate from storage.
*
* @param Request $request
* @param int $id
* @return Response
*/
public function destroy(Request $request, int $id): Response
{
$this->checkPermission('affiliatesadmin.manage');
Affiliate::destroy($id);
$this->addFlash('success', __('affiliatesadmin.affiliate_deleted_successfully'));
return $this->redirect('/admin/affiliatesadmin');
}
/**
* Display a listing of commissions.
*
* @param Request $request
* @return Response
*/
public function commissions(Request $request): Response
{
$this->checkPermission('affiliatesadmin.view_commissions');
$commissions = AffiliateCommission::all();
return $this->renderAdminView('@affiliatesadmin/commissions.twig', ['commissions' => $commissions]);
}
/**
* Approve a commission.
*
* @param Request $request
* @param int $id
* @return Response
*/
public function approveCommission(Request $request, int $id): Response
{
$this->checkPermission('affiliatesadmin.manage_commissions');
$commission = AffiliateCommission::findOrFail($id);
$commission->affiliate_commissions_status = 'approved';
$commission->save();
$this->addFlash('success', __('affiliatesadmin.commission_approved_successfully'));
return $this->redirect('/admin/affiliatesadmin/commissions');
}
/**
* Reject a commission.
*
* @param Request $request
* @param int $id
* @return Response
*/
public function rejectCommission(Request $request, int $id): Response
{
$this->checkPermission('affiliatesadmin.manage_commissions');
$commission = AffiliateCommission::findOrFail($id);
$commission->affiliate_commissions_status = 'rejected';
$commission->save();
$this->addFlash('success', __('affiliatesadmin.commission_rejected_successfully'));
return $this->redirect('/admin/affiliatesadmin/commissions');
}
/**
* Display a listing of banners.
*
* @param Request $request
* @return Response
*/
public function banners(Request $request): Response
{
$this->checkPermission('affiliatesadmin.view_banners');
$banners = AffiliateBanner::all();
return $this->renderAdminView('@affiliatesadmin/banners.twig', ['banners' => $banners]);
}
/**
* Show the form for creating a new banner.
*
* @param Request $request
* @return Response
*/
public function createBanner(Request $request): Response
{
$this->checkPermission('affiliatesadmin.manage_banners');
return $this->renderAdminView('@affiliatesadmin/banner-form.twig');
}
/**
* Store a newly created banner in storage.
*
* @param Request $request
* @return Response
*/
public function storeBanner(Request $request): Response
{
$this->checkPermission('affiliatesadmin.manage_banners');
$data = $request->request->all();
if (empty($data['affiliate_banners_name']) || empty($data['affiliate_banners_target_url'])) {
$this->addFlash('error', __('affiliatesadmin.validation_banner_fields_required'));
return $this->redirect('/admin/affiliatesadmin/banners/create');
}
if ($request->files->has('banner_file')) {
$file = $request->files->get('banner_file');
if ($file && $file->isValid()) {
$data['affiliate_banners_image_url'] = $this->handleBannerUpload($file);
}
}
if (empty($data['affiliate_banners_image_url'])) {
$this->addFlash('error', 'O arquivo de imagem do banner é obrigatório.');
return $this->redirect('/admin/affiliatesadmin/banners/create');
}
AffiliateBanner::create($data);
$this->addFlash('success', __('affiliatesadmin.banner_created_successfully'));
return $this->redirect('/admin/affiliatesadmin/banners');
}
/**
* Show the form for editing the specified banner.
*
* @param Request $request
* @param int $id
* @return Response
*/
public function editBanner(Request $request, int $id): Response
{
$banner = AffiliateBanner::findOrFail($id);
return $this->renderAdminView('@affiliatesadmin/banner-form.twig', ['banner' => $banner]);
}
/**
* Update the specified banner in storage.
*
* @param Request $request
* @param int $id
* @return Response
*/
public function updateBanner(Request $request, int $id): Response
{
$banner = AffiliateBanner::findOrFail($id);
$data = $request->request->all();
if (empty($data['affiliate_banners_name']) || empty($data['affiliate_banners_target_url'])) {
$this->addFlash('error', __('affiliatesadmin.validation_banner_fields_required'));
return $this->redirect('/admin/affiliatesadmin/banners/' . $id . '/edit');
}
if ($request->files->has('banner_file')) {
$file = $request->files->get('banner_file');
if ($file && $file->isValid()) {
// Delete old file if exists
if ($banner->affiliate_banners_image_url) {
$oldPath = dirname(__DIR__, 4) . '/storage/uploads/affiliatesadmin/' . $banner->affiliate_banners_image_url;
if (file_exists($oldPath) && is_file($oldPath)) {
unlink($oldPath);
}
}
$data['affiliate_banners_image_url'] = $this->handleBannerUpload($file);
}
}
$banner->update($data);
$this->addFlash('success', __('affiliatesadmin.banner_updated_successfully'));
return $this->redirect('/admin/affiliatesadmin/banners');
}
/**
* Handle banner image upload.
*
* @param \Symfony\Component\HttpFoundation\File\UploadedFile $file
* @return string|null
*/
private function handleBannerUpload($file): ?string
{
$uploadDir = dirname(__DIR__, 4) . '/storage/uploads/affiliatesadmin';
if (!is_dir($uploadDir)) {
if (!mkdir($uploadDir, 0777, true)) {
return null;
}
}
$filename = uniqid('banner_') . '.' . $file->guessExtension();
$file->move($uploadDir, $filename);
// Store ONLY the filename in the database
// The view will prepend /storage/uploads/affiliatesadmin/
return $filename;
}
/**
* Remove the specified banner from storage.
*
* @param Request $request
* @param int $id
* @return Response
*/
public function destroyBanner(Request $request, int $id): Response
{
$banner = AffiliateBanner::findOrFail($id);
if ($banner->affiliate_banners_image_url) {
$path = dirname(__DIR__, 4) . '/storage/uploads/affiliatesadmin/' . $banner->affiliate_banners_image_url;
if (file_exists($path) && is_file($path)) {
unlink($path);
}
}
AffiliateBanner::destroy($id);
$this->addFlash('success', __('affiliatesadmin.banner_deleted_successfully'));
return $this->redirect('/admin/affiliatesadmin/banners');
}
/**
* Display affiliate statistics.
*
* @param Request $request
* @return Response
*/
public function statistics(Request $request): Response
{
$this->checkPermission('affiliatesadmin.view_statistics');
$stats = [
'total_affiliates' => Affiliate::count(),
'total_commissions_approved' => AffiliateCommission::where('affiliate_commissions_status', 'approved')->sum('affiliate_commissions_amount'),
'total_clicks' => AffiliateClick::count(),
'conversion_rate' => 0,
];
if ($stats['total_clicks'] > 0) {
$approvedCount = AffiliateCommission::where('affiliate_commissions_status', 'approved')->count();
$stats['conversion_rate'] = round(($approvedCount / $stats['total_clicks']) * 100, 2);
}
return $this->renderAdminView('@affiliatesadmin/statistics.twig', [
'stats' => $stats
]);
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace Modules\AffiliatesAdmin\Models;
use Illuminate\Database\Eloquent\Model;
class Affiliate extends Model
{
protected $table = 'affiliates';
protected $primaryKey = 'affiliates_id';
protected $fillable = [
'affiliates_code',
'affiliates_name',
'affiliates_email',
'affiliates_phone',
'affiliates_address',
'affiliates_website',
'affiliates_commission_rate',
'affiliates_total_earned',
'affiliates_total_paid',
'affiliates_status',
];
/**
* The "booting" method of the model.
*
* @return void
*/
protected static function boot()
{
parent::boot();
// Ensure affiliates_code is generated before creating
static::creating(function ($affiliate) {
if (empty($affiliate->affiliates_code)) {
$affiliate->affiliates_code = static::generateUniqueAffiliateCode();
}
});
}
/**
* Generate a unique affiliate code.
*
* @return string
*/
protected static function generateUniqueAffiliateCode(): string
{
$code = \Illuminate\Support\Str::random(10); // Using Laravel's Str helper
while (static::where('affiliates_code', $code)->exists()) {
$code = \Illuminate\Support\Str::random(10);
}
return $code;
}
/**
* Get the commissions for the affiliate.
*/
public function commissions()
{
return $this->hasMany(AffiliateCommission::class, 'affiliate_commissions_affiliate_id', 'affiliates_id');
}
/**
* Get the banners created by the affiliate.
*/
public function banners()
{
return $this->hasMany(AffiliateBanner::class, 'affiliate_banners_affiliate_id', 'affiliates_id');
}
/**
* Get the clicks for the affiliate's banners.
*/
public function clicks()
{
return $this->hasMany(AffiliateClick::class, 'affiliate_clicks_affiliate_id', 'affiliates_id');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Modules\AffiliatesAdmin\Models;
use Illuminate\Database\Eloquent\Model;
class AffiliateBanner extends Model
{
protected $table = 'affiliate_banners';
protected $primaryKey = 'affiliate_banners_id';
protected $fillable = [
'affiliate_banners_name',
'affiliate_banners_description',
'affiliate_banners_image_url',
'affiliate_banners_target_url',
'affiliate_banners_width',
'affiliate_banners_height',
'affiliate_banners_is_active',
];
/**
* Get the affiliate that owns the banner.
*/
public function affiliate()
{
return $this->belongsTo(Affiliate::class, 'affiliate_banners_affiliate_id', 'affiliates_id');
}
/**
* Get the clicks for the banner.
*/
public function clicks()
{
return $this->hasMany(AffiliateClick::class, 'affiliate_clicks_banner_id', 'affiliate_banners_id');
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace Modules\AffiliatesAdmin\Models;
use Illuminate\Database\Eloquent\Model;
class AffiliateClick extends Model
{
protected $table = 'affiliate_clicks';
protected $primaryKey = 'affiliate_clicks_id';
protected $fillable = [
'affiliate_clicks_affiliate_id',
'affiliate_clicks_banner_id',
'affiliate_clicks_ip_address',
'affiliate_clicks_user_agent',
'affiliate_clicks_referrer',
];
/**
* Get the affiliate that generated the click.
*/
public function affiliate()
{
return $this->belongsTo(Affiliate::class, 'affiliate_clicks_affiliate_id', 'affiliates_id');
}
/**
* Get the banner that was clicked.
*/
public function banner()
{
return $this->belongsTo(AffiliateBanner::class, 'affiliate_clicks_banner_id', 'affiliate_banners_id');
}
}
@@ -0,0 +1,29 @@
<?php
namespace Modules\AffiliatesAdmin\Models;
use Illuminate\Database\Eloquent\Model;
class AffiliateCommission extends Model
{
protected $table = 'affiliate_commissions';
protected $primaryKey = 'affiliate_commissions_id';
protected $fillable = [
'affiliate_commissions_affiliate_id',
'affiliate_commissions_order_id',
'affiliate_commissions_amount',
'affiliate_commissions_commission_rate',
'affiliate_commissions_status',
'affiliate_commissions_notes',
];
/**
* Get the affiliate that owns the commission.
*/
public function affiliate()
{
return $this->belongsTo(Affiliate::class, 'affiliate_commissions_affiliate_id', 'affiliates_id');
}
}
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
class CreateAffiliatesTable
{
public function up()
{
if (!Capsule::schema()->hasTable('affiliates')) {
Capsule::schema()->create('affiliates', function (Blueprint $table) {
$table->bigIncrements('affiliates_id');
$table->string('affiliates_code')->unique();
$table->string('affiliates_name');
$table->string('affiliates_email')->unique();
$table->string('affiliates_phone')->nullable();
$table->text('affiliates_address')->nullable();
$table->string('affiliates_website')->nullable();
$table->decimal('affiliates_commission_rate', 5, 2)->default(10.00);
$table->decimal('affiliates_total_earned', 12, 2)->default(0.00);
$table->decimal('affiliates_total_paid', 12, 2)->default(0.00);
$table->enum('affiliates_status', ['active', 'pending', 'suspended'])->default('pending');
$table->timestamps();
});
}
}
public function down()
{
Capsule::schema()->dropIfExists('affiliates');
}
}
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
class CreateAffiliateCommissionsTable
{
public function up()
{
if (!Capsule::schema()->hasTable('affiliate_commissions')) {
Capsule::schema()->create('affiliate_commissions', function (Blueprint $table) {
$table->bigIncrements('affiliate_commissions_id');
$table->unsignedBigInteger('affiliate_commissions_affiliate_id');
$table->string('affiliate_commissions_order_id')->nullable(); // Or whatever identifier for the sale
$table->decimal('affiliate_commissions_amount', 10, 2);
$table->decimal('affiliate_commissions_commission_rate', 5, 2);
$table->enum('affiliate_commissions_status', ['pending', 'approved', 'rejected', 'paid'])->default('pending');
$table->text('affiliate_commissions_notes')->nullable();
$table->timestamps();
$table->foreign('affiliate_commissions_affiliate_id')->references('affiliates_id')->on('affiliates')->onDelete('cascade');
});
}
}
public function down()
{
Capsule::schema()->dropIfExists('affiliate_commissions');
}
}
@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
class CreateAffiliateBannersTable
{
public function up()
{
if (!Capsule::schema()->hasTable('affiliate_banners')) {
Capsule::schema()->create('affiliate_banners', function (Blueprint $table) {
$table->bigIncrements('affiliate_banners_id');
$table->string('affiliate_banners_name');
$table->text('affiliate_banners_description')->nullable();
$table->string('affiliate_banners_image_url');
$table->string('affiliate_banners_target_url')->nullable();
$table->integer('affiliate_banners_width')->nullable();
$table->integer('affiliate_banners_height')->nullable();
$table->boolean('affiliate_banners_is_active')->default(true);
$table->timestamps();
});
}
}
public function down()
{
Capsule::schema()->dropIfExists('affiliate_banners');
}
}
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
class CreateAffiliateClicksTable
{
public function up()
{
if (!Capsule::schema()->hasTable('affiliate_clicks')) {
Capsule::schema()->create('affiliate_clicks', function (Blueprint $table) {
$table->bigIncrements('affiliate_clicks_id');
$table->unsignedBigInteger('affiliate_clicks_affiliate_id');
$table->unsignedBigInteger('affiliate_clicks_banner_id');
$table->string('affiliate_clicks_ip_address')->nullable();
$table->text('affiliate_clicks_user_agent')->nullable();
$table->string('affiliate_clicks_referrer')->nullable();
$table->timestamps();
$table->foreign('affiliate_clicks_affiliate_id')->references('affiliates_id')->on('affiliates')->onDelete('cascade');
$table->foreign('affiliate_clicks_banner_id')->references('affiliate_banners_id')->on('affiliate_banners')->onDelete('cascade');
});
}
}
public function down()
{
Capsule::schema()->dropIfExists('affiliate_clicks');
}
}
@@ -0,0 +1,233 @@
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
class RegisterAffiliatesModules
{
public function up()
{
// Register modules in 'modules' table
$modules = [
[
'modules_name' => 'affiliatesadmin',
'modules_display_name' => 'Gerenciamento de Afiliados',
'modules_description' => 'Módulo administrativo para gerenciar afiliados, comissões e banners',
'modules_version' => '1.0.0',
'modules_author' => 'SysCMS Team',
'modules_dependencies' => json_encode(['auth']),
'modules_ative' => 1,
'modules_public' => 0,
'modules_blocked' => 0,
'modules_settings' => json_encode([
'commission_rate_default' => 10,
'commission_min_payout' => 50,
'banner_default_width' => 300,
'banner_default_height' => 250
]),
'modules_permissions' => json_encode([
'affiliatesadmin.affiliates.view',
'affiliatesadmin.affiliates.create',
'affiliatesadmin.affiliates.edit',
'affiliatesadmin.affiliates.delete',
'affiliatesadmin.commissions.view',
'affiliatesadmin.commissions.approve',
'affiliatesadmin.commissions.reject',
'affiliatesadmin.commissions.pay',
'affiliatesadmin.banners.view',
'affiliatesadmin.banners.create',
'affiliatesadmin.banners.edit',
'affiliatesadmin.banners.delete',
'affiliatesadmin.statistics.view'
]),
'modules_path' => 'modules/affiliatesadmin',
'modules_namespace' => 'Modules\\AffiliatesAdmin',
'modules_icon' => 'bi bi-people',
'menu_side_bar' => 1,
'menu_nav_bar' => 0,
'modules_order' => 80,
'modules_install_date' => date('Y-m-d H:i:s'),
'modules_update_date' => date('Y-m-d H:i:s')
],
[
'modules_name' => 'affiliates',
'modules_display_name' => 'Área de Afiliados',
'modules_description' => 'Módulo para afiliados gerenciarem sua conta e banners',
'modules_version' => '1.0.0',
'modules_author' => 'SysCMS Team',
'modules_dependencies' => json_encode(['auth', 'affiliatesadmin']),
'modules_ative' => 1,
'modules_public' => 1,
'modules_blocked' => 0,
'modules_settings' => null,
'modules_permissions' => json_encode([
'affiliates.dashboard.view',
'affiliates.banners.view',
'affiliates.banners.get_code',
'affiliates.commissions.view',
'affiliates.profile.view',
'affiliates.profile.edit'
]),
'modules_path' => 'modules/affiliates',
'modules_namespace' => 'Modules\\Affiliates',
'modules_icon' => 'bi bi-person-badge',
'menu_side_bar' => 0,
'menu_nav_bar' => 0,
'modules_order' => 90,
'modules_install_date' => date('Y-m-d H:i:s'),
'modules_update_date' => date('Y-m-d H:i:s')
]
];
foreach ($modules as $moduleData) {
$moduleName = $moduleData['modules_name'];
$existing = Capsule::table('modules')->where('modules_name', $moduleName)->first();
if (!$existing) {
$moduleId = Capsule::table('modules')->insertGetId($moduleData);
} else {
$moduleId = $existing->modules_id;
Capsule::table('modules')->where('modules_name', $moduleName)->update([
'modules_display_name' => $moduleData['modules_display_name'],
'modules_description' => $moduleData['modules_description'],
'modules_version' => $moduleData['modules_version'],
'modules_author' => $moduleData['modules_author'],
'modules_dependencies' => $moduleData['modules_dependencies'],
'modules_ative' => $moduleData['modules_ative'],
'modules_public' => $moduleData['modules_public'],
'modules_blocked' => $moduleData['modules_blocked'],
'modules_settings' => $moduleData['modules_settings'],
'modules_permissions' => $moduleData['modules_permissions'],
'modules_path' => $moduleData['modules_path'],
'modules_namespace' => $moduleData['modules_namespace'],
'modules_icon' => $moduleData['modules_icon'],
'menu_side_bar' => $moduleData['menu_side_bar'],
'menu_nav_bar' => $moduleData['menu_nav_bar'],
'modules_order' => $moduleData['modules_order'],
'modules_update_date' => date('Y-m-d H:i:s')
]);
}
// Associate module with all existing companies in 'bus_modules'
if (Capsule::schema()->hasTable('bus')) {
$companies = Capsule::table('bus')->get();
foreach ($companies as $company) {
$existingBusModule = Capsule::table('bus_modules')
->where('bus_modules_bus_id', $company->bus_id)
->where('bus_modules_module_id', $moduleId)
->first();
if (!$existingBusModule) {
Capsule::table('bus_modules')->insert([
'bus_modules_bus_id' => $company->bus_id,
'bus_modules_module_id' => $moduleId,
'bus_modules_is_enabled' => 1,
'bus_modules_show_public_nav' => $moduleData['modules_public'],
'bus_modules_show_dashboard_nav' => 1
]);
}
}
}
}
// Register permissions in 'role_permissions' table for admin and member roles
$permissions = [
// AffiliatesAdmin permissions
'affiliatesadmin.affiliates.view',
'affiliatesadmin.affiliates.create',
'affiliatesadmin.affiliates.edit',
'affiliatesadmin.affiliates.delete',
'affiliatesadmin.commissions.view',
'affiliatesadmin.commissions.approve',
'affiliatesadmin.commissions.reject',
'affiliatesadmin.commissions.pay',
'affiliatesadmin.banners.view',
'affiliatesadmin.banners.create',
'affiliatesadmin.banners.edit',
'affiliatesadmin.banners.delete',
'affiliatesadmin.statistics.view',
// Affiliates permissions
'affiliates.dashboard.view',
'affiliates.banners.view',
'affiliates.banners.get_code',
'affiliates.commissions.view',
'affiliates.profile.view',
'affiliates.profile.edit',
];
$defaultCompanyId = 1;
foreach (['admin', 'member'] as $role) {
foreach ($permissions as $perm) {
$existing = Capsule::table('role_permissions')
->where('role_permissions_role', $role)
->where('role_permissions_bus_id', $defaultCompanyId)
->where('role_permissions_permission', $perm)
->first();
if (!$existing) {
Capsule::table('role_permissions')->insert([
'role_permissions_role' => $role,
'role_permissions_bus_id' => $defaultCompanyId,
'role_permissions_permission' => $perm,
'role_permissions_created_by' => null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
]);
}
}
}
}
public function down()
{
// Remove permissions from 'role_permissions' for admin and member roles
$defaultCompanyId = 1;
$permissions = [
// AffiliatesAdmin permissions
'affiliatesadmin.affiliates.view',
'affiliatesadmin.affiliates.create',
'affiliatesadmin.affiliates.edit',
'affiliatesadmin.affiliates.delete',
'affiliatesadmin.commissions.view',
'affiliatesadmin.commissions.approve',
'affiliatesadmin.commissions.reject',
'affiliatesadmin.commissions.pay',
'affiliatesadmin.banners.view',
'affiliatesadmin.banners.create',
'affiliatesadmin.banners.edit',
'affiliatesadmin.banners.delete',
'affiliatesadmin.statistics.view',
// Affiliates permissions
'affiliates.dashboard.view',
'affiliates.banners.view',
'affiliates.banners.get_code',
'affiliates.commissions.view',
'affiliates.profile.view',
'affiliates.profile.edit',
];
foreach (['admin', 'member'] as $role) {
foreach ($permissions as $perm) {
Capsule::table('role_permissions')
->where('role_permissions_role', $role)
->where('role_permissions_bus_id', $defaultCompanyId)
->where('role_permissions_permission', $perm)
->delete();
}
}
// Remove from bus_modules
$moduleNames = ['affiliatesadmin', 'affiliates'];
foreach ($moduleNames as $moduleName) {
$module = Capsule::table('modules')->where('modules_name', $moduleName)->first();
if ($module) {
Capsule::table('bus_modules')
->where('bus_modules_module_id', $module->modules_id)
->delete();
}
}
// Remove from modules table
foreach ($moduleNames as $moduleName) {
Capsule::table('modules')->where('modules_name', $moduleName)->delete();
}
}
}
@@ -0,0 +1,51 @@
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
class AddMenuToAffiliatesadmin
{
public function up()
{
// Get the affiliatesadmin module
$module = Capsule::table('modules')->where('modules_name', 'affiliatesadmin')->first();
if ($module) {
// Get current settings
$settings = $module->modules_settings ? json_decode($module->modules_settings, true) : [];
// Add menu configuration
$settings['menu'] = [
'dashboard' => [
'icon' => 'bi bi-people',
'label' => 'Gerenciamento de Afiliados',
'route' => 'admin/affiliatesadmin/affiliates',
'order' => 80
]
];
// Update the module
Capsule::table('modules')
->where('modules_name', 'affiliatesadmin')
->update([
'modules_settings' => json_encode($settings)
]);
}
}
public function down()
{
// Remove menu configuration (optional rollback)
$module = Capsule::table('modules')->where('modules_name', 'affiliatesadmin')->first();
if ($module) {
$settings = $module->modules_settings ? json_decode($module->modules_settings, true) : [];
unset($settings['menu']);
Capsule::table('modules')
->where('modules_name', 'affiliatesadmin')
->update([
'modules_settings' => json_encode($settings)
]);
}
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
return [
'module_name' => 'Gerenciamento de Afiliados',
'affiliates' => 'Afiliados',
'affiliate' => 'Afiliado',
'affiliate_code' => 'Código de Afiliado',
'name' => 'Nome',
'email' => 'Email',
'phone' => 'Telefone',
'address' => 'Endereço',
'website' => 'Website',
'commission_rate' => 'Taxa de Comissão',
'total_earned' => 'Total Ganho',
'total_paid' => 'Total Pago',
'status' => 'Status',
'actions' => 'Ações',
'create_affiliate' => 'Criar Afiliado',
'edit_affiliate' => 'Editar Afiliado',
'view_affiliate' => 'Ver Afiliado',
'delete_affiliate' => 'Excluir Afiliado',
'commissions' => 'Comissões',
'commission' => 'Comissão',
'approve_commission' => 'Aprovar Comissão',
'reject_commission' => 'Rejeitar Comissão',
'banners' => 'Banners',
'banner' => 'Banner',
'create_banner' => 'Criar Banner',
'edit_banner' => 'Editar Banner',
'delete_banner' => 'Excluir Banner',
'statistics' => 'Estatísticas',
'id' => 'ID',
'created_at' => 'Criado em',
'updated_at' => 'Atualizado em',
'save' => 'Salvar',
'cancel' => 'Cancelar',
'back_to_list' => 'Voltar para a lista',
'confirm_delete' => 'Tem certeza que deseja excluir este item?',
// Validation messages
'validation_name_email_required' => 'Nome e Email são obrigatórios.',
'validation_invalid_email' => 'O endereço de email é inválido.',
'validation_email_exists' => 'Este email já está em uso por outro afiliado.',
'validation_banner_fields_required' => 'Nome, URL da Imagem e URL de Destino são obrigatórios para o banner.',
'validation_invalid_url' => 'A URL da Imagem ou a URL de Destino são inválidas.',
// Success messages
'affiliate_created_successfully' => 'Afiliado criado com sucesso!',
'affiliate_updated_successfully' => 'Afiliado atualizado com sucesso!',
'affiliate_deleted_successfully' => 'Afiliado excluído com sucesso!',
'commission_approved_successfully' => 'Comissão aprovada com sucesso!',
'commission_rejected_successfully' => 'Comissão rejeitada com sucesso!',
'banner_created_successfully' => 'Banner criado com sucesso!',
'banner_updated_successfully' => 'Banner atualizado com sucesso!',
'banner_deleted_successfully' => 'Banner excluído com sucesso!',
];
+46
View File
@@ -0,0 +1,46 @@
{
"name": "affiliatesadmin",
"display_name": "Gerenciamento de Afiliados",
"description": "Módulo administrativo para gerenciar afiliados, comissões e banners",
"version": "1.0.0",
"author": "SysCMS Team",
"dependencies": ["auth"],
"permissions": [
"affiliatesadmin.affiliates.view",
"affiliatesadmin.affiliates.create",
"affiliatesadmin.affiliates.edit",
"affiliatesadmin.affiliates.delete",
"affiliatesadmin.commissions.view",
"affiliatesadmin.commissions.approve",
"affiliatesadmin.commissions.reject",
"affiliatesadmin.commissions.pay",
"affiliatesadmin.banners.view",
"affiliatesadmin.banners.create",
"affiliatesadmin.banners.edit",
"affiliatesadmin.banners.delete",
"affiliatesadmin.statistics.view"
],
"providers": ["Modules\\AffiliatesAdmin\\Providers\\AffiliatesAdminServiceProvider"],
"settings": {
"commission_rate_default": 10,
"commission_min_payout": 50,
"banner_default_width": 300,
"banner_default_height": 250
},
"menu": {
"dashboard": {
"icon": "bi bi-people",
"label": "affiliatesadmin.menu.dashboard",
"route": "admin/affiliatesadmin/affiliates",
"order": 50
}
},
"routes": {"web": "routes/web.php"},
"views": "views",
"locales": "locales",
"module_active": true,
"module_public": false,
"module_blocked": false,
"ready": true,
"tables": ["affiliates", "affiliate_commissions", "affiliate_banners", "affiliate_clicks"]
}
+27
View File
@@ -0,0 +1,27 @@
<?php
use Kernel\Application;
use Modules\AffiliatesAdmin\Controllers\AffiliatesAdminController;
return function (Application $app) {
$app->addRoute('GET', '/admin/affiliatesadmin', [AffiliatesAdminController::class, 'index']);
$app->addRoute('GET', '/admin/affiliatesadmin/affiliates', [AffiliatesAdminController::class, 'index']);
$app->addRoute('GET', '/admin/affiliatesadmin/create', [AffiliatesAdminController::class, 'create']);
$app->addRoute('POST', '/admin/affiliatesadmin/store', [AffiliatesAdminController::class, 'store']);
$app->addRoute('GET', '/admin/affiliatesadmin/{id:\d+}/edit', [AffiliatesAdminController::class, 'edit']);
$app->addRoute('POST', '/admin/affiliatesadmin/{id:\d+}/update', [AffiliatesAdminController::class, 'update']);
$app->addRoute('POST', '/admin/affiliatesadmin/{id:\d+}/delete', [AffiliatesAdminController::class, 'destroy']);
$app->addRoute('GET', '/admin/affiliatesadmin/commissions', [AffiliatesAdminController::class, 'commissions']);
$app->addRoute('POST', '/admin/affiliatesadmin/commissions/{id:\d+}/approve', [AffiliatesAdminController::class, 'approveCommission']);
$app->addRoute('POST', '/admin/affiliatesadmin/commissions/{id:\d+}/reject', [AffiliatesAdminController::class, 'rejectCommission']);
$app->addRoute('GET', '/admin/affiliatesadmin/banners', [AffiliatesAdminController::class, 'banners']);
$app->addRoute('GET', '/admin/affiliatesadmin/banners/create', [AffiliatesAdminController::class, 'createBanner']);
$app->addRoute('POST', '/admin/affiliatesadmin/banners/store', [AffiliatesAdminController::class, 'storeBanner']);
$app->addRoute('GET', '/admin/affiliatesadmin/banners/{id:\d+}/edit', [AffiliatesAdminController::class, 'editBanner']);
$app->addRoute('POST', '/admin/affiliatesadmin/banners/{id:\d+}/update', [AffiliatesAdminController::class, 'updateBanner']);
$app->addRoute('POST', '/admin/affiliatesadmin/banners/{id:\d+}/delete', [AffiliatesAdminController::class, 'destroyBanner']);
$app->addRoute('GET', '/admin/affiliatesadmin/statistics', [AffiliatesAdminController::class, 'statistics']);
};
+77
View File
@@ -0,0 +1,77 @@
{% extends "layouts/dashboard.twig" %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">{% if banner.affiliate_banners_id %}Editar Banner{% else %}Criar Novo Banner{% endif %}</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/admin/dashboard">Dashboard</a></li>
<li class="breadcrumb-item"><a href="/admin/affiliatesadmin/affiliates">Afiliados</a></li>
<li class="breadcrumb-item"><a href="/admin/affiliatesadmin/banners">Banners</a></li>
<li class="breadcrumb-item active">{% if banner.affiliate_banners_id %}Editar{% else %}Novo{% endif %}</li>
</ol>
{% include '@affiliatesadmin/nav.twig' %}
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-image me-1"></i>
Formulário de Banner
</div>
<div class="card-body">
<form action="{% if banner.affiliate_banners_id %}/admin/affiliatesadmin/banners/{{ banner.affiliate_banners_id }}/update{% else %}/admin/affiliatesadmin/banners/store{% endif %}" method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label for="affiliate_banners_name" class="form-label">Nome:</label>
<input type="text" class="form-control" id="affiliate_banners_name" name="affiliate_banners_name" value="{{ banner.affiliate_banners_name ?? '' }}" required>
</div>
<div class="mb-3">
<label for="affiliate_banners_description" class="form-label">Descrição:</label>
<textarea class="form-control" id="affiliate_banners_description" name="affiliate_banners_description" rows="2">{{ banner.affiliate_banners_description ?? '' }}</textarea>
</div>
<div class="mb-3">
<label for="banner_file" class="form-label">Arquivo do Banner:</label>
<input type="file" class="form-control" id="banner_file" name="banner_file" {% if not banner.affiliate_banners_id %}required{% endif %}>
<div class="form-text text-muted">Selecione uma imagem para o banner (JPG, PNG, GIF).</div>
{% if banner.affiliate_banners_image_url %}
<div class="mt-2">
<p class="small mb-1">Banner Atual:</p>
<img src="/storage/uploads/affiliatesadmin/{{ banner.affiliate_banners_image_url }}" alt="Banner Atual" class="img-thumbnail" style="max-height: 150px;">
</div>
{% endif %}
</div>
<div class="mb-3">
<label for="affiliate_banners_target_url" class="form-label">URL de Destino:</label>
<input type="url" class="form-control" id="affiliate_banners_target_url" name="affiliate_banners_target_url" value="{{ banner.affiliate_banners_target_url ?? '' }}">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="affiliate_banners_width" class="form-label">Largura (px):</label>
<input type="number" class="form-control" id="affiliate_banners_width" name="affiliate_banners_width" value="{{ banner.affiliate_banners_width ?? '' }}">
</div>
<div class="col-md-6 mb-3">
<label for="affiliate_banners_height" class="form-label">Altura (px):</label>
<input type="number" class="form-control" id="affiliate_banners_height" name="affiliate_banners_height" value="{{ banner.affiliate_banners_height ?? '' }}">
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="affiliate_banners_is_active" name="affiliate_banners_is_active" value="1" {% if banner.affiliate_banners_is_active ?? true %}checked{% endif %}>
<label class="form-check-label" for="affiliate_banners_is_active">Banner Ativo</label>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Salvar
</button>
<a href="/admin/affiliatesadmin/banners" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Cancelar
</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
+90
View File
@@ -0,0 +1,90 @@
{% extends "layouts/dashboard.twig" %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">Gerenciamento de Banners</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/admin/dashboard">Dashboard</a></li>
<li class="breadcrumb-item"><a href="/admin/affiliatesadmin/affiliates">Afiliados</a></li>
<li class="breadcrumb-item active">Banners</li>
</ol>
{% include '@affiliatesadmin/nav.twig' %}
<div class="card mb-4">
<div class="card-header d-flex align-items-center justify-content-between">
<div>
<i class="bi bi-image me-1"></i>
Lista de Banners
</div>
<a href="/admin/affiliatesadmin/banners/create" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg"></i> Novo Banner
</a>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-striped" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Imagem</th>
<th>URL de Destino</th>
<th>Ativo</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for banner in banners %}
<tr>
<td>{{ banner.affiliate_banners_id }}</td>
<td>{{ banner.affiliate_banners_name }}</td>
<td>
<a href="/storage/uploads/affiliatesadmin/{{ banner.affiliate_banners_image_url }}" data-lightbox="banner-{{ banner.affiliate_banners_id }}">
<img src="/storage/uploads/affiliatesadmin/{{ banner.affiliate_banners_image_url }}" alt="{{ banner.affiliate_banners_name }}" class="img-thumbnail" style="max-width: 80px;">
</a>
</td>
<td>
<a href="{{ banner.affiliate_banners_target_url }}" target="_blank" class="text-truncate d-inline-block" style="max-width: 200px;">
{{ banner.affiliate_banners_target_url }}
</a>
</td>
<td>
<span class="badge bg-{{ banner.affiliate_banners_is_active ? 'success' : 'secondary' }}">
{{ banner.affiliate_banners_is_active ? 'Sim' : 'Não' }}
</span>
</td>
<td>
<div class="btn-group" role="group">
<a href="/admin/affiliatesadmin/banners/{{ banner.affiliate_banners_id }}/edit" class="btn btn-info btn-sm" title="Editar">
<i class="bi bi-pencil"></i>
</a>
<form action="/admin/affiliatesadmin/banners/{{ banner.affiliate_banners_id }}/delete" method="POST" onsubmit="return confirm('Tem certeza que deseja excluir este banner?')">
<button type="submit" class="btn btn-danger btn-sm" title="Excluir">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
$('#dataTable').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
},
order: [[0, 'desc']]
});
});
</script>
{% endblock %}
+95
View File
@@ -0,0 +1,95 @@
{% extends "layouts/dashboard.twig" %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">Gerenciamento de Comissões</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/admin/dashboard">Dashboard</a></li>
<li class="breadcrumb-item"><a href="/admin/affiliatesadmin/affiliates">Afiliados</a></li>
<li class="breadcrumb-item active">Comissões</li>
</ol>
{% include '@affiliatesadmin/nav.twig' %}
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-currency-dollar me-1"></i>
Lista de Comissões
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-striped" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>ID</th>
<th>Afiliado</th>
<th>ID do Pedido</th>
<th>Valor</th>
<th>Taxa (%)</th>
<th>Status</th>
<th>Notas</th>
<th>Data</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for commission in commissions %}
<tr>
<td>{{ commission.affiliate_commissions_id }}</td>
<td>{{ commission.affiliate.affiliates_name ?? 'N/A' }} <br><small class="text-muted">({{ commission.affiliate.affiliates_code ?? 'N/A' }})</small></td>
<td>{{ commission.affiliate_commissions_order_id ?? 'N/A' }}</td>
<td>R$ {{ commission.affiliate_commissions_amount|number_format(2, ',', '.') }}</td>
<td>{{ commission.affiliate_commissions_commission_rate }}%</td>
<td>
{% set statusColors = {
'pending': 'warning',
'approved': 'success',
'rejected': 'danger',
'paid': 'info'
} %}
<span class="badge bg-{{ statusColors[commission.affiliate_commissions_status] ?? 'secondary' }}">
{{ commission.affiliate_commissions_status }}
</span>
</td>
<td>{{ commission.affiliate_commissions_notes ?? 'N/A' }}</td>
<td>{{ commission.created_at|date("d/m/Y H:i") }}</td>
<td>
{% if commission.affiliate_commissions_status == 'pending' %}
<div class="btn-group" role="group">
<form action="/admin/affiliatesadmin/commissions/{{ commission.affiliate_commissions_id }}/approve" method="POST">
<button type="submit" class="btn btn-success btn-sm" title="Aprovar">
<i class="bi bi-check-lg"></i>
</button>
</form>
<form action="/admin/affiliatesadmin/commissions/{{ commission.affiliate_commissions_id }}/reject" method="POST">
<button type="submit" class="btn btn-warning btn-sm" title="Rejeitar">
<i class="bi bi-x-lg"></i>
</button>
</form>
</div>
{% else %}
<span class="text-muted small">Processado</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
$('#dataTable').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
},
order: [[0, 'desc']]
});
});
</script>
{% endblock %}
+90
View File
@@ -0,0 +1,90 @@
{% extends "layouts/dashboard.twig" %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">{% if affiliate.affiliates_id %}Editar Afiliado{% else %}Criar Novo Afiliado{% endif %}</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/admin/dashboard">Dashboard</a></li>
<li class="breadcrumb-item"><a href="/admin/affiliatesadmin/affiliates">Afiliados</a></li>
<li class="breadcrumb-item active">{% if affiliate.affiliates_id %}Editar{% else %}Novo{% endif %}</li>
</ol>
{% include '@affiliatesadmin/nav.twig' %}
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-person-plus me-1"></i>
Formulário de Afiliado
</div>
<div class="card-body">
<form action="{% if affiliate.affiliates_id %}/admin/affiliatesadmin/{{ affiliate.affiliates_id }}/update{% else %}/admin/affiliatesadmin/store{% endif %}" method="POST">
<div class="row">
<div class="col-md-6 mb-3">
<label for="affiliates_name" class="form-label">Nome:</label>
<input type="text" class="form-control" id="affiliates_name" name="affiliates_name" value="{{ affiliate.affiliates_name ?? '' }}" required>
</div>
<div class="col-md-6 mb-3">
<label for="affiliates_email" class="form-label">Email:</label>
<input type="email" class="form-control" id="affiliates_email" name="affiliates_email" value="{{ affiliate.affiliates_email ?? '' }}" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="affiliates_phone" class="form-label">Telefone:</label>
<input type="text" class="form-control" id="affiliates_phone" name="affiliates_phone" value="{{ affiliate.affiliates_phone ?? '' }}">
</div>
{% if not affiliate.affiliates_id %}
<div class="col-md-6 mb-3">
<label for="password" class="form-label">Senha do Usuário:</label>
<input type="password" class="form-control" id="password" name="password">
<div class="form-text text-muted">Obrigatória apenas para novos cadastros. Se o e-mail já existir no sistema, esta senha será ignorada e o usuário será vinculado.</div>
</div>
{% else %}
<div class="col-md-6 mb-3">
<label for="affiliates_website" class="form-label">Website:</label>
<input type="url" class="form-control" id="affiliates_website" name="affiliates_website" value="{{ affiliate.affiliates_website ?? '' }}">
</div>
{% endif %}
</div>
{% if not affiliate.affiliates_id %}
<div class="mb-3">
<label for="affiliates_website" class="form-label">Website:</label>
<input type="url" class="form-control" id="affiliates_website" name="affiliates_website" value="{{ affiliate.affiliates_website ?? '' }}">
</div>
{% endif %}
<div class="mb-3">
<label for="affiliates_address" class="form-label">Endereço:</label>
<textarea class="form-control" id="affiliates_address" name="affiliates_address" rows="3">{{ affiliate.affiliates_address ?? '' }}</textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="affiliates_commission_rate" class="form-label">Taxa de Comissão (%):</label>
<input type="number" step="0.01" class="form-control" id="affiliates_commission_rate" name="affiliates_commission_rate" value="{{ affiliate.affiliates_commission_rate ?? '10.00' }}" required>
</div>
<div class="col-md-6 mb-3">
<label for="affiliates_status" class="form-label">Status:</label>
<select class="form-select" id="affiliates_status" name="affiliates_status" required>
<option value="active" {% if affiliate.affiliates_status ?? '' == 'active' %}selected{% endif %}>Ativo</option>
<option value="pending" {% if affiliate.affiliates_status ?? '' == 'pending' %}selected{% endif %}>Pendente</option>
<option value="suspended" {% if affiliate.affiliates_status ?? '' == 'suspended' %}selected{% endif %}>Suspenso</option>
</select>
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Salvar
</button>
<a href="/admin/affiliatesadmin/affiliates" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Cancelar
</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
+83
View File
@@ -0,0 +1,83 @@
{% extends "layouts/dashboard.twig" %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">Gerenciamento de Afiliados</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/admin/dashboard">Dashboard</a></li>
<li class="breadcrumb-item active">Afiliados</li>
</ol>
{% include '@affiliatesadmin/nav.twig' %}
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-people me-1"></i>
Lista de Afiliados
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-striped" id="dataTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Email</th>
<th>Código</th>
<th>Comissão</th>
<th>Status</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for affiliate in affiliates %}
<tr>
<td>{{ affiliate.affiliates_id }}</td>
<td>{{ affiliate.affiliates_name }}</td>
<td>{{ affiliate.affiliates_email }}</td>
<td><span class="badge bg-secondary">{{ affiliate.affiliates_code }}</span></td>
<td>{{ affiliate.affiliates_commission_rate }}%</td>
<td>
{% set statusColors = {
'active': 'success',
'pending': 'warning',
'suspended': 'danger'
} %}
<span class="badge bg-{{ statusColors[affiliate.affiliates_status] ?? 'secondary' }}">
{{ affiliate.affiliates_status }}
</span>
</td>
<td>
<div class="btn-group" role="group">
<a href="/admin/affiliatesadmin/{{ affiliate.affiliates_id }}/edit" class="btn btn-info btn-sm" title="Editar">
<i class="bi bi-pencil"></i>
</a>
<form action="/admin/affiliatesadmin/{{ affiliate.affiliates_id }}/delete" method="POST" onsubmit="return confirm('Tem certeza que deseja excluir este afiliado?')">
<button type="submit" class="btn btn-danger btn-sm" title="Excluir">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
$('#dataTable').DataTable({
language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
},
order: [[0, 'desc']]
});
});
</script>
{% endblock %}
+19
View File
@@ -0,0 +1,19 @@
<div class="mb-4 d-print-none">
<div class="btn-group flex-wrap" role="group" aria-label="Navegação de Afiliados">
<a href="/admin/affiliatesadmin/statistics" class="btn btn-outline-primary">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
<a href="/admin/affiliatesadmin/affiliates" class="btn btn-outline-primary">
<i class="bi bi-people"></i> Afiliados
</a>
<a href="/admin/affiliatesadmin/commissions" class="btn btn-outline-primary">
<i class="bi bi-currency-dollar"></i> Comissões
</a>
<a href="/admin/affiliatesadmin/banners" class="btn btn-outline-primary">
<i class="bi bi-image"></i> Banners
</a>
<a href="/admin/affiliatesadmin/create" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Novo Afiliado
</a>
</div>
</div>
+39
View File
@@ -0,0 +1,39 @@
{% extends "layouts/dashboard.twig" %}
{% block title %}{{ affiliate.name }} - {{ __("affiliatesadmin.affiliate_details") }}{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h3 class="card-title">{{ affiliate.name }}</h3>
<div class="card-tools">
<a href="/admin/affiliatesadmin/{{ affiliate.id }}/edit" class="btn btn-sm btn-primary">
<i class="fas fa-edit"></i> {{ __("affiliatesadmin.edit") }}
</a>
<a href="/admin/affiliatesadmin" class="btn btn-sm btn-secondary">
<i class="fas fa-list"></i> {{ __("affiliatesadmin.back_to_list") }}
</a>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>{{ __("affiliatesadmin.affiliate_code") }}:</strong> {{ affiliate.affiliate_code }}</p>
<p><strong>{{ __("affiliatesadmin.name") }}:</strong> {{ affiliate.name }}</p>
<p><strong>{{ __("affiliatesadmin.email") }}:</strong> {{ affiliate.email }}</p>
<p><strong>{{ __("affiliatesadmin.phone") }}:</strong> {{ affiliate.phone }}</p>
<p><strong>{{ __("affiliatesadmin.address") }}:</strong> {{ affiliate.address }}</p>
<p><strong>{{ __("affiliatesadmin.website") }}:</strong> {{ affiliate.website }}</p>
</div>
<div class="col-md-6">
<p><strong>{{ __("affiliatesadmin.commission_rate") }}:</strong> {{ affiliate.commission_rate }}%</p>
<p><strong>{{ __("affiliatesadmin.total_earned") }}:</strong> {{ affiliate.total_earned|number_format(2, ',', '.') }}</p>
<p><strong>{{ __("affiliatesadmin.total_paid") }}:</strong> {{ affiliate.total_paid|number_format(2, ',', '.') }}</p>
<p><strong>{{ __("affiliatesadmin.status") }}:</strong> {{ affiliate.status }}</p>
<p><strong>{{ __("affiliatesadmin.created_at") }}:</strong> {{ affiliate.created_at|date("d/m/Y H:i") }}</p>
<p><strong>{{ __("affiliatesadmin.updated_at") }}:</strong> {{ affiliate.updated_at|date("d/m/Y H:i") }}</p>
</div>
</div>
</div>
</div>
{% endblock %}
+103
View File
@@ -0,0 +1,103 @@
{% extends "layouts/dashboard.twig" %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">Estatísticas de Afiliados</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/admin/dashboard">Dashboard</a></li>
<li class="breadcrumb-item active">Estatísticas</li>
</ol>
{% include '@affiliatesadmin/nav.twig' %}
<div class="row">
<div class="col-xl-3 col-md-6">
<div class="card bg-primary text-white mb-4">
<div class="card-body">Total de Afiliados</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<h4 class="mb-0">{{ stats.total_affiliates ?? 0 }}</h4>
<i class="bi bi-people-fill fs-2 opacity-50"></i>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-success text-white mb-4">
<div class="card-body">Comissões Aprovadas</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<h4 class="mb-0">R$ {{ stats.total_commissions_approved ?? 0|number_format(2, ',', '.') }}</h4>
<i class="bi bi-cash-stack fs-2 opacity-50"></i>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-warning text-white mb-4">
<div class="card-body">Cliques nos Banners</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<h4 class="mb-0">{{ stats.total_clicks ?? 0 }}</h4>
<i class="bi bi-mouse2 fs-2 opacity-50"></i>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-info text-white mb-4">
<div class="card-body">Conversão Média</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<h4 class="mb-0">{{ stats.conversion_rate ?? 0 }}%</h4>
<i class="bi bi-graph-up-arrow fs-2 opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-graph-up me-1"></i>
Gráfico de Ganhos por Mês
</div>
<div class="card-body">
<canvas id="myAreaChart" width="100%" height="40"></canvas>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var ctx = document.getElementById("myAreaChart");
if (ctx) {
var myLineChart = new Chart(ctx, {
type: 'line',
data: {
labels: ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"],
datasets: [{
label: "Comissões (R$)",
lineTension: 0.3,
backgroundColor: "rgba(78, 115, 223, 0.05)",
borderColor: "rgba(78, 115, 223, 1)",
pointRadius: 3,
pointBackgroundColor: "rgba(78, 115, 223, 1)",
pointBorderColor: "rgba(78, 115, 223, 1)",
pointHoverRadius: 3,
pointHoverBackgroundColor: "rgba(78, 115, 223, 1)",
pointHoverBorderColor: "rgba(78, 115, 223, 1)",
pointHitRadius: 10,
pointBorderWidth: 2,
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // Dados reais viriam do controller
}],
},
options: {
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
});
</script>
{% endblock %}