First Commit
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Affiliates\Middleware;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
|
||||
class AffiliateMiddleware
|
||||
{
|
||||
public function handle(Request $request, callable $next): Response
|
||||
{
|
||||
if (!isset($_SESSION['user_id']) || !in_array($_SESSION['user_role'], ['Admin', 'Member'])) {
|
||||
return new RedirectResponse('/login');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Affiliates\Providers;
|
||||
|
||||
use Kernel\Application;
|
||||
use Modules\Affiliates\Middleware\AffiliateMiddleware;
|
||||
|
||||
class AffiliatesServiceProvider
|
||||
{
|
||||
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 AffiliateMiddleware());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Affiliates\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 Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
|
||||
class AffiliatesController 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;
|
||||
}
|
||||
}
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
$affiliateId = $_SESSION['affiliate_id'] ?? null;
|
||||
|
||||
if (!$affiliateId) {
|
||||
return new RedirectResponse('/login');
|
||||
}
|
||||
|
||||
$affiliate = Affiliate::find($affiliateId);
|
||||
|
||||
if (!$affiliate) {
|
||||
return new RedirectResponse('/login');
|
||||
}
|
||||
|
||||
$totalEarned = AffiliateCommission::where('affiliate_id', $affiliateId)
|
||||
->where('status', 'approved')
|
||||
->sum('amount');
|
||||
|
||||
$pendingCommissions = AffiliateCommission::where('affiliate_id', $affiliateId)
|
||||
->where('status', 'pending')
|
||||
->count();
|
||||
|
||||
$banners = AffiliateBanner::where('is_active', true)->get();
|
||||
|
||||
return $this->renderAdminView('@affiliates/dashboard/index.twig', [
|
||||
'affiliate' => $affiliate,
|
||||
'totalEarned' => $totalEarned,
|
||||
'pendingCommissions' => $pendingCommissions,
|
||||
'banners' => $banners,
|
||||
]);
|
||||
}
|
||||
|
||||
public function banners(): Response
|
||||
{
|
||||
$affiliateId = $_SESSION['affiliate_id'] ?? null;
|
||||
if (!$affiliateId) {
|
||||
return new RedirectResponse('/login');
|
||||
}
|
||||
|
||||
$banners = AffiliateBanner::where('is_active', true)->get();
|
||||
return $this->renderAdminView('@affiliates/banners/index.twig', ['banners' => $banners]);
|
||||
}
|
||||
|
||||
public function bannerCode(int $id): Response
|
||||
{
|
||||
$affiliateId = $_SESSION['affiliate_id'] ?? null;
|
||||
if (!$affiliateId) {
|
||||
return new RedirectResponse('/login');
|
||||
}
|
||||
|
||||
$banner = AffiliateBanner::findOrFail($id);
|
||||
return $this->renderAdminView('@affiliates/banners/code.twig', ['banner' => $banner, 'affiliateId' => $affiliateId]);
|
||||
}
|
||||
|
||||
public function commissions(): Response
|
||||
{
|
||||
$affiliateId = $_SESSION['affiliate_id'] ?? null;
|
||||
if (!$affiliateId) {
|
||||
return new RedirectResponse('/login');
|
||||
}
|
||||
|
||||
$commissions = AffiliateCommission::where('affiliate_id', $affiliateId)->get();
|
||||
return $this->renderAdminView('@affiliates/commissions/index.twig', ['commissions' => $commissions]);
|
||||
}
|
||||
|
||||
public function profile(): Response
|
||||
{
|
||||
$affiliateId = $_SESSION['affiliate_id'] ?? null;
|
||||
if (!$affiliateId) {
|
||||
return new RedirectResponse('/login');
|
||||
}
|
||||
|
||||
$affiliate = Affiliate::findOrFail($affiliateId);
|
||||
return $this->renderAdminView('@affiliates/profile/index.twig', ['affiliate' => $affiliate]);
|
||||
}
|
||||
|
||||
public function trackClick(Request $request, string $affiliateCode, int $bannerId): Response
|
||||
{
|
||||
$affiliate = Affiliate::where('affiliate_code', $affiliateCode)->first();
|
||||
$banner = AffiliateBanner::find($bannerId);
|
||||
|
||||
if ($affiliate && $banner) {
|
||||
AffiliateClick::create([
|
||||
'affiliate_id' => $affiliate->id,
|
||||
'banner_id' => $banner->id,
|
||||
'ip_address' => $request->getClientIp(),
|
||||
'user_agent' => $request->headers->get('User-Agent'),
|
||||
'referrer' => $request->headers->get('Referer'),
|
||||
]);
|
||||
return new RedirectResponse($banner->target_url);
|
||||
}
|
||||
|
||||
return new RedirectResponse('/');
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'module_name' => 'Área de Afiliados',
|
||||
'dashboard' => 'Dashboard',
|
||||
'banners' => 'Banners',
|
||||
'commissions' => 'Comissões',
|
||||
'profile' => 'Perfil',
|
||||
'total_earned' => 'Total Ganho',
|
||||
'pending_commissions' => 'Comissões Pendentes',
|
||||
'your_banners' => 'Seus Banners',
|
||||
'get_code' => 'Obter Código',
|
||||
'no_banners_available' => 'Nenhum banner disponível no momento.',
|
||||
'banner_code_for' => 'Código do Banner para',
|
||||
'copy_code' => 'Copiar Código',
|
||||
'commission_history' => 'Histórico de Comissões',
|
||||
'amount' => 'Valor',
|
||||
'status' => 'Status',
|
||||
'date' => 'Data',
|
||||
'no_commissions_found' => 'Nenhuma comissão encontrada.',
|
||||
'affiliate_profile' => 'Perfil do Afiliado',
|
||||
'name' => 'Nome',
|
||||
'email' => 'Email',
|
||||
'affiliate_code' => 'Código de Afiliado',
|
||||
'commission_rate' => 'Taxa de Comissão',
|
||||
'edit_profile' => 'Editar Perfil',
|
||||
'update_profile' => 'Atualizar Perfil',
|
||||
'profile_updated_success' => 'Perfil atualizado com sucesso!',
|
||||
];
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "affiliates",
|
||||
"display_name": "Área de Afiliados",
|
||||
"description": "Módulo para afiliados gerenciarem sua conta e banners",
|
||||
"version": "1.0.0",
|
||||
"author": "SysCMS Team",
|
||||
"dependencies": ["auth", "affiliatesadmin"],
|
||||
"menu": {
|
||||
"dashboard": {
|
||||
"icon": "bi bi-award",
|
||||
"label": "affiliates.menu.dashboard",
|
||||
"route": "affiliates/dashboard",
|
||||
"order": 51
|
||||
}
|
||||
},
|
||||
"module_active": true,
|
||||
"module_public": true,
|
||||
"module_blocked": false,
|
||||
"ready": true
|
||||
}
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
use Kernel\Application;
|
||||
use Modules\Affiliates\Controllers\AffiliatesController;
|
||||
|
||||
return function (Application $app) {
|
||||
$app->addRoute('GET', '/affiliates/track/{affiliateCode}/{bannerId}', [AffiliatesController::class, 'trackClick']);
|
||||
|
||||
$app->addRoute('GET', '/affiliates', [AffiliatesController::class, 'index']);
|
||||
$app->addRoute('GET', '/affiliates/dashboard', [AffiliatesController::class, 'index']);
|
||||
$app->addRoute('GET', '/affiliates/banners', [AffiliatesController::class, 'banners']);
|
||||
$app->addRoute('GET', '/affiliates/banners/{id:\d+}/code', [AffiliatesController::class, 'bannerCode']);
|
||||
$app->addRoute('GET', '/affiliates/commissions', [AffiliatesController::class, 'commissions']);
|
||||
$app->addRoute('GET', '/affiliates/profile', [AffiliatesController::class, 'profile']);
|
||||
};
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
{% extends "layouts/dashboard.twig" %}
|
||||
|
||||
{% block title %}{{ __("affiliates.banner_code") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">{{ __("affiliates.banner_code_for") }} "{{ banner.name }}"</h3>
|
||||
<div class="card-tools">
|
||||
<a href="/affiliates/banners" class="btn btn-sm btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {{ __("affiliates.back_to_banners") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="bannerCode">{{ __("affiliates.html_code") }}</label>
|
||||
<textarea id="bannerCode" class="form-control" rows="10" readonly>
|
||||
{%- set trackingUrl = base_url ~ '/affiliates/track/' ~ affiliate.affiliate_code ~ '/' ~ banner.id -%}
|
||||
{%- set imageUrl = banner.image_url -%}
|
||||
{%- set targetUrl = banner.target_url -%}
|
||||
{%- set bannerWidth = banner.width -%}
|
||||
{%- set bannerHeight = banner.height -%}
|
||||
<a href="{{ trackingUrl }}" target="_blank">
|
||||
<img src="{{ imageUrl }}" alt="{{ banner.name }}" width="{{ bannerWidth }}" height="{{ bannerHeight }}" />
|
||||
</a>
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="previewBanner">{{ __("affiliates.banner_preview") }}</label>
|
||||
<div id="previewBanner" style="border: 1px solid #ccc; padding: 10px; display: inline-block;">
|
||||
<a href="{{ base_url ~ '/affiliates/track/' ~ affiliate.affiliate_code ~ '/' ~ banner.id }}" target="_blank">
|
||||
<img src="{{ banner.image_url }}" alt="{{ banner.name }}" width="{{ banner.width }}" height="{{ banner.height }}" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
{% extends "layouts/dashboard.twig" %}
|
||||
|
||||
{% block title %}{{ __("affiliates.banners") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">{{ __("affiliates.available_banners") }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if banners is not empty %}
|
||||
<div class="row">
|
||||
{% for banner in banners %}
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card h-100">
|
||||
<img src="{{ banner.image_url }}" class="card-img-top" alt="{{ banner.name }}" style="max-width: 100%; height: auto;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ banner.name }}</h5>
|
||||
<p class="card-text">{{ banner.description }}</p>
|
||||
<p class="card-text"><strong>{{ __("affiliates.size") }}:</strong> {{ banner.width }}x{{ banner.height }}px</p>
|
||||
<a href="/affiliates/banners/{{ banner.id }}/code" class="btn btn-primary btn-sm">{{ __("affiliates.get_code") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>{{ __("affiliates.no_banners_available") }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
{% extends "layouts/dashboard.twig" %}
|
||||
|
||||
{% block title %}{{ __("affiliates.my_commissions") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">{{ __("affiliates.commission_history") }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if commissions is not empty %}
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __("affiliates.commission_id") }}</th>
|
||||
<th>{{ __("affiliates.order_id") }}</th>
|
||||
<th>{{ __("affiliates.amount") }}</th>
|
||||
<th>{{ __("affiliates.commission_rate") }}</th>
|
||||
<th>{{ __("affiliates.status") }}</th>
|
||||
<th>{{ __("affiliates.created_at") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for commission in commissions %}
|
||||
<tr>
|
||||
<td>{{ commission.id }}</td>
|
||||
<td>{{ commission.order_id }}</td>
|
||||
<td>{{ commission.amount|number_format(2, ',', '.') }}</td>
|
||||
<td>{{ commission.commission_rate }}%</td>
|
||||
<td>{{ commission.status }}</td>
|
||||
<td>{{ commission.created_at|date("d/m/Y H:i") }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>{{ __("affiliates.no_commissions_yet") }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
{% extends "layouts/dashboard.twig" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<h1 class="h3 mb-4 text-gray-800">Dashboard do Afiliado</h1>
|
||||
|
||||
<div class="row">
|
||||
<!-- Earnings (Monthly) Card Example -->
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-primary shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
||||
Ganhos Totais</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">R$ {{ totalEarned|number_format(2, ',', '.') }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-dollar-sign fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Earnings (Annual) Card Example -->
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-success shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
|
||||
Comissões Pendentes</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ pendingCommissions }}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-clipboard-list fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Seus Banners</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if banners is not empty %}
|
||||
<div class="row">
|
||||
{% for banner in banners %}
|
||||
<div class="col-lg-4 col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<img class="card-img-top" src="{{ banner.image_url }}" alt="{{ banner.name }}">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ banner.name }}</h5>
|
||||
<p class="card-text">{{ banner.description }}</p>
|
||||
<a href="/affiliates/banners/{{ banner.id }}/code" class="btn btn-primary btn-sm">Obter Código</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Nenhum banner disponível no momento.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
{% extends "layouts/dashboard.twig" %}
|
||||
|
||||
{% block title %}{{ __("affiliates.my_profile") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">{{ __("affiliates.profile_details") }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>{{ __("affiliates.affiliate_code") }}:</strong> {{ affiliate.affiliate_code }}</p>
|
||||
<p><strong>{{ __("affiliates.name") }}:</strong> {{ affiliate.name }}</p>
|
||||
<p><strong>{{ __("affiliates.email") }}:</strong> {{ affiliate.email }}</p>
|
||||
<p><strong>{{ __("affiliates.phone") }}:</strong> {{ affiliate.phone }}</p>
|
||||
<p><strong>{{ __("affiliates.address") }}:</strong> {{ affiliate.address }}</p>
|
||||
<p><strong>{{ __("affiliates.website") }}:</strong> {{ affiliate.website }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>{{ __("affiliates.commission_rate") }}:</strong> {{ affiliate.commission_rate }}%</p>
|
||||
<p><strong>{{ __("affiliates.total_earned") }}:</strong> {{ affiliate.total_earned|number_format(2, ',', '.') }}</p>
|
||||
<p><strong>{{ __("affiliates.total_paid") }}:</strong> {{ affiliate.total_paid|number_format(2, ',', '.') }}</p>
|
||||
<p><strong>{{ __("affiliates.status") }}:</strong> {{ affiliate.status }}</p>
|
||||
<p><strong>{{ __("affiliates.created_at") }}:</strong> {{ affiliate.created_at|date("d/m/Y H:i") }}</p>
|
||||
<p><strong>{{ __("affiliates.updated_at") }}:</strong> {{ affiliate.updated_at|date("d/m/Y H:i") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
+32
@@ -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');
|
||||
}
|
||||
}
|
||||
+30
@@ -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');
|
||||
}
|
||||
}
|
||||
+29
@@ -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');
|
||||
}
|
||||
}
|
||||
+30
@@ -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');
|
||||
}
|
||||
}
|
||||
+233
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!',
|
||||
];
|
||||
Executable
+46
@@ -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"]
|
||||
}
|
||||
Executable
+27
@@ -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
@@ -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 %}
|
||||
Executable
+90
@@ -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
@@ -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 %}
|
||||
Executable
+90
@@ -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 %}
|
||||
Executable
+83
@@ -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 %}
|
||||
Executable
+19
@@ -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>
|
||||
Executable
+39
@@ -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
@@ -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 %}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
SysCMS Commercial License
|
||||
|
||||
Copyright (c) 2026 Marco Costa. All rights reserved.
|
||||
|
||||
This software is licensed, not sold. Permission is granted to use this software subject to the terms of the SysCMS Commercial License.
|
||||
|
||||
Redistribution, sublicensing, resale, or disclosure of this software, in whole or in part, is strictly prohibited without prior written permission from the author.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
# Auth Module for SysCMS
|
||||
|
||||
Authentication and authorization module for user management.
|
||||
|
||||
## Features
|
||||
- User registration and login
|
||||
- Password reset functionality
|
||||
- User profile management
|
||||
- Role-based access control
|
||||
- Email verification (optional)
|
||||
- Session management
|
||||
|
||||
## Installation
|
||||
1. Upload the `auth.zip` file through the module manager or extract it to the `modules/` directory.
|
||||
2. Run the migrations to register the module and create the database tables.
|
||||
3. Configure authentication settings in the admin panel.
|
||||
|
||||
## Permissions
|
||||
- `auth.login`: User login
|
||||
- `auth.logout`: User logout
|
||||
- `auth.register`: User registration
|
||||
- `auth.profile`: View and edit profile
|
||||
- `auth.password.reset`: Request password reset
|
||||
- `auth.password.change`: Change password
|
||||
|
||||
## Configuration
|
||||
The module supports the following settings:
|
||||
- `allow_registration`: Enable/disable user registration
|
||||
- `require_email_verification`: Require email verification before account activation
|
||||
- `password_min_length`: Minimum password length (default: 8)
|
||||
- `session_timeout`: Session timeout in minutes (default: 120)
|
||||
|
||||
## License
|
||||
Copyright (c) 2026 Marco Costa. All rights reserved.
|
||||
Licensed under the SysCMS Commercial License.
|
||||
+1009
File diff suppressed because it is too large
Load Diff
+187
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Auth\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Modules\Auth\Models\User;
|
||||
use Kernel\Application;
|
||||
|
||||
class AuthMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
// Check if user is authenticated
|
||||
if (!$this->isAuthenticated()) {
|
||||
if ($this->expectsJson($request)) {
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'message' => __('auth.unauthenticated'),
|
||||
'timestamp' => time()
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Redirect to login page for web requests
|
||||
return new Response('', 302, [
|
||||
'Location' => url('/login')
|
||||
]);
|
||||
}
|
||||
|
||||
// Check if user session is still valid
|
||||
if (!$this->isSessionValid()) {
|
||||
$this->logout();
|
||||
|
||||
if ($this->expectsJson($request)) {
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'message' => __('auth.session_expired'),
|
||||
'timestamp' => time()
|
||||
], 401);
|
||||
}
|
||||
|
||||
return new Response('', 302, [
|
||||
'Location' => url('/login?expired=1')
|
||||
]);
|
||||
}
|
||||
|
||||
// Set user in request for easy access
|
||||
$request->attributes->set('user', $this->getCurrentUser());
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAuthenticated(): bool
|
||||
{
|
||||
if (isset($_SESSION['user_id']) && !empty($_SESSION['user_id'])) {
|
||||
return true;
|
||||
}
|
||||
if (!empty($_COOKIE['remember_token'])) {
|
||||
$secret = $_ENV['APP_KEY'] ?? '';
|
||||
$parts = explode(':', $_COOKIE['remember_token']);
|
||||
if (count($parts) === 3) {
|
||||
[$id, $token, $sig] = $parts;
|
||||
$payload = $id . ':' . $token;
|
||||
$expected = hash_hmac('sha256', $payload, $secret);
|
||||
if (hash_equals($expected, $sig)) {
|
||||
try {
|
||||
$user = User::find((int)$id);
|
||||
if ($user && $user->users_remember_token === $token) {
|
||||
$_SESSION['user_id'] = $user->users_id;
|
||||
$_SESSION['user_idbus'] = $user->users_bus_id;
|
||||
$_SESSION['user_role'] = $user->users_role;
|
||||
$_SESSION['last_activity'] = time();
|
||||
return true;
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if session is still valid.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isSessionValid(): bool
|
||||
{
|
||||
if (!isset($_SESSION['last_activity'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$timeoutSetting = 120;
|
||||
try {
|
||||
if (class_exists('\App\Models\Module')) {
|
||||
$module = \App\Models\Module::byName('auth')->first();
|
||||
if ($module) {
|
||||
$timeoutSetting = (int)($module->getSetting('session_timeout', 120));
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {}
|
||||
$sessionTimeout = (int)($_ENV['SESSION_LIFETIME'] ?? $timeoutSetting) * 60;
|
||||
$lastActivity = $_SESSION['last_activity'];
|
||||
$currentTime = time();
|
||||
|
||||
// Check if session has expired
|
||||
if (($currentTime - $lastActivity) > $sessionTimeout) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update last activity time
|
||||
$_SESSION['last_activity'] = $currentTime;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current authenticated user.
|
||||
*
|
||||
* @return User|null
|
||||
*/
|
||||
protected function getCurrentUser(): ?User
|
||||
{
|
||||
if (!$this->isAuthenticated()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return User::find($_SESSION['user_id']);
|
||||
} catch (\Exception $e) {
|
||||
// Log error and invalidate session
|
||||
error_log("Error loading user: " . $e->getMessage());
|
||||
$this->logout();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request expects JSON response.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return bool
|
||||
*/
|
||||
protected function expectsJson(Request $request): bool
|
||||
{
|
||||
return $request->headers->get('Accept') === 'application/json' ||
|
||||
$request->headers->get('Content-Type') === 'application/json' ||
|
||||
str_contains($request->getPathInfo(), '/api/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user and destroy session.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function logout(): void
|
||||
{
|
||||
// Unset all session variables
|
||||
unset($_SESSION['user_id']);
|
||||
unset($_SESSION['user_idbus']);
|
||||
unset($_SESSION['user_role']);
|
||||
unset($_SESSION['last_activity']);
|
||||
unset($_SESSION['csrf_token']);
|
||||
|
||||
// Destroy session
|
||||
session_destroy();
|
||||
|
||||
// Start new session for possible flash messages
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+526
@@ -0,0 +1,526 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SysCMS - Content Management System
|
||||
*
|
||||
* Copyright (c) 2025 Marco Costa
|
||||
* All rights reserved.
|
||||
*
|
||||
* Author: Marco Costa
|
||||
* Email: contato@syspanel.com.br
|
||||
* Project: SysCMS
|
||||
*
|
||||
* This software is licensed, not sold.
|
||||
* Permission is granted to use this software
|
||||
* subject to the terms of the SysCMS Commercial License.
|
||||
*
|
||||
* Redistribution, sublicensing, resale, or
|
||||
* disclosure of this software, in whole or in part,
|
||||
* is strictly prohibited without prior written permission.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
||||
*/
|
||||
|
||||
namespace Modules\Auth\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class User extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'users';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'users_id';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'users_name',
|
||||
'users_email',
|
||||
'users_password',
|
||||
'users_role',
|
||||
'users_bus_id',
|
||||
'users_avatar',
|
||||
'users_bio',
|
||||
'users_phone',
|
||||
'users_email_verified_at',
|
||||
'users_last_login_at',
|
||||
'users_status',
|
||||
'users_preferences'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for arrays.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = [
|
||||
'users_password',
|
||||
'remember_token'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'users_email_verified_at' => 'datetime',
|
||||
'users_last_login_at' => 'datetime',
|
||||
'users_preferences' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = [
|
||||
'users_email_verified_at',
|
||||
'users_last_login_at',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* Boot the model
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// Add company scope by default
|
||||
static::addGlobalScope('company', function ($query) {
|
||||
if (isset($_SESSION['user_idbus']) && $_SESSION['user_idbus'] !== null && $_SESSION['user_role'] !== 'super_admin') {
|
||||
$query->where('users_bus_id', $_SESSION['user_idbus']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the company that owns the user.
|
||||
*/
|
||||
public function company(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\Modules\Company\Models\Company::class, 'users_bus_id', 'bus_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permissions for the user.
|
||||
*/
|
||||
public function permissions(): HasMany
|
||||
{
|
||||
return $this->hasMany(\App\Models\UserPermission::class, 'user_permissions_user_id', 'users_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the activity logs for the user.
|
||||
*/
|
||||
public function activityLogs(): HasMany
|
||||
{
|
||||
return $this->hasMany(\App\Models\ActivityLog::class, 'activity_logs_user_id', 'users_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is super admin
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSuperAdmin(): bool
|
||||
{
|
||||
return $this->users_role === 'super_admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is admin
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->users_role === 'admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is member
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isMember(): bool
|
||||
{
|
||||
return $this->users_role === 'member';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is regular user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isUser(): bool
|
||||
{
|
||||
return $this->users_role === 'user';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has specific permission
|
||||
*
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPermission(string $permission): bool
|
||||
{
|
||||
// Super admin has all permissions
|
||||
if ($this->isSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Admin role has full access within their company scope
|
||||
if ($this->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check direct permissions
|
||||
$hasDirectPermission = \App\Models\UserPermission::where('user_permissions_user_id', $this->users_id)
|
||||
->where('user_permissions_permission', $permission)
|
||||
->where('user_permissions_bus_id', $this->users_bus_id)
|
||||
->where(function ($query) {
|
||||
$query->whereNull('user_permissions_expires_at')
|
||||
->orWhere('user_permissions_expires_at', '>', \Carbon\Carbon::now());
|
||||
})
|
||||
->exists();
|
||||
|
||||
if ($hasDirectPermission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check role permissions
|
||||
return \App\Models\RolePermission::where('role_permissions_role', $this->users_role)
|
||||
->where('role_permissions_bus_id', $this->users_bus_id)
|
||||
->where('role_permissions_permission', $permission)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all permissions for the user as an array of strings.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPermissions(): array
|
||||
{
|
||||
// Se for Super Admin, retorna um token que indica todas as permissões
|
||||
// (Ou poderíamos retornar todas as permissões do sistema se necessário)
|
||||
if ($this->isSuperAdmin()) {
|
||||
return ['*'];
|
||||
}
|
||||
|
||||
// Se for Admin, retorna todas as permissões da empresa (ou um token)
|
||||
if ($this->isAdmin()) {
|
||||
return ['*.company'];
|
||||
}
|
||||
|
||||
$directPermissions = \App\Models\UserPermission::where('user_permissions_user_id', $this->users_id)
|
||||
->where('user_permissions_bus_id', $this->users_bus_id)
|
||||
->where(function ($query) {
|
||||
$query->whereNull('user_permissions_expires_at')
|
||||
->orWhere('user_permissions_expires_at', '>', \Carbon\Carbon::now());
|
||||
})
|
||||
->pluck('user_permissions_permission')
|
||||
->toArray();
|
||||
|
||||
$rolePermissions = \App\Models\RolePermission::where('role_permissions_role', $this->users_role)
|
||||
->where('role_permissions_bus_id', $this->users_bus_id)
|
||||
->pluck('role_permissions_permission')
|
||||
->toArray();
|
||||
|
||||
return array_unique(array_merge($directPermissions, $rolePermissions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the avatar URL attribute
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAvatarUrlAttribute(): string
|
||||
{
|
||||
if ($this->users_avatar) {
|
||||
return url('storage/uploads/avatars/' . $this->users_avatar);
|
||||
}
|
||||
|
||||
return asset_url('img/default-avatar.png');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDisplayNameAttribute(): string
|
||||
{
|
||||
return $this->users_name ?: $this->users_email;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get id attribute alias
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getIdAttribute(): int
|
||||
{
|
||||
return $this->users_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash the password before saving
|
||||
*
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public function setUsersPasswordAttribute($value): void
|
||||
{
|
||||
$this->attributes['users_password'] = password_hash($value, PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for setting password via 'password' attribute
|
||||
*
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public function setPasswordAttribute($value): void
|
||||
{
|
||||
$this->setUsersPasswordAttribute($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the password attribute for authentication
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUsersPasswordAttribute(): string
|
||||
{
|
||||
return $this->attributes['users_password'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for getting password via 'password' attribute
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPasswordAttribute(): string
|
||||
{
|
||||
return $this->getUsersPasswordAttribute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the email attribute
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEmailAttribute(): string
|
||||
{
|
||||
return $this->attributes['users_email'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name attribute
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNameAttribute(): string
|
||||
{
|
||||
return $this->attributes['users_name'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role attribute
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRoleAttribute(): string
|
||||
{
|
||||
return $this->attributes['users_role'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status attribute
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getStatusAttribute(): string
|
||||
{
|
||||
return $this->attributes['users_status'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the email_verified_at attribute
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getEmailVerifiedAtAttribute(): ?string
|
||||
{
|
||||
return $this->attributes['users_email_verified_at'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the email_verified_at attribute
|
||||
*
|
||||
* @param string|null $value
|
||||
* @return void
|
||||
*/
|
||||
public function setEmailVerifiedAtAttribute($value): void
|
||||
{
|
||||
$this->attributes['users_email_verified_at'] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the avatar attribute
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAvatarAttribute(): ?string
|
||||
{
|
||||
return $this->attributes['users_avatar'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the avatar attribute
|
||||
*
|
||||
* @param string|null $value
|
||||
* @return void
|
||||
*/
|
||||
public function setAvatarAttribute($value): void
|
||||
{
|
||||
$this->attributes['users_avatar'] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bio attribute
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getBioAttribute(): ?string
|
||||
{
|
||||
return $this->attributes['users_bio'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bio attribute
|
||||
*
|
||||
* @param string|null $value
|
||||
* @return void
|
||||
*/
|
||||
public function setBioAttribute($value): void
|
||||
{
|
||||
$this->attributes['users_bio'] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the phone attribute
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPhoneAttribute(): ?string
|
||||
{
|
||||
return $this->attributes['users_phone'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the phone attribute
|
||||
*
|
||||
* @param string|null $value
|
||||
* @return void
|
||||
*/
|
||||
public function setPhoneAttribute($value): void
|
||||
{
|
||||
$this->attributes['users_phone'] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preferences attribute
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getPreferencesAttribute(): ?array
|
||||
{
|
||||
$value = $this->attributes['users_preferences'] ?? null;
|
||||
return $value ? json_decode($value, true) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the preferences attribute
|
||||
*
|
||||
* @param array|null $value
|
||||
* @return void
|
||||
*/
|
||||
public function setPreferencesAttribute($value): void
|
||||
{
|
||||
$this->attributes['users_preferences'] = is_array($value) ? json_encode($value) : $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users by company
|
||||
*
|
||||
* @param int $companyId
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public static function getByCompany(int $companyId)
|
||||
{
|
||||
return self::where('users_bus_id', $companyId)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active users only
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('users_status', 'active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users by role
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $role
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeRole($query, string $role)
|
||||
{
|
||||
return $query->where('users_role', $role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last login timestamp
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function updateLastLogin(): bool
|
||||
{
|
||||
return $this->update(['users_last_login_at' => \Carbon\Carbon::now()]);
|
||||
}
|
||||
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Auth\Providers;
|
||||
|
||||
use Kernel\Application;
|
||||
use Kernel\View;
|
||||
use Modules\Auth\Models\User;
|
||||
use Modules\Auth\Middleware\AuthMiddleware;
|
||||
|
||||
class AuthServiceProvider
|
||||
{
|
||||
public function register()
|
||||
{
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
$app = Application::getInstance();
|
||||
|
||||
// Register Middleware
|
||||
$app->addProtectedMiddleware(new AuthMiddleware());
|
||||
|
||||
// Register Public Routes
|
||||
$app->addPublicPath('/login');
|
||||
$app->addPublicPath('/register');
|
||||
$app->addPublicPath('/forgot-password');
|
||||
$app->addPublicPath('/reset-password*'); // Prefix
|
||||
$app->addPublicPath('/verify-email*'); // Prefix
|
||||
|
||||
// Inject Auth Global
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
try {
|
||||
if (class_exists(User::class)) {
|
||||
$user = User::find($_SESSION['user_id']);
|
||||
if ($user) {
|
||||
$view = new View();
|
||||
$view->getEngine()->addGlobal('auth', ['user' => $user]);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateUsersTable
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
if (!Capsule::schema()->hasTable('users')) {
|
||||
Capsule::schema()->create('users', function (Blueprint $table) {
|
||||
$table->increments('users_id');
|
||||
$table->string('users_name');
|
||||
$table->string('users_email')->unique();
|
||||
$table->string('users_password');
|
||||
$table->string('users_role')->default('user'); // super_admin, admin, member, user
|
||||
$table->integer('users_bus_id')->default(0)->index();
|
||||
$table->string('users_avatar')->nullable();
|
||||
$table->text('users_bio')->nullable();
|
||||
$table->string('users_phone')->nullable();
|
||||
$table->timestamp('users_email_verified_at')->nullable();
|
||||
$table->timestamp('users_last_login_at')->nullable();
|
||||
$table->string('users_status')->default('active'); // active, inactive, banned
|
||||
$table->json('users_preferences')->nullable();
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Capsule::schema()->dropIfExists('users');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateAuthPermissionsTables
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
// User Permissions
|
||||
if (!Capsule::schema()->hasTable('user_permissions')) {
|
||||
Capsule::schema()->create('user_permissions', function (Blueprint $table) {
|
||||
$table->increments('user_permissions_id');
|
||||
$table->integer('user_permissions_user_id')->unsigned();
|
||||
$table->integer('user_permissions_bus_id')->unsigned()->nullable(); // Permission might be global or company specific
|
||||
$table->string('user_permissions_permission');
|
||||
$table->integer('user_permissions_granted_by')->unsigned()->nullable();
|
||||
$table->timestamp('user_permissions_granted_at')->useCurrent();
|
||||
$table->timestamp('user_permissions_expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_permissions_user_id')->references('users_id')->on('users')->onDelete('cascade');
|
||||
// Optional: foreign key for bus_id if module order permits
|
||||
});
|
||||
}
|
||||
|
||||
// Role Permissions
|
||||
if (!Capsule::schema()->hasTable('role_permissions')) {
|
||||
Capsule::schema()->create('role_permissions', function (Blueprint $table) {
|
||||
$table->increments('role_permissions_id');
|
||||
$table->string('role_permissions_role');
|
||||
$table->integer('role_permissions_bus_id')->unsigned()->nullable();
|
||||
$table->string('role_permissions_permission');
|
||||
$table->integer('role_permissions_created_by')->unsigned()->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
// Activity Logs
|
||||
if (!Capsule::schema()->hasTable('activity_logs')) {
|
||||
Capsule::schema()->create('activity_logs', function (Blueprint $table) {
|
||||
$table->increments('activity_logs_id');
|
||||
$table->integer('activity_logs_user_id')->unsigned()->nullable();
|
||||
$table->integer('activity_logs_bus_id')->unsigned()->nullable();
|
||||
$table->string('activity_logs_action');
|
||||
$table->string('activity_logs_resource_type')->nullable();
|
||||
$table->string('activity_logs_resource_id')->nullable();
|
||||
$table->json('activity_logs_old_values')->nullable();
|
||||
$table->json('activity_logs_new_values')->nullable();
|
||||
$table->string('activity_logs_ip_address')->nullable();
|
||||
$table->string('activity_logs_user_agent')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('activity_logs_user_id');
|
||||
$table->index('activity_logs_bus_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Capsule::schema()->dropIfExists('activity_logs');
|
||||
Capsule::schema()->dropIfExists('role_permissions');
|
||||
Capsule::schema()->dropIfExists('user_permissions');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateAuthAttemptsTable
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Capsule::schema()->hasTable('auth_attempts')) {
|
||||
Capsule::schema()->create('auth_attempts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('email')->nullable();
|
||||
$table->string('ip')->index();
|
||||
$table->string('action')->index();
|
||||
$table->boolean('success')->default(false);
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Capsule::schema()->dropIfExists('auth_attempts');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use App\Models\Module;
|
||||
use Modules\Company\Models\Company;
|
||||
|
||||
class InsertAuthModule
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$moduleData = [
|
||||
'modules_name' => 'auth',
|
||||
'modules_display_name' => 'Authentication',
|
||||
'modules_description' => 'Authentication and authorization module for user management',
|
||||
'modules_version' => '1.0.0',
|
||||
'modules_author' => 'SysCMS Team',
|
||||
'modules_ative' => true,
|
||||
'modules_public' => true,
|
||||
'modules_blocked' => true, // Core module, cannot be disabled easily
|
||||
'modules_dependencies' => [],
|
||||
'modules_permissions' => [
|
||||
"auth.login",
|
||||
"auth.logout",
|
||||
"auth.register",
|
||||
"auth.profile",
|
||||
"auth.password.reset",
|
||||
"auth.password.change"
|
||||
],
|
||||
'modules_settings' => [
|
||||
"allow_registration" => true,
|
||||
"require_email_verification" => false,
|
||||
"password_min_length" => 8,
|
||||
"session_timeout" => 120
|
||||
],
|
||||
'modules_path' => 'modules/auth',
|
||||
'modules_namespace' => 'Modules\\Auth',
|
||||
'modules_icon' => 'bi bi-person-check',
|
||||
'menu_side_bar' => true,
|
||||
'menu_nav_bar' => true,
|
||||
'modules_order' => 1,
|
||||
'modules_install_date' => date('Y-m-d H:i:s'),
|
||||
'modules_update_date' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// Check if module already exists
|
||||
$existingModule = Module::where('modules_name', 'auth')->first();
|
||||
|
||||
if (!$existingModule) {
|
||||
$module = Module::create($moduleData);
|
||||
|
||||
// Associate with all existing companies (bus_modules)
|
||||
// Assuming Company model and table exist.
|
||||
if (Capsule::schema()->hasTable('bus')) {
|
||||
$companies = Company::all();
|
||||
foreach ($companies as $company) {
|
||||
$exists = Capsule::table('bus_modules')
|
||||
->where('bus_modules_bus_id', $company->bus_id)
|
||||
->where('bus_modules_module_id', $module->modules_id)
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
Capsule::table('bus_modules')->insert([
|
||||
'bus_modules_bus_id' => $company->bus_id,
|
||||
'bus_modules_module_id' => $module->modules_id,
|
||||
'bus_modules_is_enabled' => true,
|
||||
'bus_modules_settings' => json_encode([]),
|
||||
'bus_modules_enabled_by' => 1,
|
||||
'bus_modules_enabled_at' => date('Y-m-d H:i:s'),
|
||||
'bus_modules_show_public_nav' => true,
|
||||
'bus_modules_show_dashboard_nav' => true,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$module = Module::where('modules_name', 'auth')->first();
|
||||
if ($module) {
|
||||
Capsule::table('bus_modules')->where('bus_modules_module_id', $module->modules_id)->delete();
|
||||
$module->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Authentifizierung",
|
||||
"login": "Anmeldung"
|
||||
},
|
||||
"login_title": "Anmeldung - :platformName",
|
||||
"register_title": "Registrierung - :platformName",
|
||||
"profile_title": "Mein Profil",
|
||||
"forgot_password_title": "Passwort vergessen",
|
||||
"reset_password_title": "Passwort zurücksetzen",
|
||||
"email": "E-Mail",
|
||||
"name": "Name",
|
||||
"password": "Passwort",
|
||||
"password_confirmation": "Passwort Bestätigung",
|
||||
"email_placeholder": "Geben Sie Ihre E-Mail ein",
|
||||
"password_placeholder": "Passwort eingeben",
|
||||
"password_confirmation_placeholder": "Passwort bestätigen",
|
||||
"name_placeholder": "Name eingeben",
|
||||
"current_password_placeholder": "Aktuelles Passwort",
|
||||
"new_password_placeholder": "Neues Passwort",
|
||||
"new_password_confirmation_placeholder": "Neues Passwort bestätigen",
|
||||
"accept_terms": "Ich akzeptiere die Bedingungen",
|
||||
"read_terms": "Bedingungen lesen",
|
||||
"privacy_policy": "Datenschutz",
|
||||
"terms_of_service": "Nutzungsbedingungen",
|
||||
"cookie_policy": "Cookie-Richtlinie",
|
||||
"data_protection": "Datenschutz",
|
||||
"remember_me": "Angemeldet bleiben",
|
||||
"login_button": "Anmelden",
|
||||
"register_button": "Registrieren",
|
||||
"forgot_password": "Passwort vergessen?",
|
||||
"update_profile": "Profil aktualisieren",
|
||||
"back_to_login": "Zur Anmeldung",
|
||||
"logout": "Abmelden",
|
||||
"send_reset_link": "Link zum Zurücksetzen senden",
|
||||
"reset_token": "Zurücksetzungs-Token",
|
||||
"reset_email": "E-Mail zum Zurücksetzen",
|
||||
"reset_token_invalid": "Ungültiger Token",
|
||||
"reset_token_expired": "Token abgelaufen",
|
||||
"reset_email_invalid": "Ungültige E-Mail",
|
||||
"reset_password_button": "Passwort zurücksetzen",
|
||||
"invalid_credentials": "Ungültige Anmeldedaten",
|
||||
"account_inactive": "Konto inaktiv",
|
||||
"session_expired": "Sitzung abgelaufen. Bitte erneut anmelden.",
|
||||
"registration_pending": "Registrierung ausstehend",
|
||||
"registration_error": "Registrierungsfehler",
|
||||
"registration_success": "Registrierung erfolgreich. Bitte anmelden.",
|
||||
"logout_success": "Erfolgreich abgemeldet",
|
||||
"profile_updated": "Profil aktualisiert",
|
||||
"reset_link_sent": "Link zum Zurücksetzen gesendet",
|
||||
"current_avatar": "Aktueller Avatar",
|
||||
"close": "Schließen",
|
||||
"security_check": "Sicherheitsüberprüfung",
|
||||
"captcha_solved": "Richtig!",
|
||||
"captcha_instruction": "Lösen Sie die Summe, um fortzufahren.",
|
||||
"captcha_placeholder": "Ergebnis eingeben",
|
||||
"captcha_question": "Wie viel ist :a + :b?",
|
||||
"invalid_captcha": "Ungültiges Captcha",
|
||||
"email_verified_success": "E-Mail bestätigt. Bitte anmelden.",
|
||||
"verification_email_sent": "Bestätigungs-E-Mail gesendet.",
|
||||
"resend_verification": "Bestätigung erneut senden",
|
||||
"too_many_attempts": "Zu viele Versuche. Später erneut probieren.",
|
||||
"captcha_reload": "Herausforderung neu laden",
|
||||
"terms_and_conditions": "Allgemeine Geschäftsbedingungen",
|
||||
"privacy_policy_text": "Text der Datenschutzerklärung...",
|
||||
"terms_of_service_text": "Text der Nutzungsbedingungen...",
|
||||
"cookie_policy_text": "Text der Cookie-Richtlinie...",
|
||||
"data_protection_text": "Text zum Datenschutz...",
|
||||
"phone": "Telefon",
|
||||
"bio": "Biografie",
|
||||
"current_password": "Aktuelles Passwort",
|
||||
"avatar": "Avatar",
|
||||
"cancel": "Abbrechen",
|
||||
"preview": "Vorschau",
|
||||
"email_already_verified": "Este e-mail já foi verificado. Faça login.",
|
||||
"email_not_found": "E-mail não encontrado."
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"title": "Authentifizierung",
|
||||
"description": "Systemauthentifizierungs- und Autorisierungsmodul",
|
||||
"welcome": "Willkommen beim Content Management System",
|
||||
"login_required": "Anmeldung erforderlich, um auf diese Seite zuzugreifen",
|
||||
"access_denied": "Zugriff verweigert",
|
||||
"invalid_credentials": "Ungültige Anmeldedaten",
|
||||
"session_expired": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an."
|
||||
}
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Authentication",
|
||||
"login": "Login"
|
||||
},
|
||||
"login_title": "Login - :platformName",
|
||||
"register_title": "Register - :platformName",
|
||||
"profile_title": "My Profile",
|
||||
"forgot_password_title": "Forgot Password",
|
||||
"reset_password_title": "Reset Password",
|
||||
"email": "Email",
|
||||
"name": "Name",
|
||||
"password": "Password",
|
||||
"password_confirmation": "Password Confirmation",
|
||||
"email_placeholder": "Enter your email",
|
||||
"password_placeholder": "Enter your password",
|
||||
"password_confirmation_placeholder": "Confirm your password",
|
||||
"name_placeholder": "Enter your name",
|
||||
"current_password_placeholder": "Current password",
|
||||
"new_password_placeholder": "New password",
|
||||
"new_password_confirmation_placeholder": "Confirm new password",
|
||||
"accept_terms": "I accept the terms",
|
||||
"read_terms": "Read terms",
|
||||
"privacy_policy": "Privacy Policy",
|
||||
"terms_of_service": "Terms of Service",
|
||||
"cookie_policy": "Cookie Policy",
|
||||
"data_protection": "Data Protection",
|
||||
"remember_me": "Remember me",
|
||||
"login_button": "Sign In",
|
||||
"register_button": "Register",
|
||||
"forgot_password": "Forgot your password?",
|
||||
"update_profile": "Update Profile",
|
||||
"back_to_login": "Back to Login",
|
||||
"logout": "Logout",
|
||||
"send_reset_link": "Send Reset Link",
|
||||
"reset_token": "Reset Token",
|
||||
"reset_email": "Reset Email",
|
||||
"reset_token_invalid": "Invalid token",
|
||||
"reset_token_expired": "Token expired",
|
||||
"reset_email_invalid": "Invalid email",
|
||||
"reset_password_button": "Reset Password",
|
||||
"invalid_credentials": "Invalid credentials",
|
||||
"account_inactive": "Inactive account",
|
||||
"session_expired": "Session expired. Please log in again.",
|
||||
"registration_pending": "Registration pending",
|
||||
"registration_error": "Registration error",
|
||||
"registration_success": "Registration successful! Please log in to continue.",
|
||||
"logout_success": "Logout successful",
|
||||
"profile_updated": "Profile updated successfully",
|
||||
"reset_link_sent": "Reset link sent successfully",
|
||||
"current_avatar": "Current avatar",
|
||||
"close": "Close",
|
||||
"security_check": "Security Check",
|
||||
"captcha_solved": "Correct!",
|
||||
"captcha_instruction": "Solve the sum to continue.",
|
||||
"captcha_placeholder": "Enter the result",
|
||||
"captcha_question": "What is :a + :b?",
|
||||
"invalid_captcha": "Invalid captcha",
|
||||
"email_verified_success": "Email verified successfully. Please log in.",
|
||||
"verification_email_sent": "Verification email sent. Check your inbox.",
|
||||
"resend_verification": "Resend verification",
|
||||
"too_many_attempts": "Too many attempts. Please try again later.",
|
||||
"captcha_reload": "Reload challenge",
|
||||
"terms_and_conditions": "Terms and Conditions",
|
||||
"privacy_policy_text": "Privacy policy text...",
|
||||
"terms_of_service_text": "Terms of service text...",
|
||||
"cookie_policy_text": "Cookie policy text...",
|
||||
"data_protection_text": "Data protection text...",
|
||||
"phone": "Phone",
|
||||
"bio": "Biography",
|
||||
"current_password": "Current Password",
|
||||
"avatar": "Avatar",
|
||||
"cancel": "Cancel",
|
||||
"preview": "Preview",
|
||||
"email_already_verified": "Este e-mail já foi verificado. Faça login.",
|
||||
"email_not_found": "E-mail não encontrado."
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"title": "Authentication",
|
||||
"description": "System authentication and authorization module",
|
||||
"welcome": "Welcome to the Content Management System",
|
||||
"login_required": "Login required to access this page",
|
||||
"access_denied": "Access denied",
|
||||
"invalid_credentials": "Invalid credentials",
|
||||
"session_expired": "Your session has expired. Please log in again."
|
||||
}
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Autenticación",
|
||||
"login": "Login"
|
||||
},
|
||||
"login_title": "Inicio de sesión - :platformName",
|
||||
"register_title": "Registro - :platformName",
|
||||
"profile_title": "Mi Perfil",
|
||||
"forgot_password_title": "Olvidé mi contraseña",
|
||||
"reset_password_title": "Restablecer contraseña",
|
||||
"email": "Correo Electrónico",
|
||||
"name": "Nombre",
|
||||
"password": "Contraseña",
|
||||
"password_confirmation": "Confirmación de Contraseña",
|
||||
"email_placeholder": "Introduce tu correo",
|
||||
"password_placeholder": "Introduce tu contraseña",
|
||||
"password_confirmation_placeholder": "Confirma tu contraseña",
|
||||
"name_placeholder": "Introduce tu nombre",
|
||||
"current_password_placeholder": "Contraseña actual",
|
||||
"new_password_placeholder": "Nueva contraseña",
|
||||
"new_password_confirmation_placeholder": "Confirmar nueva contraseña",
|
||||
"accept_terms": "Acepto los términos",
|
||||
"read_terms": "Leer términos",
|
||||
"privacy_policy": "Política de privacidad",
|
||||
"terms_of_service": "Términos de servicio",
|
||||
"cookie_policy": "Política de cookies",
|
||||
"data_protection": "Protección de datos",
|
||||
"remember_me": "Recordarme",
|
||||
"login_button": "Entrar",
|
||||
"register_button": "Registrar",
|
||||
"forgot_password": "¿Olvidaste tu contraseña?",
|
||||
"update_profile": "Actualizar perfil",
|
||||
"back_to_login": "Volver al inicio",
|
||||
"logout": "Salir",
|
||||
"send_reset_link": "Enviar enlace de restablecimiento",
|
||||
"reset_token": "Token de restablecimiento",
|
||||
"reset_email": "Correo para restablecer",
|
||||
"reset_token_invalid": "Token inválido",
|
||||
"reset_token_expired": "Token expirado",
|
||||
"reset_email_invalid": "Correo inválido",
|
||||
"reset_password_button": "Restablecer contraseña",
|
||||
"invalid_credentials": "Credenciales inválidas",
|
||||
"account_inactive": "Cuenta inactiva",
|
||||
"session_expired": "Sesión expirada. Por favor, inicia sesión nuevamente.",
|
||||
"registration_pending": "Registro pendiente",
|
||||
"registration_error": "Error en el registro",
|
||||
"registration_success": "Registro exitoso. Inicia sesión para continuar.",
|
||||
"logout_success": "Salida exitosa",
|
||||
"profile_updated": "Perfil actualizado",
|
||||
"reset_link_sent": "Enlace de restablecimiento enviado",
|
||||
"current_avatar": "Avatar actual",
|
||||
"close": "Cerrar",
|
||||
"security_check": "Verificación de seguridad",
|
||||
"captcha_solved": "¡Correcto!",
|
||||
"captcha_instruction": "Resuelve la suma para continuar.",
|
||||
"captcha_placeholder": "Introduce el resultado",
|
||||
"captcha_question": "¿Cuánto es :a + :b?",
|
||||
"invalid_captcha": "Captcha inválido",
|
||||
"email_verified_success": "Correo verificado. Inicia sesión.",
|
||||
"verification_email_sent": "Correo de verificación enviado.",
|
||||
"resend_verification": "Reenviar verificación",
|
||||
"too_many_attempts": "Demasiados intentos. Inténtalo más tarde.",
|
||||
"captcha_reload": "Recargar desafío",
|
||||
"terms_and_conditions": "Términos y Condiciones",
|
||||
"privacy_policy_text": "Texto de la política de privacidad...",
|
||||
"terms_of_service_text": "Texto de los términos de servicio...",
|
||||
"cookie_policy_text": "Texto de la política de cookies...",
|
||||
"data_protection_text": "Texto de protección de datos...",
|
||||
"phone": "Teléfono",
|
||||
"bio": "Biografía",
|
||||
"current_password": "Contraseña actual",
|
||||
"avatar": "Avatar",
|
||||
"cancel": "Cancelar",
|
||||
"preview": "Vista previa",
|
||||
"email_already_verified": "Este e-mail já foi verificado. Faça login.",
|
||||
"email_not_found": "E-mail não encontrado."
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"title": "Autenticación",
|
||||
"description": "Módulo de autenticación y autorización del sistema",
|
||||
"welcome": "Bienvenido al Sistema de Gestión de Contenidos",
|
||||
"login_required": "Es necesario iniciar sesión para acceder a esta página",
|
||||
"access_denied": "Acceso denegado",
|
||||
"invalid_credentials": "Credenciales inválidas",
|
||||
"session_expired": "Tu sesión ha expirado. Por favor, inicia sesión de nuevo."
|
||||
}
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Authentification",
|
||||
"login": "Connexion"
|
||||
},
|
||||
"login_title": "Connexion - :platformName",
|
||||
"register_title": "Inscription - :platformName",
|
||||
"profile_title": "Mon Profil",
|
||||
"forgot_password_title": "Mot de passe oublié",
|
||||
"reset_password_title": "Réinitialiser le mot de passe",
|
||||
"email": "Email",
|
||||
"name": "Nom",
|
||||
"password": "Mot de passe",
|
||||
"password_confirmation": "Confirmation du mot de passe",
|
||||
"email_placeholder": "Entrez votre email",
|
||||
"password_placeholder": "Saisissez votre mot de passe",
|
||||
"password_confirmation_placeholder": "Confirmez votre mot de passe",
|
||||
"name_placeholder": "Saisissez votre nom",
|
||||
"current_password_placeholder": "Mot de passe actuel",
|
||||
"new_password_placeholder": "Nouveau mot de passe",
|
||||
"new_password_confirmation_placeholder": "Confirmez le nouveau mot de passe",
|
||||
"accept_terms": "J'accepte les conditions",
|
||||
"read_terms": "Lire les conditions",
|
||||
"privacy_policy": "Politique de confidentialité",
|
||||
"terms_of_service": "Conditions d'utilisation",
|
||||
"cookie_policy": "Politique de cookies",
|
||||
"data_protection": "Protection des données",
|
||||
"remember_me": "Se souvenir de moi",
|
||||
"login_button": "Se connecter",
|
||||
"register_button": "S'inscrire",
|
||||
"forgot_password": "Mot de passe oublié ?",
|
||||
"update_profile": "Mettre à jour le profil",
|
||||
"back_to_login": "Retour à la connexion",
|
||||
"logout": "Se déconnecter",
|
||||
"send_reset_link": "Envoyer le lien de réinitialisation",
|
||||
"reset_token": "Jeton de réinitialisation",
|
||||
"reset_email": "E-mail de réinitialisation",
|
||||
"reset_token_invalid": "Jeton invalide",
|
||||
"reset_token_expired": "Jeton expiré",
|
||||
"reset_email_invalid": "E-mail invalide",
|
||||
"reset_password_button": "Réinitialiser le mot de passe",
|
||||
"invalid_credentials": "Identifiants invalides",
|
||||
"account_inactive": "Compte inactif",
|
||||
"session_expired": "Session expirée. Veuillez vous reconnecter.",
|
||||
"registration_pending": "Inscription en attente",
|
||||
"registration_error": "Erreur d'inscription",
|
||||
"registration_success": "Inscription réussie ! Connectez-vous pour continuer.",
|
||||
"logout_success": "Déconnexion réussie",
|
||||
"profile_updated": "Profil mis à jour avec succès",
|
||||
"reset_link_sent": "Lien de réinitialisation envoyé avec succès",
|
||||
"current_avatar": "Avatar actuel",
|
||||
"close": "Fermer",
|
||||
"security_check": "Vérification de sécurité",
|
||||
"captcha_solved": "Correct !",
|
||||
"captcha_instruction": "Résolvez l'addition pour continuer.",
|
||||
"invalid_captcha": "Captcha invalide",
|
||||
"email_verified_success": "E-mail confirmé avec succès. Connectez-vous.",
|
||||
"verification_email_sent": "E-mail de confirmation envoyé. Vérifiez votre boîte de réception.",
|
||||
"email_already_verified": "Cet e-mail a déjà été vérifié. Connectez-vous.",
|
||||
"email_not_found": "E-mail introuvable.",
|
||||
"resend_verification": "Renvoyer la confirmation",
|
||||
"too_many_attempts": "Trop de tentatives. Réessayez plus tard.",
|
||||
"captcha_reload": "Recharger le défi",
|
||||
"terms_and_conditions": "Termes et Conditions",
|
||||
"privacy_policy_text": "Texte de la politique de confidentialité...",
|
||||
"terms_of_service_text": "Texte des conditions d'utilisation...",
|
||||
"cookie_policy_text": "Texte de la politique de cookies...",
|
||||
"data_protection_text": "Texte de la protection des données...",
|
||||
"phone": "Téléphone",
|
||||
"bio": "Biographie",
|
||||
"current_password": "Mot de passe actuel",
|
||||
"avatar": "Avatar",
|
||||
"cancel": "Annuler",
|
||||
"preview": "Aperçu",
|
||||
"captcha_question": "What is :a + :b?",
|
||||
"captcha_placeholder": "Enter the result"
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"title": "Authentification",
|
||||
"description": "Module d'authentification et d'autorisation",
|
||||
"welcome": "Bienvenue dans le système de gestion de contenu",
|
||||
"login_required": "Connexion requise pour accéder à cette page",
|
||||
"access_denied": "Accès refusé",
|
||||
"invalid_credentials": "Identifiants invalides",
|
||||
"session_expired": "Votre session a expiré. Veuillez vous reconnecter."
|
||||
}
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Autenticação",
|
||||
"login": "Login"
|
||||
},
|
||||
"login_title": "Login - :platformName",
|
||||
"register_title": "Cadastro - :platformName",
|
||||
"profile_title": "Meu Perfil",
|
||||
"forgot_password_title": "Esqueci minha senha",
|
||||
"reset_password_title": "Redefinir senha",
|
||||
"password_reset_success": "Senha redefinida com sucesso! Faça login para continuar.",
|
||||
"password_confirmation_error": "As senhas não coincidem.",
|
||||
"weak_password_error": "A senha deve ter pelo menos 8 caracteres.",
|
||||
"reset_error": "Ocorreu um erro ao redefinir sua senha. Verifique os dados e tente novamente.",
|
||||
"email": "E-mail",
|
||||
"name": "Nome",
|
||||
"password": "Senha",
|
||||
"password_confirmation": "Confirmação de Senha",
|
||||
"email_placeholder": "Digite seu email",
|
||||
"password_placeholder": "Digite sua senha",
|
||||
"password_confirmation_placeholder": "Confirme sua senha",
|
||||
"name_placeholder": "Digite seu nome",
|
||||
"current_password_placeholder": "Senha atual",
|
||||
"new_password_placeholder": "Nova senha",
|
||||
"new_password_confirmation_placeholder": "Confirme a nova senha",
|
||||
"accept_terms": "Aceito os termos",
|
||||
"read_terms": "Ler os termos",
|
||||
"privacy_policy": "Política de Privacidade",
|
||||
"terms_of_service": "Termos de Serviço",
|
||||
"cookie_policy": "Política de Cookies",
|
||||
"data_protection": "Proteção de Dados",
|
||||
"remember_me": "Lembrar-me",
|
||||
"login_button": "Entrar",
|
||||
"register_button": "Cadastrar",
|
||||
"forgot_password": "Esqueceu a senha?",
|
||||
"update_profile": "Atualizar Perfil",
|
||||
"back_to_login": "Voltar ao Login",
|
||||
"logout": "Sair",
|
||||
"send_reset_link": "Enviar Link de Redefinição",
|
||||
"reset_token": "Token de Redefinição",
|
||||
"reset_email": "Email para Redefinição",
|
||||
"reset_token_invalid": "Token inválido",
|
||||
"reset_token_expired": "Token expirado",
|
||||
"reset_email_invalid": "Email inválido",
|
||||
"reset_password_button": "Redefinir Senha",
|
||||
"invalid_credentials": "Credenciais inválidas",
|
||||
"account_inactive": "Conta inativa",
|
||||
"session_expired": "Sessão expirada. Por favor, faça login novamente.",
|
||||
"registration_pending": "Cadastro pendente",
|
||||
"registration_error": "Erro no cadastro",
|
||||
"registration_success": "Cadastro realizado com sucesso! Faça login para continuar.",
|
||||
"logout_success": "Logout realizado com sucesso",
|
||||
"profile_updated": "Perfil atualizado com sucesso",
|
||||
"reset_link_sent": "Link de redefinição enviado com sucesso",
|
||||
"current_avatar": "Avatar atual",
|
||||
"close": "Fechar",
|
||||
"security_check": "Verificação de Segurança",
|
||||
"captcha_solved": "Correto!",
|
||||
"captcha_instruction": "Resolva a soma para continuar.",
|
||||
"invalid_captcha": "Captcha inválido",
|
||||
"email_verified_success": "E-mail confirmado com sucesso. Faça login.",
|
||||
"verification_email_sent": "E-mail de confirmação enviado. Verifique sua caixa de entrada.",
|
||||
"email_already_verified": "Este e-mail já foi verificado. Faça login.",
|
||||
"email_not_found": "E-mail não encontrado.",
|
||||
"resend_verification": "Reenviar confirmação",
|
||||
"too_many_attempts": "Muitas tentativas. Tente novamente mais tarde.",
|
||||
"captcha_reload": "Trocar desafio",
|
||||
"terms_and_conditions": "Termos e Condições",
|
||||
"privacy_policy_text": "Texto da política de privacidade...",
|
||||
"terms_of_service_text": "Texto dos termos de serviço...",
|
||||
"cookie_policy_text": "Texto da política de cookies...",
|
||||
"data_protection_text": "Texto da proteção de dados...",
|
||||
"phone": "Telefone",
|
||||
"bio": "Biografia",
|
||||
"current_password": "Senha atual",
|
||||
"new_password": "Nova senha",
|
||||
"avatar": "Avatar",
|
||||
"cancel": "Cancelar",
|
||||
"preview": "Visualização",
|
||||
"captcha_question": "What is :a + :b?",
|
||||
"captcha_placeholder": "Enter the result"
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"title": "Autenticação",
|
||||
"description": "Módulo de autenticação e autorização do sistema",
|
||||
"welcome": "Bem-vindo ao Sistema de Gerenciamento de Conteúdo",
|
||||
"login_required": "É necessário fazer login para acessar esta página",
|
||||
"access_denied": "Acesso negado",
|
||||
"invalid_credentials": "Credenciais inválidas",
|
||||
"session_expired": "Sua sessão expirou. Por favor, faça login novamente."
|
||||
}
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "auth",
|
||||
"display_name": "Authentication",
|
||||
"description": "Authentication and authorization module for user management",
|
||||
"version": "1.0.0",
|
||||
"author": "SysCMS Team",
|
||||
"dependencies": [],
|
||||
"permissions": [
|
||||
"auth.login",
|
||||
"auth.logout",
|
||||
"auth.register",
|
||||
"auth.profile",
|
||||
"auth.password.reset",
|
||||
"auth.password.change"
|
||||
],
|
||||
"settings": {
|
||||
"allow_registration": true,
|
||||
"require_email_verification": false,
|
||||
"password_min_length": 8,
|
||||
"session_timeout": 120
|
||||
},
|
||||
"assets": {
|
||||
"css": [],
|
||||
"js": []
|
||||
},
|
||||
"providers": [
|
||||
"Modules\\Auth\\Providers\\AuthServiceProvider"
|
||||
],
|
||||
"routes": {
|
||||
"web": "routes/web.php",
|
||||
"api": "routes/api.php"
|
||||
},
|
||||
"menu": {
|
||||
"dashboard": {
|
||||
"icon": "bi bi-person-check",
|
||||
"label": "auth.menu.dashboard",
|
||||
"route": "admin/dashboard",
|
||||
"order": 1
|
||||
},
|
||||
"public": {
|
||||
"label": "auth.menu.login",
|
||||
"route": "login",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
"views": "views",
|
||||
"locales": "locales",
|
||||
"module_ative": true,
|
||||
"module_public": true,
|
||||
"module_blocked": true,
|
||||
"ready": true,
|
||||
"tables": [
|
||||
"users"
|
||||
]
|
||||
}
|
||||
Executable
Executable
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SysCMS - Content Management System
|
||||
*
|
||||
* Copyright (c) 2025 Marco Costa
|
||||
* All rights reserved.
|
||||
*
|
||||
* Author: Marco Costa
|
||||
* Email: contato@syspanel.com.br
|
||||
* Project: SysCMS
|
||||
*
|
||||
* This software is licensed, not sold.
|
||||
* Permission is granted to use this software
|
||||
* subject to the terms of the SysCMS Commercial License.
|
||||
*
|
||||
* Redistribution, sublicensing, resale, or
|
||||
* disclosure of this software, in whole or in part,
|
||||
* is strictly prohibited without prior written permission.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
||||
*/
|
||||
|
||||
use Kernel\Application;
|
||||
|
||||
/**
|
||||
* Register auth module routes
|
||||
*
|
||||
* @param Application $app
|
||||
* @return void
|
||||
*/
|
||||
return function (Application $app) {
|
||||
// Authentication routes
|
||||
$app->addRoute('GET', '/login', [\Modules\Auth\Controllers\AuthController::class, 'showLoginForm']);
|
||||
$app->addRoute('POST', '/login', [\Modules\Auth\Controllers\AuthController::class, 'login']);
|
||||
$app->addRoute('GET', '/logout', [\Modules\Auth\Controllers\AuthController::class, 'logout']);
|
||||
|
||||
// Registration routes
|
||||
$app->addRoute('GET', '/register', [\Modules\Auth\Controllers\AuthController::class, 'showRegistrationForm']);
|
||||
$app->addRoute('POST', '/register', [\Modules\Auth\Controllers\AuthController::class, 'register']);
|
||||
|
||||
// Profile routes
|
||||
$app->addRoute('GET', '/profile', [\Modules\Auth\Controllers\AuthController::class, 'profile']);
|
||||
$app->addRoute('POST', '/profile', [\Modules\Auth\Controllers\AuthController::class, 'updateProfile']);
|
||||
|
||||
// Password reset routes
|
||||
$app->addRoute('GET', '/forgot-password', [\Modules\Auth\Controllers\AuthController::class, 'showForgotPasswordForm']);
|
||||
$app->addRoute('POST', '/forgot-password', [\Modules\Auth\Controllers\AuthController::class, 'sendResetLink']);
|
||||
$app->addRoute('GET', '/reset-password/{token}', [\Modules\Auth\Controllers\AuthController::class, 'showResetPasswordForm']);
|
||||
$app->addRoute('POST', '/reset-password', [\Modules\Auth\Controllers\AuthController::class, 'resetPassword']);
|
||||
|
||||
// Email verification
|
||||
$app->addRoute('GET', '/verify-email/resend', [\Modules\Auth\Controllers\AuthController::class, 'showResendVerificationForm']);
|
||||
$app->addRoute('POST', '/verify-email/resend', [\Modules\Auth\Controllers\AuthController::class, 'resendVerification']);
|
||||
$app->addRoute('GET', '/verify-email/{token}', [\Modules\Auth\Controllers\AuthController::class, 'verifyEmail']);
|
||||
};
|
||||
Executable
+106
@@ -0,0 +1,106 @@
|
||||
{% extends "layouts/base.twig" %}
|
||||
|
||||
{% block title %}{{ __('auth.forgot_password_title', {'platformName': platformName}) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-5">
|
||||
<div class="card shadow-lg border-0 rounded-lg mt-5">
|
||||
<div class="card-header">
|
||||
<h3 class="text-center font-weight-light my-4">
|
||||
{{ __('auth.forgot_password_title', {'platformName': platformName}) }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error == 1 ? __('auth.email_not_found') : '' }}
|
||||
{{ error == 'captcha' ? __('auth.invalid_captcha') : '' }}
|
||||
{{ error == 'rate' ? __('auth.too_many_attempts') : '' }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert-success" role="alert">
|
||||
{{ __('auth.reset_link_sent') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{{ url('forgot-password') }}">
|
||||
{{ csrf_field() }}
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input type="email" class="form-control" id="email" name="email" placeholder="{{ __('auth.email_placeholder') }}" required>
|
||||
<label for="email">{{ __('auth.email') }}</label>
|
||||
</div>
|
||||
|
||||
{% if require_captcha_forgot %}
|
||||
<div class="mb-3" x-data="{
|
||||
num1: 0,
|
||||
num2: 0,
|
||||
userAnswer: '',
|
||||
solved: false,
|
||||
generate() {
|
||||
this.num1 = Math.floor(Math.random() * 10) + 1;
|
||||
this.num2 = Math.floor(Math.random() * 10) + 1;
|
||||
this.userAnswer = '';
|
||||
this.solved = false;
|
||||
},
|
||||
check() {
|
||||
if (parseInt(this.userAnswer) === (this.num1 + this.num2)) {
|
||||
this.solved = true;
|
||||
} else {
|
||||
this.solved = false;
|
||||
}
|
||||
}
|
||||
}" x-init="generate()">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold mb-2">{{ __('auth.security_check') }}</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="px-3 py-2 bg-white border rounded fw-bold fs-5 text-nowrap">
|
||||
<span x-text="num1"></span> + <span x-text="num2"></span> =
|
||||
</div>
|
||||
<input type="number" class="form-control" style="width: 80px;" x-model="userAnswer" @input="check()" placeholder="?">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm ms-1" @click="generate()" title="{{ __('auth.captcha_reload') }}">
|
||||
<i class="bi bi-arrow-clockwise"></i> ↻
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2" style="min-height: 24px;">
|
||||
<span x-show="solved" class="text-success fw-bold small">
|
||||
✓ {{ __('auth.captcha_solved') }}
|
||||
</span>
|
||||
<span x-show="!solved" class="text-muted small">
|
||||
{{ __('auth.captcha_instruction') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="captcha_valid" :value="solved ? '1' : ''">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-primary" type="submit">
|
||||
{{ __('auth.send_reset_link') }}
|
||||
</button>
|
||||
<a href="{{ url('login') }}" class="btn btn-link">
|
||||
{{ __('auth.back_to_login') }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Auto-focus on email field
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const emailField = document.getElementById('email');
|
||||
if (emailField) {
|
||||
emailField.focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Executable
Executable
+259
@@ -0,0 +1,259 @@
|
||||
{% extends "layouts/base.twig" %}
|
||||
|
||||
{% block title %}{{ __('auth.login_title', {'platformName': platformName}) }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.login-container {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
.login-card {
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
}
|
||||
.login-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.login-header h3 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
.login-body {
|
||||
padding: 40px 30px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.form-floating {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-control {
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 12px 15px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
.btn-link {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.btn-link:hover {
|
||||
color: #764ba2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.form-check-input:checked {
|
||||
background-color: #667eea;
|
||||
border-color: #667eea;
|
||||
}
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.captcha-container {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.captcha-image {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
color: #6c757d;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="login-container">
|
||||
<div class="row justify-content-center w-100">
|
||||
<div class="col-lg-5">
|
||||
<div class="card login-card">
|
||||
<div class="login-header">
|
||||
<h3>{{ __('auth.login_title', {'platformName': platformName}) }}</h3>
|
||||
</div>
|
||||
<div class="login-body">
|
||||
{% if password_reset_success %}
|
||||
<div class="alert alert-success" role="alert">
|
||||
{{ __('auth.password_reset_success') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error == 1 ? __('auth.invalid_credentials') : '' }}
|
||||
{{ error == 2 ? __('auth.account_inactive') : '' }}
|
||||
{{ error == 3 ? __('auth.registration_pending') : '' }}
|
||||
{{ error == 'captcha' ? __('auth.invalid_captcha') : '' }}
|
||||
{{ error == 'rate' ? __('auth.too_many_attempts') : '' }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if expired %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
{{ __('auth.session_expired') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{{ url('login') }}">
|
||||
{{ csrf_field() }}
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input type="email" class="form-control" id="email" name="email" placeholder="{{ __('auth.email_placeholder') }}" value="{{ old('email') }}" required>
|
||||
<label for="email">{{ __('auth.email') }}</label>
|
||||
</div>
|
||||
|
||||
{% include 'components/password-field.twig' with {
|
||||
id: 'password',
|
||||
name: 'password',
|
||||
label: __('auth.password'),
|
||||
placeholder: __('auth.password_placeholder'),
|
||||
required: true
|
||||
} %}
|
||||
|
||||
{% if require_captcha_login %}
|
||||
<div class="captcha-container" x-data="{
|
||||
num1: 0,
|
||||
num2: 0,
|
||||
userAnswer: '',
|
||||
solved: false,
|
||||
generate() {
|
||||
this.num1 = Math.floor(Math.random() * 10) + 1;
|
||||
this.num2 = Math.floor(Math.random() * 10) + 1;
|
||||
this.userAnswer = '';
|
||||
this.solved = false;
|
||||
},
|
||||
check() {
|
||||
if (parseInt(this.userAnswer) === (this.num1 + this.num2)) {
|
||||
this.solved = true;
|
||||
} else {
|
||||
this.solved = false;
|
||||
}
|
||||
}
|
||||
}" x-init="generate()">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold mb-2">{{ __('auth.security_check') }}</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="px-3 py-2 bg-white border rounded fw-bold fs-5 text-nowrap">
|
||||
<span x-text="num1"></span> + <span x-text="num2"></span> =
|
||||
</div>
|
||||
<input type="number" class="form-control" style="width: 80px;" x-model="userAnswer" @input="check()" placeholder="?">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm ms-1" @click="generate()" title="{{ __('auth.captcha_reload') }}">
|
||||
<i class="bi bi-arrow-clockwise"></i> ↻
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2" style="min-height: 24px;">
|
||||
<span x-show="solved" class="text-success fw-bold small">
|
||||
✓ {{ __('auth.captcha_solved') }}
|
||||
</span>
|
||||
<span x-show="!solved" class="text-muted small">
|
||||
{{ __('auth.captcha_instruction') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="captcha_valid" :value="solved ? '1' : ''">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="remember" name="remember">
|
||||
<label class="form-check-label" for="remember">{{ __('auth.remember_me') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-3">
|
||||
<button class="btn btn-primary btn-lg" type="submit">
|
||||
{{ __('auth.login_button') }}
|
||||
</button>
|
||||
<a href="{{ url('forgot-password') }}" class="btn btn-link">
|
||||
{{ __('auth.forgot_password') }}
|
||||
</a>
|
||||
{% if allow_registration %}
|
||||
<a href="{{ url('register') }}" class="btn btn-link">
|
||||
{{ __('auth.register_button') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-3 d-grid">
|
||||
<a href="{{ url('verify-email/resend') }}" class="btn btn-outline-secondary btn-sm">
|
||||
{{ __('auth.resend_verification') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert-success" role="alert">
|
||||
{{ __('auth.logout_success') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if registered %}
|
||||
<div class="alert alert-success" role="alert">
|
||||
{{ __('auth.registration_success') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if verified %}
|
||||
<div class="alert alert-success" role="alert">
|
||||
{{ __('auth.email_verified_success') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if resend %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
{{ __('auth.verification_email_sent') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Auto-focus on email field
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const emailField = document.getElementById('email');
|
||||
if (emailField) {
|
||||
emailField.focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Executable
+135
@@ -0,0 +1,135 @@
|
||||
{% extends "layouts/dashboard.twig" %}
|
||||
|
||||
{% block title %}{{ __('auth.profile_title') }} - {% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">{{ __('auth.profile_title') }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if success %}
|
||||
<div class="alert alert-success" role="alert">
|
||||
{{ __('auth.profile_updated') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{{ url('profile') }}" enctype="multipart/form-data">
|
||||
{{ csrf_field() }}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="name" class="form-label">{{ __('auth.name') }}</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="{{ user.name }}" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="email" class="form-label">{{ __('auth.email') }}</label>
|
||||
<input type="email" class="form-control" id="email" name="email" value="{{ user.email }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="phone" class="form-label">{{ __('auth.phone') }}</label>
|
||||
<input type="text" class="form-control" id="phone" name="phone" value="{{ user.phone }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mb-3">
|
||||
<label for="bio" class="form-label">{{ __('auth.bio') }}</label>
|
||||
<textarea class="form-control" id="bio" name="bio" rows="4">{{ user.bio }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="avatar" class="form-label">{{ __('auth.avatar') }}</label>
|
||||
<input type="file" class="form-control" id="avatar" name="avatar" accept="image/*">
|
||||
<div id="avatarPreview" class="mt-2"></div>
|
||||
{% if user.avatar %}
|
||||
<div class="mt-2">
|
||||
<small class="form-text text-muted">{{ __('auth.current_avatar') }}:</small>
|
||||
<img src="{{ user.avatar_url }}" alt="{{ __('auth.current_avatar') }}" class="img-thumbnail" style="max-width: 100px;">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3" x-data="{ show: false }">
|
||||
<label for="current_password" class="form-label">{{ __('auth.current_password') }}</label>
|
||||
<div class="input-group">
|
||||
<input :type="show ? 'text' : 'password'" class="form-control" id="current_password" name="current_password" placeholder="{{ __('auth.current_password_placeholder') }}" autocomplete="new-password" value="">
|
||||
<button class="btn btn-outline-secondary" type="button" @click="show = !show">
|
||||
<i :class="show ? 'bi bi-eye-slash' : 'bi bi-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3" x-data="{ show: false }">
|
||||
<label for="password" class="form-label">{{ __('auth.new_password') }}</label>
|
||||
<div class="input-group">
|
||||
<input :type="show ? 'text' : 'password'" class="form-control" id="password" name="password" placeholder="{{ __('auth.new_password_placeholder') }}" autocomplete="new-password" value="">
|
||||
<button class="btn btn-outline-secondary" type="button" @click="show = !show">
|
||||
<i :class="show ? 'bi bi-eye-slash' : 'bi bi-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3" x-data="{ show: false }">
|
||||
<label for="password_confirmation" class="form-label">{{ __('auth.password_confirmation') }}</label>
|
||||
<div class="input-group">
|
||||
<input :type="show ? 'text' : 'password'" class="form-control" id="password_confirmation" name="password_confirmation" placeholder="{{ __('auth.password_confirmation_placeholder') }}" autocomplete="new-password" value="">
|
||||
<button class="btn btn-outline-secondary" type="button" @click="show = !show">
|
||||
<i :class="show ? 'bi bi-eye-slash' : 'bi bi-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary">{{ __('auth.update_profile') }}</button>
|
||||
<a href="{{ url('dashboard') }}" class="btn btn-secondary">{{ __('auth.cancel') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Preview avatar before upload
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const avatarInput = document.getElementById('avatar');
|
||||
const avatarPreview = document.getElementById('avatarPreview');
|
||||
|
||||
if (avatarInput && avatarPreview) {
|
||||
avatarInput.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
avatarPreview.innerHTML = `<img src="${e.target.result}" alt="{{ __('auth.preview') }}" class="img-thumbnail" style="max-width: 200px;">`;
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Executable
+131
@@ -0,0 +1,131 @@
|
||||
{% extends "layouts/base.twig" %}
|
||||
|
||||
{% block title %}{{ __('auth.register_title', {'platformName': platformName}) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-5">
|
||||
<div class="card shadow-lg border-0 rounded-lg mt-5">
|
||||
<div class="card-header">
|
||||
<h3 class="text-center font-weight-light my-4">
|
||||
{{ __('auth.register_title', {'platformName': platformName}) }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error == 'captcha' ? __('auth.invalid_captcha') : __('auth.registration_error') }}
|
||||
{{ error == 'rate' ? __('auth.too_many_attempts') : '' }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{{ url('register') }}">
|
||||
{{ csrf_field() }}
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" class="form-control" id="name" name="name" placeholder="{{ __('auth.name_placeholder') }}" value="{{ old('name') }}" required>
|
||||
<label for="name">{{ __('auth.name') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input type="email" class="form-control" id="email" name="email" placeholder="{{ __('auth.email_placeholder') }}" value="{{ old('email') }}" required>
|
||||
<label for="email">{{ __('auth.email') }}</label>
|
||||
</div>
|
||||
|
||||
{% include 'components/password-field.twig' with {
|
||||
id: 'password',
|
||||
name: 'password',
|
||||
label: __('auth.password'),
|
||||
placeholder: __('auth.password_placeholder'),
|
||||
required: true
|
||||
} %}
|
||||
|
||||
{% include 'components/password-field.twig' with {
|
||||
id: 'password_confirmation',
|
||||
name: 'password_confirmation',
|
||||
label: __('auth.password_confirmation'),
|
||||
placeholder: __('auth.password_confirmation_placeholder'),
|
||||
required: true
|
||||
} %}
|
||||
|
||||
{% if require_captcha_register %}
|
||||
<div class="mb-3" x-data="{
|
||||
num1: 0,
|
||||
num2: 0,
|
||||
userAnswer: '',
|
||||
solved: false,
|
||||
generate() {
|
||||
this.num1 = Math.floor(Math.random() * 10) + 1;
|
||||
this.num2 = Math.floor(Math.random() * 10) + 1;
|
||||
this.userAnswer = '';
|
||||
this.solved = false;
|
||||
},
|
||||
check() {
|
||||
if (parseInt(this.userAnswer) === (this.num1 + this.num2)) {
|
||||
this.solved = true;
|
||||
} else {
|
||||
this.solved = false;
|
||||
}
|
||||
}
|
||||
}" x-init="generate()">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold mb-2">{{ __('auth.security_check') }}</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="px-3 py-2 bg-white border rounded fw-bold fs-5 text-nowrap">
|
||||
<span x-text="num1"></span> + <span x-text="num2"></span> =
|
||||
</div>
|
||||
<input type="number" class="form-control" style="width: 80px;" x-model="userAnswer" @input="check()" placeholder="?">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm ms-1" @click="generate()" title="{{ __('auth.captcha_reload') }}">
|
||||
<i class="bi bi-arrow-clockwise"></i> ↻
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2" style="min-height: 24px;">
|
||||
<span x-show="solved" class="text-success fw-bold small">
|
||||
✓ {{ __('auth.captcha_solved') }}
|
||||
</span>
|
||||
<span x-show="!solved" class="text-muted small">
|
||||
{{ __('auth.captcha_instruction') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="captcha_valid" :value="solved ? '1' : ''">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="terms" name="terms" required>
|
||||
<label class="form-check-label" for="terms">
|
||||
{{ __('auth.accept_terms') }}
|
||||
<a href="/user_terms.html" target="_blank">
|
||||
{{ __('auth.read_terms') }}
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-primary btn-lg" type="submit">
|
||||
{{ __('auth.register_button') }}
|
||||
</button>
|
||||
<a href="{{ url('login') }}" class="btn btn-link">
|
||||
{{ __('auth.back_to_login') }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Auto-focus on name field
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const nameField = document.getElementById('name');
|
||||
if (nameField) {
|
||||
nameField.focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
{% extends "layouts/base.twig" %}
|
||||
|
||||
{% block title %}{{ __('auth.resend_verification') }} - {% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-5">
|
||||
<div class="card shadow-lg border-0 rounded-lg mt-5">
|
||||
<div class="card-header">
|
||||
<h3 class="text-center font-weight-light my-4">
|
||||
{{ __('auth.resend_verification') }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error == 1 ? __('auth.email_not_found') : '' }}
|
||||
{{ error == 'captcha' ? __('auth.invalid_captcha') : '' }}
|
||||
{{ error == 'rate' ? __('auth.too_many_attempts') : '' }}
|
||||
{{ error == 'verified' ? __('auth.email_already_verified') : '' }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert-success" role="alert">
|
||||
{{ __('auth.verification_email_sent') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{{ url('verify-email/resend') }}">
|
||||
{{ csrf_field() }}
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input type="email" class="form-control" id="email" name="email" placeholder="{{ __('auth.email_placeholder') }}" required value="{{ old('email') }}">
|
||||
<label for="email">{{ __('auth.email') }}</label>
|
||||
</div>
|
||||
|
||||
{% if require_captcha_resend %}
|
||||
<div class="mb-3" x-data="{
|
||||
num1: 0,
|
||||
num2: 0,
|
||||
userAnswer: '',
|
||||
solved: false,
|
||||
generate() {
|
||||
this.num1 = Math.floor(Math.random() * 10) + 1;
|
||||
this.num2 = Math.floor(Math.random() * 10) + 1;
|
||||
this.userAnswer = '';
|
||||
this.solved = false;
|
||||
},
|
||||
check() {
|
||||
if (parseInt(this.userAnswer) === (this.num1 + this.num2)) {
|
||||
this.solved = true;
|
||||
} else {
|
||||
this.solved = false;
|
||||
}
|
||||
}
|
||||
}" x-init="generate()">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold mb-2">{{ __('auth.security_check') }}</label>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="px-3 py-2 bg-white border rounded fw-bold fs-5 text-nowrap">
|
||||
<span x-text="num1"></span> + <span x-text="num2"></span> =
|
||||
</div>
|
||||
<input type="number" class="form-control" style="width: 80px;" x-model="userAnswer" @input="check()" placeholder="?">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm ms-1" @click="generate()" title="{{ __('auth.captcha_reload') }}">
|
||||
<i class="bi bi-arrow-clockwise"></i> ↻
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2" style="min-height: 24px;">
|
||||
<span x-show="solved" class="text-success fw-bold small">
|
||||
✓ {{ __('auth.captcha_solved') }}
|
||||
</span>
|
||||
<span x-show="!solved" class="text-muted small">
|
||||
{{ __('auth.captcha_instruction') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="captcha_valid" :value="solved ? '1' : ''">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-primary" type="submit">
|
||||
{{ __('auth.resend_verification') }}
|
||||
</button>
|
||||
<a href="{{ url('login') }}" class="btn btn-link">
|
||||
{{ __('auth.back_to_login') }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Auto-focus on email field
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const emailField = document.getElementById('email');
|
||||
if (emailField) {
|
||||
emailField.focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Executable
+163
@@ -0,0 +1,163 @@
|
||||
{% extends "layouts/base.twig" %}
|
||||
|
||||
{% block title %}{{ __('auth.reset_password_title', {'platformName': platformName}) }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.reset-container {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
.reset-card {
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
}
|
||||
.reset-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.reset-header h3 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
.reset-body {
|
||||
padding: 40px 30px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.form-floating {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-control {
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 12px 15px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
.btn-link {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.btn-link:hover {
|
||||
color: #764ba2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="reset-container">
|
||||
<div class="row justify-content-center w-100">
|
||||
<div class="col-lg-5">
|
||||
<div class="card reset-card">
|
||||
<div class="reset-header">
|
||||
<h3>{{ __('auth.reset_password_title', {'platformName': platformName}) }}</h3>
|
||||
</div>
|
||||
<div class="reset-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{% if error == 'password_confirmation' %}
|
||||
{{ __('auth.password_confirmation_error') }}
|
||||
{% elseif error == 'weak_password' %}
|
||||
{{ __('auth.weak_password_error') }}
|
||||
{% elseif error == '4' %}
|
||||
{{ __('auth.reset_error') }}
|
||||
{% elseif error == '1' %}
|
||||
{{ __('auth.reset_token_invalid') }}
|
||||
{% elseif error == '2' %}
|
||||
{{ __('auth.reset_token_expired') }}
|
||||
{% elseif error == '3' %}
|
||||
{{ __('auth.reset_email_invalid') }}
|
||||
{% else %}
|
||||
{{ __('auth.reset_error') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{{ url('reset-password') }}">
|
||||
{{ csrf_field() }}
|
||||
|
||||
<input type="hidden" name="token" value="{{ token }}">
|
||||
<input type="hidden" name="email" value="{{ email }}">
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input type="email" class="form-control" id="email_display" value="{{ email }}" disabled>
|
||||
<label for="email_display">{{ __('auth.email') }}</label>
|
||||
</div>
|
||||
|
||||
{% include 'components/password-field.twig' with {
|
||||
id: 'password',
|
||||
name: 'password',
|
||||
label: __('auth.new_password'),
|
||||
placeholder: __('auth.new_password_placeholder'),
|
||||
required: true
|
||||
} %}
|
||||
|
||||
{% include 'components/password-field.twig' with {
|
||||
id: 'password_confirmation',
|
||||
name: 'password_confirmation',
|
||||
label: __('auth.password_confirmation'),
|
||||
placeholder: __('auth.password_confirmation_placeholder'),
|
||||
required: true
|
||||
} %}
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-primary btn-lg" type="submit">
|
||||
{{ __('auth.reset_password_button') }}
|
||||
</button>
|
||||
<a href="{{ url('login') }}" class="btn btn-link text-decoration-none text-center">
|
||||
{{ __('auth.back_to_login') }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Auto-focus on password field
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const passwordField = document.getElementById('password');
|
||||
if (passwordField) {
|
||||
passwordField.focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
SysCMS Commercial License
|
||||
|
||||
Copyright (c) 2026 Marco Costa. All rights reserved.
|
||||
|
||||
This software is licensed, not sold. Permission is granted to use this software subject to the terms of the SysCMS Commercial License.
|
||||
|
||||
Redistribution, sublicensing, resale, or disclosure of this software, in whole or in part, is strictly prohibited without prior written permission from the author.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
# Company Module for SysCMS
|
||||
|
||||
Module for managing company information and settings.
|
||||
|
||||
## Features
|
||||
- Company profile management
|
||||
- Multi-company support
|
||||
- Company-specific configurations
|
||||
- Module enable/disable per company
|
||||
|
||||
## Installation
|
||||
1. Upload the `company.zip` file through the module manager or extract it to the `modules/` directory.
|
||||
2. Run the migrations to register the module and create the database tables.
|
||||
3. Configure company settings in the admin panel.
|
||||
|
||||
## Routes
|
||||
- Web routes: `routes/web.php`
|
||||
|
||||
## License
|
||||
Copyright (c) 2026 Marco Costa. All rights reserved.
|
||||
Licensed under the SysCMS Commercial License.
|
||||
+692
@@ -0,0 +1,692 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Controllers;
|
||||
|
||||
use App\Controllers\BaseAdminController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Modules\Company\Models\Company;
|
||||
use App\Models\Module;
|
||||
use App\Helpers\Log;
|
||||
use App\Helpers\Crypto;
|
||||
use Intervention\Image\ImageManagerStatic as Image;
|
||||
use Kernel\License;
|
||||
|
||||
class CompanyController extends BaseAdminController
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
|
||||
// Allow access to SuperAdmin, Admin, or Member with company.view permission
|
||||
$hasAccess = $user && (
|
||||
$user->isSuperAdmin() ||
|
||||
$user->isAdmin() ||
|
||||
($user->isMember() && $user->hasPermission('company.view'))
|
||||
);
|
||||
|
||||
if (!$hasAccess) {
|
||||
return new RedirectResponse(url('admin/dashboard?error=unauthorized'));
|
||||
}
|
||||
|
||||
if ($user->isSuperAdmin()) {
|
||||
$companies = Company::all();
|
||||
} else {
|
||||
$companies = Company::where('bus_id', $user->users_bus_id)->get();
|
||||
}
|
||||
|
||||
return $this->renderAdminView('company/views/admin/index.twig', [
|
||||
'companies' => $companies,
|
||||
'title' => 'Companies Management',
|
||||
'can_create_company' => $user->isSuperAdmin() && License::canCreateCompany()
|
||||
]);
|
||||
}
|
||||
|
||||
public function settings(Request $request): Response
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user) {
|
||||
return new RedirectResponse(url('login'));
|
||||
}
|
||||
|
||||
if ($user->isSuperAdmin()) {
|
||||
return new RedirectResponse(url('admin/companies'));
|
||||
}
|
||||
|
||||
return $this->edit($request, $user->users_bus_id);
|
||||
}
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user || !$user->isSuperAdmin()) {
|
||||
return new RedirectResponse(url('admin/dashboard?error=unauthorized'));
|
||||
}
|
||||
|
||||
if (!License::canCreateCompany()) {
|
||||
return new RedirectResponse(url('admin/companies?error=license_limit'));
|
||||
}
|
||||
|
||||
$error = $request->query->get('error');
|
||||
$errorMessage = $error ? $this->getErrorMessage($error) : null;
|
||||
|
||||
$modules = Module::active()->ordered()->get();
|
||||
|
||||
return $this->renderAdminView('company/views/admin/form.twig', [
|
||||
'title' => 'Create Company',
|
||||
'error' => $errorMessage,
|
||||
'target_company' => null,
|
||||
'modules' => $modules
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): Response
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user || !$user->isSuperAdmin()) {
|
||||
return new RedirectResponse(url('admin/dashboard?error=unauthorized'));
|
||||
}
|
||||
|
||||
$currentUserId = $_SESSION['user_id'] ?? null;
|
||||
$currentUserRole = $_SESSION['user_role'] ?? null;
|
||||
$currentUserBus = $_SESSION['user_idbus'] ?? null;
|
||||
|
||||
$data = $request->request->all();
|
||||
|
||||
// Basic validation
|
||||
if (empty($data['name']) || empty($data['email'])) {
|
||||
return new RedirectResponse(url('admin/companies/create?error=missing_fields'));
|
||||
}
|
||||
|
||||
// Email format validation
|
||||
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
return new RedirectResponse(url('admin/companies/create?error=invalid_email'));
|
||||
}
|
||||
|
||||
// Check if email exists
|
||||
if (Company::where('bus_email', $data['email'])->exists()) {
|
||||
return new RedirectResponse(url('admin/companies/create?error=email_exists'));
|
||||
}
|
||||
|
||||
// Generate slug from name
|
||||
$slug = strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $data['name'])));
|
||||
|
||||
// Check if slug exists
|
||||
if (Company::where('bus_slug', $slug)->exists()) {
|
||||
$slug .= '-' . time();
|
||||
}
|
||||
|
||||
$company = new Company();
|
||||
$company->bus_name = $data['name'];
|
||||
$company->bus_slug = $slug;
|
||||
$company->bus_description = $data['description'] ?? '';
|
||||
$company->bus_email = $data['email'];
|
||||
$company->bus_phone = $data['phone'] ?? '';
|
||||
$company->bus_address = $data['address'] ?? '';
|
||||
$company->bus_city = $data['city'] ?? '';
|
||||
$company->bus_state = $data['state'] ?? '';
|
||||
$company->bus_country = $data['country'] ?? '';
|
||||
$company->bus_postal_code = $data['postal_code'] ?? '';
|
||||
$company->bus_website = $data['website'] ?? '';
|
||||
$company->bus_tax_id = $data['tax_id'] ?? '';
|
||||
$company->bus_type = $data['bus_type'] ?? 'business';
|
||||
$company->bus_status = $data['status'] ?? 'active';
|
||||
$company->platform_name = $data['platform_name'] ?? '';
|
||||
|
||||
// Encrypt OpenRouter API Key if present
|
||||
$newApiKey = $data['openrouter_api_key'] ?? '';
|
||||
if (!empty($newApiKey) && strpos($newApiKey, '***') === false) {
|
||||
$company->openrouter_api_key = Crypto::encrypt($newApiKey);
|
||||
}
|
||||
|
||||
$company->ai_provider = $data['ai_provider'] ?? 'openrouter';
|
||||
$company->ai_model = $data['ai_model'] ?? 'google/gemini-2.0-flash-001';
|
||||
$company->ai_endpoint = $data['ai_endpoint'] ?? '';
|
||||
|
||||
// Settings are now handled via modules table
|
||||
// $company->bus_settings = $data['bus_settings'] ?? [];
|
||||
|
||||
$company->save();
|
||||
|
||||
// Handle modules - ONLY Super Admin can change which modules a company has access to
|
||||
if ($user->isSuperAdmin()) {
|
||||
$modulesData = $request->request->all()['modules'] ?? [];
|
||||
$syncData = [];
|
||||
|
||||
foreach ($modulesData as $moduleId => $settings) {
|
||||
$syncData[$moduleId] = [
|
||||
'bus_modules_is_enabled' => isset($settings['enabled']),
|
||||
'bus_modules_enabled_by' => $currentUserId,
|
||||
'bus_modules_enabled_at' => \Carbon\Carbon::now(),
|
||||
'bus_modules_show_public_nav' => isset($settings['public']),
|
||||
'bus_modules_show_dashboard_nav' => isset($settings['dashboard'])
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($syncData)) {
|
||||
$company->modules()->sync($syncData);
|
||||
} else {
|
||||
$company->modules()->detach();
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->files->has('logo')) {
|
||||
$file = $request->files->get('logo');
|
||||
if ($file && $file->isValid()) {
|
||||
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (in_array($file->getMimeType(), $allowedMimes)) {
|
||||
$uploadDir = dirname(__DIR__, 4) . '/storage/uploads/company/logos';
|
||||
if (!file_exists($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
$formatSetting = strtolower((string) get_env('IMG_FORMAT', 'webp'));
|
||||
$format = $formatSetting === 'auto' ? 'webp' : $formatSetting;
|
||||
$quality = env_int('IMG_QUALITY', 80);
|
||||
$resolutionPercent = env_int('IMG_RESOLUTION', 100);
|
||||
$image = Image::make($file->getPathname());
|
||||
$width = $image->width();
|
||||
$height = $image->height();
|
||||
if ($resolutionPercent > 0 && $resolutionPercent < 100) {
|
||||
$newW = (int) round($width * $resolutionPercent / 100);
|
||||
$newH = (int) round($height * $resolutionPercent / 100);
|
||||
$image->resize($newW, $newH, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
$constraint->upsize();
|
||||
});
|
||||
}
|
||||
$ext = $format === 'jpeg' || $format === 'jpg' ? 'jpg' : ($format === 'png' ? 'png' : ($format === 'gif' ? 'gif' : 'webp'));
|
||||
$filename = 'logo_' . $company->bus_id . '_' . time() . '.' . $ext;
|
||||
if ($format === 'jpeg' || $format === 'jpg') {
|
||||
$image->interlace(true);
|
||||
$image->encode('jpeg', max(1, min(100, $quality)));
|
||||
} elseif ($format === 'png') {
|
||||
$pngCompression = max(0, min(9, (int) round((100 - max(1, min(100, $quality))) / 10)));
|
||||
$image->encode('png', $pngCompression);
|
||||
} elseif ($format === 'gif') {
|
||||
$image->encode('gif');
|
||||
} else {
|
||||
$image->encode('webp', max(1, min(100, $quality)));
|
||||
}
|
||||
$image->save($uploadDir . '/' . $filename);
|
||||
// Delete old logo if exists (check both old and new directories)
|
||||
if ($company->bus_logo) {
|
||||
$oldDirs = [
|
||||
dirname(__DIR__, 4) . '/storage/uploads/logos',
|
||||
dirname(__DIR__, 4) . '/storage/uploads/company/logos'
|
||||
];
|
||||
foreach ($oldDirs as $dir) {
|
||||
$oldFile = $dir . '/' . $company->bus_logo;
|
||||
if (file_exists($oldFile)) {
|
||||
unlink($oldFile);
|
||||
break; // Delete only once
|
||||
}
|
||||
}
|
||||
}
|
||||
$company->bus_logo = $filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle business card upload
|
||||
if ($request->files->has('business_card')) {
|
||||
$file = $request->files->get('business_card');
|
||||
if ($file && $file->isValid()) {
|
||||
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (in_array($file->getMimeType(), $allowedMimes)) {
|
||||
$uploadDir = dirname(__DIR__, 4) . '/storage/uploads/company/business-cards';
|
||||
if (!file_exists($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
$formatSetting = strtolower((string) get_env('IMG_FORMAT', 'webp'));
|
||||
$format = $formatSetting === 'auto' ? 'webp' : $formatSetting;
|
||||
$quality = env_int('IMG_QUALITY', 80);
|
||||
$resolutionPercent = env_int('IMG_RESOLUTION', 100);
|
||||
$image = Image::make($file->getPathname());
|
||||
$width = $image->width();
|
||||
$height = $image->height();
|
||||
if ($resolutionPercent > 0 && $resolutionPercent < 100) {
|
||||
$newW = (int) round($width * $resolutionPercent / 100);
|
||||
$newH = (int) round($height * $resolutionPercent / 100);
|
||||
$image->resize($newW, $newH, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
$constraint->upsize();
|
||||
});
|
||||
}
|
||||
$ext = $format === 'jpeg' || $format === 'jpg' ? 'jpg' : ($format === 'png' ? 'png' : ($format === 'gif' ? 'gif' : 'webp'));
|
||||
$filename = 'business_card_' . $company->bus_id . '_' . time() . '.' . $ext;
|
||||
if ($format === 'jpeg' || $format === 'jpg') {
|
||||
$image->interlace(true);
|
||||
$image->encode('jpeg', max(1, min(100, $quality)));
|
||||
} elseif ($format === 'png') {
|
||||
$pngCompression = max(0, min(9, (int) round((100 - max(1, min(100, $quality))) / 10)));
|
||||
$image->encode('png', $pngCompression);
|
||||
} elseif ($format === 'gif') {
|
||||
$image->encode('gif');
|
||||
} else {
|
||||
$image->encode('webp', max(1, min(100, $quality)));
|
||||
}
|
||||
$image->save($uploadDir . '/' . $filename);
|
||||
// Delete old business card if exists (check both old and new directories)
|
||||
if ($company->bus_business_card) {
|
||||
$oldDirs = [
|
||||
dirname(__DIR__, 4) . '/storage/uploads/business-cards',
|
||||
dirname(__DIR__, 4) . '/storage/uploads/company/business-cards'
|
||||
];
|
||||
foreach ($oldDirs as $dir) {
|
||||
$oldFile = $dir . '/' . $company->bus_business_card;
|
||||
if (file_exists($oldFile)) {
|
||||
unlink($oldFile);
|
||||
break; // Delete only once
|
||||
}
|
||||
}
|
||||
}
|
||||
$company->bus_business_card = $filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle document upload
|
||||
if ($request->files->has('document')) {
|
||||
$file = $request->files->get('document');
|
||||
if ($file && $file->isValid()) {
|
||||
$allowedMimes = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
||||
];
|
||||
if (in_array($file->getMimeType(), $allowedMimes)) {
|
||||
$uploadDir = dirname(__DIR__, 4) . '/storage/uploads/company/documents';
|
||||
if (!file_exists($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
$originalName = $file->getClientOriginalName();
|
||||
$filename = 'document_' . $company->bus_id . '_' . time() . '_' . $originalName;
|
||||
$file->move($uploadDir, $filename);
|
||||
// Delete old document if exists (check both old and new directories)
|
||||
if ($company->bus_document) {
|
||||
$oldDirs = [
|
||||
dirname(__DIR__, 4) . '/storage/uploads/documents',
|
||||
dirname(__DIR__, 4) . '/storage/uploads/company/documents'
|
||||
];
|
||||
foreach ($oldDirs as $dir) {
|
||||
$oldFile = $dir . '/' . $company->bus_document;
|
||||
if (file_exists($oldFile)) {
|
||||
unlink($oldFile);
|
||||
break; // Delete only once
|
||||
}
|
||||
}
|
||||
}
|
||||
$company->bus_document = $filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$company->save();
|
||||
|
||||
// Restore session to ensure super_admin remains logged
|
||||
$_SESSION['user_id'] = $currentUserId;
|
||||
$_SESSION['user_role'] = $currentUserRole;
|
||||
$_SESSION['user_idbus'] = $currentUserBus;
|
||||
|
||||
return new RedirectResponse(url('admin/companies'));
|
||||
}
|
||||
|
||||
public function show(Request $request, string $id): Response
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user || (!$user->isSuperAdmin() && $user->users_bus_id != $id)) {
|
||||
return new RedirectResponse(url('admin/dashboard?error=unauthorized'));
|
||||
}
|
||||
|
||||
$company = Company::find($id);
|
||||
if (!$company) {
|
||||
return new RedirectResponse(url('admin/companies'));
|
||||
}
|
||||
|
||||
return $this->renderAdminView('company/views/admin/show.twig', [
|
||||
'company' => $company,
|
||||
'title' => 'Company Details'
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(Request $request, string $id): Response
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user || (!$user->isSuperAdmin() && $user->users_bus_id != $id)) {
|
||||
return new RedirectResponse(url('admin/dashboard?error=unauthorized'));
|
||||
}
|
||||
|
||||
$company = Company::find($id);
|
||||
if (!$company) {
|
||||
return new RedirectResponse(url('admin/companies'));
|
||||
}
|
||||
|
||||
// Mask the API Key for the view
|
||||
if ($company->openrouter_api_key) {
|
||||
$company->openrouter_api_key = substr(Crypto::decrypt($company->openrouter_api_key), 0, 8) . '****************';
|
||||
}
|
||||
|
||||
$error = $request->query->get('error');
|
||||
$errorMessage = $error ? $this->getErrorMessage($error) : null;
|
||||
|
||||
$modules = Module::active()->ordered()->get();
|
||||
|
||||
return $this->renderAdminView('company/views/admin/form.twig', [
|
||||
'target_company' => $company,
|
||||
'title' => 'Edit Company',
|
||||
'error' => $errorMessage,
|
||||
'modules' => $modules
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, string $id): Response
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user || (!$user->isSuperAdmin() && $user->users_bus_id != $id)) {
|
||||
return new RedirectResponse(url('admin/dashboard?error=unauthorized'));
|
||||
}
|
||||
|
||||
$currentUserId = $_SESSION['user_id'] ?? null;
|
||||
$currentUserRole = $_SESSION['user_role'] ?? null;
|
||||
$currentUserBus = $_SESSION['user_idbus'] ?? null;
|
||||
|
||||
$company = Company::find($id);
|
||||
if (!$company) {
|
||||
return new RedirectResponse(url('admin/companies'));
|
||||
}
|
||||
|
||||
$data = $request->request->all();
|
||||
|
||||
// Basic validation
|
||||
if (empty($data['name']) || empty($data['email'])) {
|
||||
return new RedirectResponse(url('admin/companies/' . $id . '/edit?error=missing_fields'));
|
||||
}
|
||||
|
||||
// Email format validation
|
||||
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
return new RedirectResponse(url('admin/companies/' . $id . '/edit?error=invalid_email'));
|
||||
}
|
||||
|
||||
// Check if email exists (exclude current company)
|
||||
if ($data['email'] !== $company->bus_email && Company::where('bus_email', $data['email'])->exists()) {
|
||||
return new RedirectResponse(url('admin/companies/' . $id . '/edit?error=email_exists'));
|
||||
}
|
||||
|
||||
$company->bus_name = $data['name'];
|
||||
$company->bus_description = $data['description'] ?? '';
|
||||
$company->bus_email = $data['email'];
|
||||
$company->bus_phone = $data['phone'] ?? '';
|
||||
$company->bus_address = $data['address'] ?? '';
|
||||
$company->bus_city = $data['city'] ?? '';
|
||||
$company->bus_state = $data['state'] ?? '';
|
||||
$company->bus_country = $data['country'] ?? '';
|
||||
$company->bus_postal_code = $data['postal_code'] ?? '';
|
||||
$company->bus_website = $data['website'] ?? '';
|
||||
$company->bus_tax_id = $data['tax_id'] ?? '';
|
||||
$company->bus_type = $data['bus_type'] ?? 'business';
|
||||
$company->bus_status = $data['status'] ?? 'active';
|
||||
$company->platform_name = $data['platform_name'] ?? '';
|
||||
|
||||
// Encrypt OpenRouter API Key if present
|
||||
$newApiKey = $data['openrouter_api_key'] ?? '';
|
||||
if (!empty($newApiKey) && strpos($newApiKey, '***') === false) {
|
||||
$company->openrouter_api_key = Crypto::encrypt($newApiKey);
|
||||
}
|
||||
|
||||
$company->ai_provider = $data['ai_provider'] ?? 'openrouter';
|
||||
$company->ai_model = $data['ai_model'] ?? 'google/gemini-2.0-flash-001';
|
||||
$company->ai_endpoint = $data['ai_endpoint'] ?? '';
|
||||
|
||||
// Settings are now handled via modules table
|
||||
// $company->bus_settings = $data['bus_settings'] ?? [];
|
||||
|
||||
// Update slug if name changed
|
||||
if ($data['name'] !== $company->getOriginal('name')) {
|
||||
$slug = strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $data['name'])));
|
||||
|
||||
// Check if slug exists (exclude current company)
|
||||
if ($slug !== $company->bus_slug && Company::where('bus_slug', $slug)->exists()) {
|
||||
$slug .= '-' . time();
|
||||
}
|
||||
$company->bus_slug = $slug;
|
||||
}
|
||||
|
||||
$company->save();
|
||||
|
||||
// Handle modules - ONLY Super Admin can change which modules a company has access to
|
||||
if ($user->isSuperAdmin()) {
|
||||
$modulesData = $request->request->all()['modules'] ?? [];
|
||||
$syncData = [];
|
||||
|
||||
foreach ($modulesData as $moduleId => $settings) {
|
||||
$syncData[$moduleId] = [
|
||||
'bus_modules_is_enabled' => isset($settings['enabled']),
|
||||
'bus_modules_enabled_by' => $currentUserId,
|
||||
'bus_modules_enabled_at' => \Carbon\Carbon::now(),
|
||||
'bus_modules_show_public_nav' => isset($settings['public']),
|
||||
'bus_modules_show_dashboard_nav' => isset($settings['dashboard'])
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($syncData)) {
|
||||
$company->modules()->sync($syncData);
|
||||
} else {
|
||||
$company->modules()->detach();
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->files->has('logo')) {
|
||||
$file = $request->files->get('logo');
|
||||
if ($file && $file->isValid()) {
|
||||
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (in_array($file->getMimeType(), $allowedMimes)) {
|
||||
$uploadDir = dirname(__DIR__, 4) . '/storage/uploads/company/logos';
|
||||
if (!file_exists($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
$formatSetting = strtolower((string) get_env('IMG_FORMAT', 'webp'));
|
||||
$format = $formatSetting === 'auto' ? 'webp' : $formatSetting;
|
||||
$quality = env_int('IMG_QUALITY', 80);
|
||||
$resolutionPercent = env_int('IMG_RESOLUTION', 100);
|
||||
$image = Image::make($file->getPathname());
|
||||
$width = $image->width();
|
||||
$height = $image->height();
|
||||
if ($resolutionPercent > 0 && $resolutionPercent < 100) {
|
||||
$newW = (int) round($width * $resolutionPercent / 100);
|
||||
$newH = (int) round($height * $resolutionPercent / 100);
|
||||
$image->resize($newW, $newH, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
$constraint->upsize();
|
||||
});
|
||||
}
|
||||
$ext = $format === 'jpeg' || $format === 'jpg' ? 'jpg' : ($format === 'png' ? 'png' : ($format === 'gif' ? 'gif' : 'webp'));
|
||||
$filename = 'logo_' . $company->bus_id . '_' . time() . '.' . $ext;
|
||||
if ($format === 'jpeg' || $format === 'jpg') {
|
||||
$image->interlace(true);
|
||||
$image->encode('jpeg', max(1, min(100, $quality)));
|
||||
} elseif ($format === 'png') {
|
||||
$pngCompression = max(0, min(9, (int) round((100 - max(1, min(100, $quality))) / 10)));
|
||||
$image->encode('png', $pngCompression);
|
||||
} elseif ($format === 'gif') {
|
||||
$image->encode('gif');
|
||||
} else {
|
||||
$image->encode('webp', max(1, min(100, $quality)));
|
||||
}
|
||||
$image->save($uploadDir . '/' . $filename);
|
||||
if ($company->bus_logo) {
|
||||
$oldDirs = [
|
||||
dirname(__DIR__, 4) . '/storage/uploads/logos',
|
||||
dirname(__DIR__, 4) . '/storage/uploads/company/logos'
|
||||
];
|
||||
foreach ($oldDirs as $dir) {
|
||||
$oldFile = $dir . '/' . $company->bus_logo;
|
||||
if (file_exists($oldFile)) {
|
||||
unlink($oldFile);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$company->bus_logo = $filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle business card upload
|
||||
if ($request->files->has('business_card')) {
|
||||
$file = $request->files->get('business_card');
|
||||
if ($file && $file->isValid()) {
|
||||
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (in_array($file->getMimeType(), $allowedMimes)) {
|
||||
$uploadDir = dirname(__DIR__, 4) . '/storage/uploads/company/business-cards';
|
||||
if (!file_exists($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
$formatSetting = strtolower((string) get_env('IMG_FORMAT', 'webp'));
|
||||
$format = $formatSetting === 'auto' ? 'webp' : $formatSetting;
|
||||
$quality = env_int('IMG_QUALITY', 80);
|
||||
$resolutionPercent = env_int('IMG_RESOLUTION', 100);
|
||||
$image = Image::make($file->getPathname());
|
||||
$width = $image->width();
|
||||
$height = $image->height();
|
||||
if ($resolutionPercent > 0 && $resolutionPercent < 100) {
|
||||
$newW = (int) round($width * $resolutionPercent / 100);
|
||||
$newH = (int) round($height * $resolutionPercent / 100);
|
||||
$image->resize($newW, $newH, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
$constraint->upsize();
|
||||
});
|
||||
}
|
||||
$ext = $format === 'jpeg' || $format === 'jpg' ? 'jpg' : ($format === 'png' ? 'png' : ($format === 'gif' ? 'gif' : 'webp'));
|
||||
$filename = 'business_card_' . $company->bus_id . '_' . time() . '.' . $ext;
|
||||
if ($format === 'jpeg' || $format === 'jpg') {
|
||||
$image->interlace(true);
|
||||
$image->encode('jpeg', max(1, min(100, $quality)));
|
||||
} elseif ($format === 'png') {
|
||||
$pngCompression = max(0, min(9, (int) round((100 - max(1, min(100, $quality))) / 10)));
|
||||
$image->encode('png', $pngCompression);
|
||||
} elseif ($format === 'gif') {
|
||||
$image->encode('gif');
|
||||
} else {
|
||||
$image->encode('webp', max(1, min(100, $quality)));
|
||||
}
|
||||
$image->save($uploadDir . '/' . $filename);
|
||||
if ($company->bus_business_card) {
|
||||
$oldDirs = [
|
||||
dirname(__DIR__, 4) . '/storage/uploads/business-cards',
|
||||
dirname(__DIR__, 4) . '/storage/uploads/company/business-cards'
|
||||
];
|
||||
foreach ($oldDirs as $dir) {
|
||||
$oldFile = $dir . '/' . $company->bus_business_card;
|
||||
if (file_exists($oldFile)) {
|
||||
unlink($oldFile);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$company->bus_business_card = $filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle document upload
|
||||
if ($request->files->has('document')) {
|
||||
$file = $request->files->get('document');
|
||||
if ($file && $file->isValid()) {
|
||||
$allowedMimes = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
||||
];
|
||||
if (in_array($file->getMimeType(), $allowedMimes)) {
|
||||
$uploadDir = dirname(__DIR__, 4) . '/storage/uploads/company/documents';
|
||||
if (!file_exists($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
$originalName = $file->getClientOriginalName();
|
||||
$filename = 'document_' . $company->bus_id . '_' . time() . '_' . $originalName;
|
||||
$file->move($uploadDir, $filename);
|
||||
if ($company->bus_document) {
|
||||
$oldDirs = [
|
||||
dirname(__DIR__, 4) . '/storage/uploads/documents',
|
||||
dirname(__DIR__, 4) . '/storage/uploads/company/documents'
|
||||
];
|
||||
foreach ($oldDirs as $dir) {
|
||||
$oldFile = $dir . '/' . $company->bus_document;
|
||||
if (file_exists($oldFile)) {
|
||||
unlink($oldFile);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$company->bus_document = $filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$company->save();
|
||||
|
||||
// Restore session to ensure super_admin remains logged
|
||||
$_SESSION['user_id'] = $currentUserId;
|
||||
$_SESSION['user_role'] = $currentUserRole;
|
||||
$_SESSION['user_idbus'] = $currentUserBus;
|
||||
|
||||
return new RedirectResponse(url('admin/companies'));
|
||||
}
|
||||
|
||||
private function getErrorMessage(string $error): string
|
||||
{
|
||||
$messages = [
|
||||
'missing_fields' => 'Por favor, preencha todos os campos obrigatórios.',
|
||||
'email_exists' => 'Este endereço de e-mail já está registrado.',
|
||||
'invalid_email' => 'Por favor, insira um endereço de e-mail válido.',
|
||||
];
|
||||
return $messages[$error] ?? 'Ocorreu um erro.';
|
||||
}
|
||||
|
||||
public function destroy(Request $request, string $id): Response
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user || !$user->isSuperAdmin()) {
|
||||
return new RedirectResponse(url('admin/dashboard?error=unauthorized'));
|
||||
}
|
||||
|
||||
$company = Company::find($id);
|
||||
if ($company) {
|
||||
// Delete logo file if exists
|
||||
if ($company->bus_logo) {
|
||||
$uploadDir = dirname(__DIR__, 4) . '/storage/uploads/company/logos';
|
||||
if (file_exists($uploadDir . '/' . $company->bus_logo)) {
|
||||
unlink($uploadDir . '/' . $company->bus_logo);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete business card file if exists
|
||||
if ($company->bus_business_card) {
|
||||
$uploadDir = dirname(__DIR__, 4) . '/storage/uploads/company/business-cards';
|
||||
if (file_exists($uploadDir . '/' . $company->bus_business_card)) {
|
||||
unlink($uploadDir . '/' . $company->bus_business_card);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete document file if exists
|
||||
if ($company->bus_document) {
|
||||
$uploadDir = dirname(__DIR__, 4) . '/storage/uploads/company/documents';
|
||||
if (file_exists($uploadDir . '/' . $company->bus_document)) {
|
||||
unlink($uploadDir . '/' . $company->bus_document);
|
||||
}
|
||||
}
|
||||
|
||||
$company->delete();
|
||||
}
|
||||
return new RedirectResponse(url('admin/companies'));
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
|
||||
class CompanyMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
|
||||
if (!$user) {
|
||||
return $this->unauthorizedResponse($request);
|
||||
}
|
||||
|
||||
// Super Admin can access any company
|
||||
if ($user->isSuperAdmin()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Routes that do not require a company context
|
||||
$bypassRoutes = ['/profile', '/logout', '/dashboard'];
|
||||
if (in_array($request->getPathInfo(), $bypassRoutes)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Get company ID from route or user session
|
||||
$companyId = $this->getCompanyId($request, $user);
|
||||
|
||||
// Check if user can access the company
|
||||
if (!$this->canAccessCompany($user, $companyId)) {
|
||||
return $this->forbiddenResponse($request);
|
||||
}
|
||||
|
||||
// Set company context in session for easy access
|
||||
$_SESSION['current_company_id'] = $companyId;
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current authenticated user.
|
||||
*
|
||||
* @return \Modules\Auth\Models\User|null
|
||||
*/
|
||||
protected function getCurrentUser(): ?\Modules\Auth\Models\User
|
||||
{
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if (class_exists('\Modules\Auth\Models\User')) {
|
||||
return \Modules\Auth\Models\User::find($_SESSION['user_id']);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log("Error loading user: " . $e->getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company ID from request or user session.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param \Modules\Auth\Models\User $user
|
||||
* @return int|null
|
||||
*/
|
||||
protected function getCompanyId(Request $request, $user): ?int
|
||||
{
|
||||
// 1. Check URL parameter (if present)
|
||||
// Note: FastRoute parameters are passed to controller, not easily accessible in middleware via standard request
|
||||
// We can parse the path if necessary, but standard app logic usually relies on session or explicit selection.
|
||||
|
||||
// 2. Check Session
|
||||
if (isset($_SESSION['current_company_id'])) {
|
||||
return (int) $_SESSION['current_company_id'];
|
||||
}
|
||||
|
||||
// 3. Default to user's primary business ID
|
||||
return $user->users_bus_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can access the company.
|
||||
*
|
||||
* @param \Modules\Auth\Models\User $user
|
||||
* @param int|null $companyId
|
||||
* @return bool
|
||||
*/
|
||||
protected function canAccessCompany($user, $companyId): bool
|
||||
{
|
||||
if (!$companyId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If user belongs to this company directly
|
||||
if ($user->users_bus_id == $companyId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If user has permission/association to this company (Multi-tenant logic)
|
||||
// TODO: Implement multi-company association logic if needed
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function unauthorizedResponse(Request $request)
|
||||
{
|
||||
if ($this->expectsJson($request)) {
|
||||
return new JsonResponse(['message' => 'Unauthorized'], 401);
|
||||
}
|
||||
return new Response('', 302, ['Location' => url('/login')]);
|
||||
}
|
||||
|
||||
protected function forbiddenResponse(Request $request)
|
||||
{
|
||||
if ($this->expectsJson($request)) {
|
||||
return new JsonResponse(['message' => 'Forbidden - Access denied to this company'], 403);
|
||||
}
|
||||
// Show 403 error page
|
||||
try {
|
||||
$view = new \Kernel\View();
|
||||
return new Response($view->render('errors/403.twig'), 403);
|
||||
} catch (\Throwable $e) {
|
||||
return new Response('Access denied to this company', 403);
|
||||
}
|
||||
}
|
||||
|
||||
protected function expectsJson(Request $request): bool
|
||||
{
|
||||
return $request->headers->get('Accept') === 'application/json' ||
|
||||
$request->headers->get('Content-Type') === 'application/json' ||
|
||||
str_contains($request->getPathInfo(), '/api/');
|
||||
}
|
||||
}
|
||||
Executable
+316
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class Company extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'bus';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'bus_id';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'bus_name',
|
||||
'bus_slug',
|
||||
'bus_description',
|
||||
'bus_logo',
|
||||
'bus_business_card',
|
||||
'bus_document',
|
||||
'bus_email',
|
||||
'bus_phone',
|
||||
'bus_address',
|
||||
'bus_city',
|
||||
'bus_state',
|
||||
'bus_country',
|
||||
'bus_postal_code',
|
||||
'bus_website',
|
||||
'bus_tax_id',
|
||||
'bus_type',
|
||||
'bus_status',
|
||||
'bus_settings',
|
||||
'openrouter_api_key',
|
||||
'ai_provider',
|
||||
'ai_model',
|
||||
'ai_endpoint',
|
||||
'platform_name'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'bus_settings' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = [
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get users for the company.
|
||||
*/
|
||||
public function users(): HasMany
|
||||
{
|
||||
return $this->hasMany(\Modules\Auth\Models\User::class, 'users_bus_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get modules for the company.
|
||||
*/
|
||||
public function modules(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(\App\Models\Module::class, 'bus_modules', 'bus_modules_bus_id', 'bus_modules_module_id')
|
||||
->withPivot(['bus_modules_is_enabled', 'bus_modules_settings', 'bus_modules_enabled_by', 'bus_modules_enabled_at', 'bus_modules_show_public_nav', 'bus_modules_show_dashboard_nav'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get activity logs for the company.
|
||||
*/
|
||||
public function activityLogs(): HasMany
|
||||
{
|
||||
return $this->hasMany(\App\Models\ActivityLog::class, 'activity_logs_bus_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled modules only.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeEnabledModules($query)
|
||||
{
|
||||
return $query->where('bus_modules.bus_modules_is_enabled', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active companies only.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('bus_status', 'active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get companies by type.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $type
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeType($query, string $type)
|
||||
{
|
||||
return $query->where('bus_type', $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if company has specific module enabled.
|
||||
*
|
||||
* @param string $moduleName
|
||||
* @return bool
|
||||
*/
|
||||
public function hasModuleEnabled(string $moduleName): bool
|
||||
{
|
||||
return $this->modules()
|
||||
->where('modules.modules_name', $moduleName)
|
||||
->where('bus_modules.bus_modules_is_enabled', true)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable module for company.
|
||||
*
|
||||
* @param int $moduleId
|
||||
* @param int $enabledBy
|
||||
* @param array $settings
|
||||
* @return bool
|
||||
*/
|
||||
public function enableModule(int $moduleId, int $enabledBy, array $settings = []): bool
|
||||
{
|
||||
$this->modules()->attach($moduleId, [
|
||||
'bus_modules_is_enabled' => true,
|
||||
'bus_modules_settings' => json_encode($settings),
|
||||
'bus_modules_enabled_by' => $enabledBy,
|
||||
'bus_modules_enabled_at' => \Carbon\Carbon::now()
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable module for company.
|
||||
*
|
||||
* @param int $moduleId
|
||||
* @return bool
|
||||
*/
|
||||
public function disableModule(int $moduleId): bool
|
||||
{
|
||||
return $this->modules()->updateExistingPivot($moduleId, [
|
||||
'bus_modules_is_enabled' => false
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLogoUrlAttribute(): string
|
||||
{
|
||||
if ($this->bus_logo) {
|
||||
return url('storage/uploads/company/logos/' . $this->bus_logo);
|
||||
}
|
||||
|
||||
return asset_url('img/default-logo.png');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get business card URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBusinessCardUrlAttribute(): string
|
||||
{
|
||||
if ($this->bus_business_card) {
|
||||
return url('storage/uploads/company/business-cards/' . $this->bus_business_card);
|
||||
}
|
||||
|
||||
return asset_url('img/default-business-card.png');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get document URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDocumentUrlAttribute(): string
|
||||
{
|
||||
if ($this->bus_document) {
|
||||
return url('storage/uploads/company/documents/' . $this->bus_document);
|
||||
}
|
||||
|
||||
return '#';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDisplayNameAttribute(): string
|
||||
{
|
||||
return $this->bus_name ?: $this->bus_slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full address.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFullAddressAttribute(): string
|
||||
{
|
||||
$address = [];
|
||||
|
||||
if ($this->bus_address) {
|
||||
$address[] = $this->bus_address;
|
||||
}
|
||||
|
||||
if ($this->bus_city) {
|
||||
$address[] = $this->bus_city;
|
||||
}
|
||||
|
||||
if ($this->bus_state) {
|
||||
$address[] = $this->bus_state;
|
||||
}
|
||||
|
||||
if ($this->bus_country) {
|
||||
$address[] = $this->bus_country;
|
||||
}
|
||||
|
||||
if ($this->bus_postal_code) {
|
||||
$address[] = $this->bus_postal_code;
|
||||
}
|
||||
|
||||
return implode(', ', $address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get setting value.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function getSetting(string $key, $default = null)
|
||||
{
|
||||
$settings = $this->bus_settings ?? [];
|
||||
return $settings[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set setting value.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function setSetting(string $key, $value): void
|
||||
{
|
||||
$settings = $this->bus_settings ?? [];
|
||||
$settings[$key] = $value;
|
||||
$this->bus_settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user count.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUserCountAttribute(): int
|
||||
{
|
||||
return $this->users()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active user count.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getActiveUserCountAttribute(): int
|
||||
{
|
||||
return $this->users()->active()->count();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Providers;
|
||||
|
||||
use Kernel\Application;
|
||||
use Kernel\View;
|
||||
use Modules\Company\Models\Company;
|
||||
use Modules\Company\Middleware\CompanyMiddleware;
|
||||
|
||||
class CompanyServiceProvider
|
||||
{
|
||||
public function register()
|
||||
{
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
$app = Application::getInstance();
|
||||
$app->addProtectedMiddleware(new CompanyMiddleware());
|
||||
|
||||
try {
|
||||
if (class_exists(Company::class)) {
|
||||
$mainCompany = Company::find(1);
|
||||
if ($mainCompany) {
|
||||
$view = new View();
|
||||
$view->getEngine()->addGlobal('main_company', $mainCompany);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Silently fail
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateCompanyModuleTables
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
// Companies Table
|
||||
if (!Capsule::schema()->hasTable('bus')) {
|
||||
Capsule::schema()->create('bus', function (Blueprint $table) {
|
||||
$table->increments('bus_id');
|
||||
$table->string('bus_name');
|
||||
$table->string('bus_slug')->unique();
|
||||
$table->text('bus_description')->nullable();
|
||||
$table->string('bus_logo')->nullable();
|
||||
$table->string('bus_business_card')->nullable();
|
||||
$table->string('bus_document')->nullable();
|
||||
$table->string('bus_email')->nullable();
|
||||
$table->string('bus_phone')->nullable();
|
||||
$table->string('bus_address')->nullable();
|
||||
$table->string('bus_city')->nullable();
|
||||
$table->string('bus_state')->nullable();
|
||||
$table->string('bus_country')->nullable();
|
||||
$table->string('bus_postal_code')->nullable();
|
||||
$table->string('bus_website')->nullable();
|
||||
$table->string('bus_tax_id')->nullable();
|
||||
$table->string('bus_type')->nullable(); // e.g., 'personal', 'business'
|
||||
$table->enum('bus_status', ['active', 'inactive', 'suspended'])->default('active');
|
||||
$table->json('bus_settings')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
// Company Modules Pivot Table
|
||||
if (!Capsule::schema()->hasTable('bus_modules')) {
|
||||
Capsule::schema()->create('bus_modules', function (Blueprint $table) {
|
||||
$table->increments('bus_modules_id');
|
||||
$table->integer('bus_modules_bus_id')->unsigned();
|
||||
$table->integer('bus_modules_module_id')->unsigned();
|
||||
$table->boolean('bus_modules_is_enabled')->default(true);
|
||||
$table->json('bus_modules_settings')->nullable();
|
||||
$table->integer('bus_modules_enabled_by')->unsigned()->nullable();
|
||||
$table->timestamp('bus_modules_enabled_at')->useCurrent();
|
||||
|
||||
// Navigation visibility
|
||||
$table->boolean('bus_modules_show_public_nav')->default(true);
|
||||
$table->boolean('bus_modules_show_dashboard_nav')->default(true);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['bus_modules_bus_id', 'bus_modules_module_id']);
|
||||
$table->foreign('bus_modules_bus_id')->references('bus_id')->on('bus')->onDelete('cascade');
|
||||
// Note: modules table is created in SystemConfig or Core, so we might need to be careful with foreign keys if order varies.
|
||||
// Assuming modules table exists or will exist. If strict, we can add FK.
|
||||
// $table->foreign('bus_modules_module_id')->references('modules_id')->on('modules')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Capsule::schema()->dropIfExists('bus_modules');
|
||||
Capsule::schema()->dropIfExists('bus');
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
/**
|
||||
* Migration: Add Date Format to Companies Table
|
||||
*/
|
||||
class AddDateFormatToCompaniesTable
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (Capsule::schema()->hasTable('bus')) {
|
||||
Capsule::schema()->table('bus', function (Blueprint $table) {
|
||||
if (!Capsule::schema()->hasColumn('bus', 'bus_date_format')) {
|
||||
$table->string('bus_date_format', 20)->default('d/m/Y')->after('bus_status');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Capsule::schema()->hasTable('bus')) {
|
||||
Capsule::schema()->table('bus', function (Blueprint $table) {
|
||||
if (Capsule::schema()->hasColumn('bus', 'bus_date_format')) {
|
||||
$table->dropColumn('bus_date_format');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class AddNavVisibilityToBusModules
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (Capsule::schema()->hasTable('bus_modules')) {
|
||||
Capsule::schema()->table('bus_modules', function (Blueprint $table) {
|
||||
if (!Capsule::schema()->hasColumn('bus_modules', 'bus_modules_show_public_nav')) {
|
||||
$table->boolean('bus_modules_show_public_nav')->default(true)->after('bus_modules_is_enabled');
|
||||
}
|
||||
if (!Capsule::schema()->hasColumn('bus_modules', 'bus_modules_show_dashboard_nav')) {
|
||||
$table->boolean('bus_modules_show_dashboard_nav')->default(true)->after('bus_modules_show_public_nav');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Capsule::schema()->hasTable('bus_modules')) {
|
||||
Capsule::schema()->table('bus_modules', function (Blueprint $table) {
|
||||
$table->dropColumn(['bus_modules_show_public_nav', 'bus_modules_show_dashboard_nav']);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use App\Models\Module;
|
||||
use Modules\Company\Models\Company;
|
||||
|
||||
class InsertCompanyModule
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$moduleData = [
|
||||
'modules_name' => 'company',
|
||||
'modules_display_name' => 'Company',
|
||||
'modules_description' => 'Company management module',
|
||||
'modules_version' => '1.0.0',
|
||||
'modules_author' => 'SysCMS Team',
|
||||
'modules_ative' => true,
|
||||
'modules_public' => false,
|
||||
'modules_blocked' => false,
|
||||
'modules_dependencies' => ["auth"],
|
||||
'modules_permissions' => [
|
||||
"company.view",
|
||||
"company.create",
|
||||
"company.edit",
|
||||
"company.delete"
|
||||
],
|
||||
'modules_settings' => [
|
||||
'menu' => [
|
||||
'dashboard' => [
|
||||
'icon' => 'bi bi-building',
|
||||
'label' => 'company.menu.dashboard',
|
||||
'route' => 'admin/companies',
|
||||
'order' => 30
|
||||
]
|
||||
]
|
||||
],
|
||||
'modules_path' => 'modules/company',
|
||||
'modules_namespace' => 'Modules\\Company',
|
||||
'modules_icon' => 'bi bi-building',
|
||||
'menu_side_bar' => true,
|
||||
'menu_nav_bar' => false,
|
||||
'modules_order' => 30,
|
||||
'modules_install_date' => date('Y-m-d H:i:s'),
|
||||
'modules_update_date' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$existingModule = Module::where('modules_name', 'company')->first();
|
||||
|
||||
if (!$existingModule) {
|
||||
$module = Module::create($moduleData);
|
||||
|
||||
if (Capsule::schema()->hasTable('bus')) {
|
||||
$companies = Company::all();
|
||||
foreach ($companies as $company) {
|
||||
$exists = Capsule::table('bus_modules')
|
||||
->where('bus_modules_bus_id', $company->bus_id)
|
||||
->where('bus_modules_module_id', $module->modules_id)
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
Capsule::table('bus_modules')->insert([
|
||||
'bus_modules_bus_id' => $company->bus_id,
|
||||
'bus_modules_module_id' => $module->modules_id,
|
||||
'bus_modules_is_enabled' => true,
|
||||
'bus_modules_settings' => json_encode([]),
|
||||
'bus_modules_enabled_by' => 1,
|
||||
'bus_modules_enabled_at' => date('Y-m-d H:i:s'),
|
||||
'bus_modules_show_public_nav' => true,
|
||||
'bus_modules_show_dashboard_nav' => true,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$module = Module::where('modules_name', 'company')->first();
|
||||
if ($module) {
|
||||
Capsule::table('bus_modules')->where('bus_modules_module_id', $module->modules_id)->delete();
|
||||
$module->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class AddPlatformNameToCompanies
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
// Adiciona campo platform_name à tabela bus (companies)
|
||||
if (Capsule::schema()->hasTable('bus') && !Capsule::schema()->hasColumn('bus', 'platform_name')) {
|
||||
Capsule::schema()->table('bus', function (Blueprint $table) {
|
||||
$table->string('platform_name')->nullable()->after('bus_name');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if (Capsule::schema()->hasTable('bus') && Capsule::schema()->hasColumn('bus', 'platform_name')) {
|
||||
Capsule::schema()->table('bus', function (Blueprint $table) {
|
||||
$table->dropColumn('platform_name');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Firmen"
|
||||
},
|
||||
"title": "Unternehmen",
|
||||
"new_company": "Neues Unternehmen",
|
||||
"list_title": "Unternehmensliste",
|
||||
"name": "Name",
|
||||
"email": "E-Mail",
|
||||
"type": "Typ",
|
||||
"status": "Status",
|
||||
"created_at": "Erstellt am",
|
||||
"actions": "Aktionen",
|
||||
"no_companies": "Keine Unternehmen gefunden.",
|
||||
"confirm_delete": "Sind Sie sicher, dass Sie dieses Unternehmen löschen möchten?",
|
||||
"view": "Ansehen",
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"form": {
|
||||
"description_label": "Beschreibung",
|
||||
"description_placeholder": "Geben Sie hier die Unternehmensbeschreibung ein..."
|
||||
},
|
||||
"edit_title": "Bearbeiten",
|
||||
"create_title": "Erstellen",
|
||||
"details_title": "Unternehmensdetails",
|
||||
"company_name": "Unternehmensname",
|
||||
"description": "Beschreibung",
|
||||
"phone": "Telefon",
|
||||
"website": "Webseite",
|
||||
"tax_id": "Steuer-ID",
|
||||
"business_type": "Geschäftstyp",
|
||||
"individual": "Einzelperson",
|
||||
"business": "Geschäft",
|
||||
"nonprofit": "Gemeinnützig",
|
||||
"active": "Aktiv",
|
||||
"inactive": "Inaktiv",
|
||||
"address": "Adresse",
|
||||
"city": "Stadt",
|
||||
"state": "Bundesland",
|
||||
"country": "Land",
|
||||
"postal_code": "Postleitzahl",
|
||||
"module_visibility_title": "Modul-Sichtbarkeitseinstellungen",
|
||||
"module_header": "Modul",
|
||||
"enabled_header": "Aktiviert",
|
||||
"public_navbar_header": "Öffentliche Navigationsleiste",
|
||||
"dashboard_navbar_header": "Dashboard-Navigationsleiste",
|
||||
"no_active_modules": "Keine aktiven Module gefunden.",
|
||||
"logo": "Logo",
|
||||
"preview": "Vorschau:",
|
||||
"current_logo": "Aktuelles Logo:",
|
||||
"business_card": "Visitenkarte",
|
||||
"current_business_card": "Aktuelle Visitenkarte:",
|
||||
"document": "Dokument",
|
||||
"current_document": "Aktuelles Dokument:",
|
||||
"view_document": "Dokument anzeigen",
|
||||
"cancel": "Abbrechen",
|
||||
"update_button": "Unternehmen aktualisieren",
|
||||
"create_button": "Unternehmen erstellen",
|
||||
"selected_file": "Ausgewählte Datei:",
|
||||
"slug": "Slug",
|
||||
"updated_at": "Aktualisiert am",
|
||||
"back_to_list": "Zurück zur Liste",
|
||||
"edit_company": "Unternehmen bearbeiten",
|
||||
"na": "N/A",
|
||||
"alt_logo": "Unternehmenslogo",
|
||||
"alt_business_card": "Visitenkarte",
|
||||
"alt_preview": "Vorschau"
|
||||
}
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Companies"
|
||||
},
|
||||
"title": "Companies",
|
||||
"new_company": "New Company",
|
||||
"list_title": "Companies List",
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"type": "Type",
|
||||
"status": "Status",
|
||||
"created_at": "Created At",
|
||||
"actions": "Actions",
|
||||
"no_companies": "No companies found.",
|
||||
"confirm_delete": "Are you sure you want to delete this company?",
|
||||
"view": "View",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"form": {
|
||||
"description_label": "Description",
|
||||
"description_placeholder": "Type the company description here..."
|
||||
},
|
||||
"edit_title": "Edit",
|
||||
"create_title": "Create",
|
||||
"details_title": "Company Details",
|
||||
"company_name": "Company Name",
|
||||
"description": "Description",
|
||||
"phone": "Phone",
|
||||
"website": "Website",
|
||||
"tax_id": "Tax ID",
|
||||
"business_type": "Business Type",
|
||||
"individual": "Individual",
|
||||
"business": "Business",
|
||||
"nonprofit": "Non-profit",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"address": "Address",
|
||||
"city": "City",
|
||||
"state": "State",
|
||||
"country": "Country",
|
||||
"postal_code": "Postal Code",
|
||||
"module_visibility_title": "Module Visibility Settings",
|
||||
"module_header": "Module",
|
||||
"enabled_header": "Enabled",
|
||||
"public_navbar_header": "Public Navbar",
|
||||
"dashboard_navbar_header": "Dashboard Navbar",
|
||||
"no_active_modules": "No active modules found.",
|
||||
"logo": "Logo",
|
||||
"preview": "Preview:",
|
||||
"current_logo": "Current logo:",
|
||||
"business_card": "Business Card",
|
||||
"current_business_card": "Current business card:",
|
||||
"document": "Document",
|
||||
"current_document": "Current document:",
|
||||
"view_document": "View Document",
|
||||
"cancel": "Cancel",
|
||||
"update_button": "Update Company",
|
||||
"create_button": "Create Company",
|
||||
"selected_file": "Selected file:",
|
||||
"slug": "Slug",
|
||||
"updated_at": "Updated At",
|
||||
"back_to_list": "Back to List",
|
||||
"edit_company": "Edit Company",
|
||||
"na": "N/A",
|
||||
"alt_logo": "Company Logo",
|
||||
"alt_business_card": "Business Card",
|
||||
"alt_preview": "Preview"
|
||||
}
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Empresas"
|
||||
},
|
||||
"title": "Empresas",
|
||||
"new_company": "Nueva Empresa",
|
||||
"list_title": "Lista de Empresas",
|
||||
"name": "Nombre",
|
||||
"email": "Correo Electrónico",
|
||||
"type": "Tipo",
|
||||
"status": "Estado",
|
||||
"created_at": "Creado el",
|
||||
"actions": "Acciones",
|
||||
"no_companies": "No se encontraron empresas.",
|
||||
"confirm_delete": "¿Está seguro de que desea eliminar esta empresa?",
|
||||
"view": "Ver",
|
||||
"edit": "Editar",
|
||||
"delete": "Eliminar",
|
||||
"form": {
|
||||
"description_label": "Descripción",
|
||||
"description_placeholder": "Escriba la descripción de la empresa aquí..."
|
||||
},
|
||||
"edit_title": "Editar",
|
||||
"create_title": "Crear",
|
||||
"details_title": "Detalles de la Empresa",
|
||||
"company_name": "Nombre de la Empresa",
|
||||
"description": "Descripción",
|
||||
"phone": "Teléfono",
|
||||
"website": "Sitio Web",
|
||||
"tax_id": "ID Fiscal",
|
||||
"business_type": "Tipo de Negocio",
|
||||
"individual": "Individuo",
|
||||
"business": "Empresa",
|
||||
"nonprofit": "Sin Fines de Lucro",
|
||||
"active": "Activo",
|
||||
"inactive": "Inactivo",
|
||||
"address": "Dirección",
|
||||
"city": "Ciudad",
|
||||
"state": "Estado/Provincia",
|
||||
"country": "País",
|
||||
"postal_code": "Código Postal",
|
||||
"module_visibility_title": "Configuración de Visibilidad del Módulo",
|
||||
"module_header": "Módulo",
|
||||
"enabled_header": "Habilitado",
|
||||
"public_navbar_header": "Barra de Navegación Pública",
|
||||
"dashboard_navbar_header": "Barra de Navegación del Panel",
|
||||
"no_active_modules": "No se encontraron módulos activos.",
|
||||
"logo": "Logotipo",
|
||||
"preview": "Vista Previa:",
|
||||
"current_logo": "Logotipo Actual:",
|
||||
"business_card": "Tarjeta de Visita",
|
||||
"current_business_card": "Tarjeta actual:",
|
||||
"document": "Documento",
|
||||
"current_document": "Documento actual:",
|
||||
"view_document": "Ver Documento",
|
||||
"cancel": "Cancelar",
|
||||
"update_button": "Actualizar Empresa",
|
||||
"create_button": "Crear Empresa",
|
||||
"selected_file": "Archivo seleccionado:",
|
||||
"slug": "Slug",
|
||||
"updated_at": "Actualizado el",
|
||||
"back_to_list": "Volver a la Lista",
|
||||
"edit_company": "Editar Empresa",
|
||||
"na": "N/A",
|
||||
"alt_logo": "Logotipo de la Empresa",
|
||||
"alt_business_card": "Tarjeta de Visita",
|
||||
"alt_preview": "Vista Previa"
|
||||
}
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Entreprises"
|
||||
},
|
||||
"title": "Entreprises",
|
||||
"new_company": "Nouvelle Entreprise",
|
||||
"list_title": "Liste des Entreprises",
|
||||
"name": "Nom",
|
||||
"email": "Email",
|
||||
"type": "Type",
|
||||
"status": "Statut",
|
||||
"created_at": "Créé le",
|
||||
"actions": "Actions",
|
||||
"no_companies": "Aucune entreprise trouvée.",
|
||||
"confirm_delete": "Êtes-vous sûr de vouloir supprimer cette entreprise ?",
|
||||
"view": "Voir",
|
||||
"edit": "Modifier",
|
||||
"delete": "Supprimer",
|
||||
"form": {
|
||||
"description_label": "Description",
|
||||
"description_placeholder": "Entrez la description de l'entreprise ici..."
|
||||
},
|
||||
"edit_title": "Modifier",
|
||||
"create_title": "Créer",
|
||||
"details_title": "Détails de l'Entreprise",
|
||||
"company_name": "Nom de l'Entreprise",
|
||||
"description": "Description",
|
||||
"phone": "Téléphone",
|
||||
"website": "Site Web",
|
||||
"tax_id": "Numéro Fiscal",
|
||||
"business_type": "Type d'Entreprise",
|
||||
"individual": "Individu",
|
||||
"business": "Entreprise",
|
||||
"nonprofit": "À But Non Lucratif",
|
||||
"active": "Actif",
|
||||
"inactive": "Inactif",
|
||||
"address": "Adresse",
|
||||
"city": "Ville",
|
||||
"state": "État/Région",
|
||||
"country": "Pays",
|
||||
"postal_code": "Code Postal",
|
||||
"module_visibility_title": "Paramètres de Visibilité du Module",
|
||||
"module_header": "Module",
|
||||
"enabled_header": "Activé",
|
||||
"public_navbar_header": "Barre de Navigation Publique",
|
||||
"dashboard_navbar_header": "Barre de Navigation du Tableau de Bord",
|
||||
"no_active_modules": "Aucun module actif trouvé.",
|
||||
"logo": "Logo",
|
||||
"preview": "Aperçu :",
|
||||
"current_logo": "Logo actuel :",
|
||||
"business_card": "Carte de Visite",
|
||||
"current_business_card": "Carte actuelle :",
|
||||
"document": "Document",
|
||||
"current_document": "Document actuel :",
|
||||
"view_document": "Voir le Document",
|
||||
"cancel": "Annuler",
|
||||
"update_button": "Mettre à Jour l'Entreprise",
|
||||
"create_button": "Créer l'Entreprise",
|
||||
"selected_file": "Fichier sélectionné :",
|
||||
"slug": "Slug",
|
||||
"updated_at": "Mis à jour le",
|
||||
"back_to_list": "Retour à la Liste",
|
||||
"edit_company": "Modifier l'Entreprise",
|
||||
"na": "N/A",
|
||||
"alt_logo": "Logo de l'Entreprise",
|
||||
"alt_business_card": "Carte de Visite",
|
||||
"alt_preview": "Aperçu"
|
||||
}
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Dados da Empresa"
|
||||
},
|
||||
"title": "Empresas",
|
||||
"new_company": "Nova Empresa",
|
||||
"list_title": "Lista de Empresas",
|
||||
"name": "Nome",
|
||||
"email": "Email",
|
||||
"type": "Tipo",
|
||||
"status": "Status",
|
||||
"created_at": "Criado em",
|
||||
"actions": "Ações",
|
||||
"no_companies": "Nenhuma empresa encontrada.",
|
||||
"confirm_delete": "Tem certeza que deseja excluir esta empresa?",
|
||||
"view": "Visualizar",
|
||||
"edit": "Editar",
|
||||
"delete": "Excluir",
|
||||
"form": {
|
||||
"description_label": "Descrição",
|
||||
"description_placeholder": "Digite a descrição da empresa aqui..."
|
||||
},
|
||||
"edit_title": "Editar",
|
||||
"create_title": "Criar",
|
||||
"details_title": "Detalhes da Empresa",
|
||||
"company_name": "Nome da Empresa",
|
||||
"description": "Descrição",
|
||||
"phone": "Telefone",
|
||||
"website": "Website",
|
||||
"tax_id": "CNPJ/ID Fiscal",
|
||||
"business_type": "Tipo de Negócio",
|
||||
"individual": "Pessoa Física",
|
||||
"business": "Empresa",
|
||||
"nonprofit": "Sem Fins Lucrativos",
|
||||
"active": "Ativo",
|
||||
"inactive": "Inativo",
|
||||
"address": "Endereço",
|
||||
"city": "Cidade",
|
||||
"state": "Estado",
|
||||
"country": "País",
|
||||
"postal_code": "CEP",
|
||||
"module_visibility_title": "Configurações de Visibilidade do Módulo",
|
||||
"module_header": "Módulo",
|
||||
"enabled_header": "Habilitado",
|
||||
"public_navbar_header": "Navbar Público",
|
||||
"dashboard_navbar_header": "Navbar do Painel",
|
||||
"no_active_modules": "Nenhum módulo ativo encontrado.",
|
||||
"logo": "Logotipo",
|
||||
"preview": "Pré-visualização:",
|
||||
"current_logo": "Logotipo atual:",
|
||||
"business_card": "Cartão de Visitas",
|
||||
"current_business_card": "Cartão atual:",
|
||||
"document": "Documento",
|
||||
"current_document": "Documento atual:",
|
||||
"view_document": "Visualizar Documento",
|
||||
"cancel": "Cancelar",
|
||||
"update_button": "Atualizar Empresa",
|
||||
"create_button": "Criar Empresa",
|
||||
"selected_file": "Arquivo selecionado:",
|
||||
"slug": "Slug",
|
||||
"updated_at": "Atualizado em",
|
||||
"back_to_list": "Voltar para a Lista",
|
||||
"edit_company": "Editar Empresa",
|
||||
"na": "N/A",
|
||||
"alt_logo": "Logotipo da Empresa",
|
||||
"alt_business_card": "Cartão de Visitas",
|
||||
"alt_preview": "Pré-visualização",
|
||||
"platform_config_title": "Configuração da Plataforma",
|
||||
"platform_name": "Nome da Plataforma",
|
||||
"platform_name_placeholder": "Ex: MeuSistema, PlataformaCloud, etc.",
|
||||
"platform_name_help": "Este nome será exibido no título das páginas e na interface do sistema. Apenas Super Admin ou a empresa ID 1 podem alterar.",
|
||||
"ai_agent_config_title": "Configurações do Agente de IA",
|
||||
"ai_provider": "Operadora de IA",
|
||||
"recommended": "Recomendado",
|
||||
"ai_model": "Modelo de IA",
|
||||
"ai_endpoint": "Endpoint",
|
||||
"optional": "Opcional",
|
||||
"api_key_secret": "API Key (Secreta)",
|
||||
"api_key_help": "Chave de API exclusiva para esta empresa."
|
||||
}
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "company",
|
||||
"display_name": "Company",
|
||||
"description": "Company management module",
|
||||
"version": "1.0.0",
|
||||
"author": "SysCMS Team",
|
||||
"dependencies": [
|
||||
"auth"
|
||||
],
|
||||
"permissions": [
|
||||
"company.view",
|
||||
"company.create",
|
||||
"company.edit",
|
||||
"company.delete"
|
||||
],
|
||||
"settings": [],
|
||||
"providers": [
|
||||
"Modules\\Company\\Providers\\CompanyServiceProvider"
|
||||
],
|
||||
"routes": {
|
||||
"web": "routes/web.php"
|
||||
},
|
||||
"menu": {
|
||||
"dashboard": {
|
||||
"icon": "bi bi-building",
|
||||
"label": "company.menu.dashboard",
|
||||
"route": "admin/company/settings",
|
||||
"order": 30
|
||||
}
|
||||
},
|
||||
"views": "views",
|
||||
"locales": "locales",
|
||||
"module_ative": true,
|
||||
"module_public": false,
|
||||
"module_blocked": false,
|
||||
"ready": true,
|
||||
"tables": [
|
||||
"bus"
|
||||
]
|
||||
}
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
use Kernel\Application;
|
||||
|
||||
return function (Application $app) {
|
||||
// Admin routes
|
||||
$app->addRoute('GET', '/admin/companies', [\Modules\Company\Controllers\CompanyController::class, 'index']);
|
||||
$app->addRoute('GET', '/admin/company/settings', [\Modules\Company\Controllers\CompanyController::class, 'settings']);
|
||||
$app->addRoute('GET', '/admin/companies/create', [\Modules\Company\Controllers\CompanyController::class, 'create']);
|
||||
$app->addRoute('POST', '/admin/companies', [\Modules\Company\Controllers\CompanyController::class, 'store']);
|
||||
$app->addRoute('GET', '/admin/companies/{id:\d+}', [\Modules\Company\Controllers\CompanyController::class, 'show']);
|
||||
$app->addRoute('GET', '/admin/companies/{id:\d+}/edit', [\Modules\Company\Controllers\CompanyController::class, 'edit']);
|
||||
$app->addRoute('PUT', '/admin/companies/{id:\d+}', [\Modules\Company\Controllers\CompanyController::class, 'update']);
|
||||
$app->addRoute('DELETE', '/admin/companies/{id:\d+}', [\Modules\Company\Controllers\CompanyController::class, 'destroy']);
|
||||
};
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Company\Tests;
|
||||
|
||||
use Tests\ModuleTestCase;
|
||||
use Modules\Company\Models\Company;
|
||||
use Modules\Company\Controllers\CompanyController;
|
||||
|
||||
/**
|
||||
* Company Module Test
|
||||
*
|
||||
* Tests for the Companies module.
|
||||
* Ensures translations are complete and routes are properly registered.
|
||||
* Validates upload directory structure and compliance with SysCMS rules.
|
||||
*/
|
||||
class CompanyTest extends ModuleTestCase
|
||||
{
|
||||
protected string $moduleName = 'company';
|
||||
|
||||
public function testTranslationsAreComplete(): void
|
||||
{
|
||||
$this->assertTranslationsComplete($this->moduleName);
|
||||
}
|
||||
|
||||
public function testRoutesExist(): void
|
||||
{
|
||||
$this->assertRouteExists('GET', '/admin/companies');
|
||||
$this->assertRouteExists('GET', '/admin/companies/create');
|
||||
$this->assertRouteExists('POST', '/admin/companies/store');
|
||||
$this->assertRouteExists('GET', '/admin/companies/edit/{id}');
|
||||
$this->assertRouteExists('POST', '/admin/companies/update/{id}');
|
||||
$this->assertRouteExists('GET', '/admin/companies/delete/{id}');
|
||||
$this->assertRouteExists('GET', '/admin/companies/{id}');
|
||||
}
|
||||
|
||||
public function testStorageRouteExists(): void
|
||||
{
|
||||
$this->assertRouteExists('GET', '/storage/uploads/{module}/{filename}');
|
||||
}
|
||||
|
||||
public function testCompanyModelTable(): void
|
||||
{
|
||||
$company = new Company();
|
||||
$this->assertEquals('bus', $company->getTable());
|
||||
}
|
||||
|
||||
public function testCompanyModelHasUploadFields(): void
|
||||
{
|
||||
$company = new Company();
|
||||
$fillable = $company->getFillable();
|
||||
|
||||
$this->assertContains('bus_logo', $fillable);
|
||||
$this->assertContains('bus_business_card', $fillable);
|
||||
$this->assertContains('bus_document', $fillable);
|
||||
}
|
||||
|
||||
public function testCompanyModelHasUrlAccessors(): void
|
||||
{
|
||||
$company = new Company();
|
||||
|
||||
// Use reflection to check if methods exist
|
||||
$reflection = new \ReflectionClass($company);
|
||||
$this->assertTrue($reflection->hasMethod('getLogoUrlAttribute'));
|
||||
$this->assertTrue($reflection->hasMethod('getBusinessCardUrlAttribute'));
|
||||
$this->assertTrue($reflection->hasMethod('getDocumentUrlAttribute'));
|
||||
}
|
||||
|
||||
public function testUploadDirectoriesExist(): void
|
||||
{
|
||||
$baseDir = dirname(__DIR__, 4) . '/storage/uploads/company/';
|
||||
$this->assertTrue(is_dir($baseDir . 'logos'));
|
||||
$this->assertTrue(is_dir($baseDir . 'business-cards'));
|
||||
$this->assertTrue(is_dir($baseDir . 'documents'));
|
||||
}
|
||||
|
||||
public function testUploadDirectoriesAreWritable(): void
|
||||
{
|
||||
$baseDir = dirname(__DIR__, 4) . '/storage/uploads/company/';
|
||||
$this->assertTrue(is_writable($baseDir . 'logos'));
|
||||
$this->assertTrue(is_writable($baseDir . 'business-cards'));
|
||||
$this->assertTrue(is_writable($baseDir . 'documents'));
|
||||
}
|
||||
|
||||
public function testLogoUrlAccessorReturnsCorrectPath(): void
|
||||
{
|
||||
$company = new Company();
|
||||
$company->bus_logo = 'test_logo.jpg';
|
||||
|
||||
$url = $company->logo_url;
|
||||
$this->assertStringContainsString('storage/uploads/company/logos/test_logo.jpg', $url);
|
||||
$this->assertStringStartsWith(url('/'), $url);
|
||||
}
|
||||
|
||||
public function testBusinessCardUrlAccessorReturnsCorrectPath(): void
|
||||
{
|
||||
$company = new Company();
|
||||
$company->bus_business_card = 'test_card.jpg';
|
||||
|
||||
$url = $company->business_card_url;
|
||||
$this->assertStringContainsString('storage/uploads/company/business-cards/test_card.jpg', $url);
|
||||
$this->assertStringStartsWith(url('/'), $url);
|
||||
}
|
||||
|
||||
public function testDocumentUrlAccessorReturnsCorrectPath(): void
|
||||
{
|
||||
$company = new Company();
|
||||
$company->bus_document = 'test_doc.pdf';
|
||||
|
||||
$url = $company->document_url;
|
||||
$this->assertStringContainsString('storage/uploads/company/documents/test_doc.pdf', $url);
|
||||
$this->assertStringStartsWith(url('/'), $url);
|
||||
}
|
||||
|
||||
public function testCompanyControllerMethodsExist(): void
|
||||
{
|
||||
$view = new \Kernel\View();
|
||||
$controller = new CompanyController($view);
|
||||
$reflection = new \ReflectionClass($controller);
|
||||
|
||||
$this->assertTrue($reflection->hasMethod('index'));
|
||||
$this->assertTrue($reflection->hasMethod('create'));
|
||||
$this->assertTrue($reflection->hasMethod('store'));
|
||||
$this->assertTrue($reflection->hasMethod('show'));
|
||||
$this->assertTrue($reflection->hasMethod('edit'));
|
||||
$this->assertTrue($reflection->hasMethod('update'));
|
||||
$this->assertTrue($reflection->hasMethod('destroy'));
|
||||
}
|
||||
}
|
||||
Executable
+295
@@ -0,0 +1,295 @@
|
||||
{% extends "layouts/dashboard.twig" %}
|
||||
|
||||
{% block title %}{{ title }} - {{ platformName }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">{{ title }}</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="{{ url('admin/dashboard') }}">{{ __('dashboard.dashboard') }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ url('admin/companies') }}">{{ __('company.title') }}</a></li>
|
||||
<li class="breadcrumb-item active">{{ target_company ? __('company.edit_title') : __('company.create_title') }}</li>
|
||||
</ol>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-building me-1"></i>
|
||||
{{ __('company.details_title') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="{{ target_company ? url('admin/companies/' ~ target_company.bus_id) : url('admin/companies') }}" method="POST" enctype="multipart/form-data">
|
||||
{% if target_company %}
|
||||
<input type="hidden" name="_method" value="PUT">
|
||||
{% endif %}
|
||||
{{ csrf_field()|raw }}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">{{ __('company.company_name') }}</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="{{ target_company ? target_company.bus_name : '' }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">{{ __('company.email') }}</label>
|
||||
<input type="email" class="form-control" id="email" name="email" value="{{ target_company ? target_company.bus_email : '' }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">{{ __('company.description') }}</label>
|
||||
<div id="quill-editor" style="height: 300px;" data-target="description">{{ target_company ? target_company.bus_description|raw : '' }}</div>
|
||||
<textarea class="form-control" id="description" name="description" style="display:none">{{ target_company ? target_company.bus_description : '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="phone" class="form-label">{{ __('company.phone') }}</label>
|
||||
<input type="text" class="form-control" id="phone" name="phone" value="{{ target_company ? target_company.bus_phone : '' }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="website" class="form-label">{{ __('company.website') }}</label>
|
||||
<input type="url" class="form-control" id="website" name="website" value="{{ target_company ? target_company.bus_website : '' }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="tax_id" class="form-label">{{ __('company.tax_id') }}</label>
|
||||
<input type="text" class="form-control" id="tax_id" name="tax_id" value="{{ target_company ? target_company.bus_tax_id : '' }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="bus_type" class="form-label">{{ __('company.business_type') }}</label>
|
||||
<select class="form-select" id="bus_type" name="bus_type">
|
||||
<option value="individual" {{ target_company and target_company.bus_type == 'individual' ? 'selected' : '' }}>{{ __('company.individual') }}</option>
|
||||
<option value="business" {{ target_company and target_company.bus_type == 'business' ? 'selected' : '' }}>{{ __('company.business') }}</option>
|
||||
<option value="nonprofit" {{ target_company and target_company.bus_type == 'nonprofit' ? 'selected' : '' }}>{{ __('company.nonprofit') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="status" class="form-label">{{ __('company.status') }}</label>
|
||||
<select class="form-select" id="status" name="status">
|
||||
<option value="active" {{ target_company and target_company.bus_status == 'active' ? 'selected' : '' }}>{{ __('company.active') }}</option>
|
||||
<option value="inactive" {{ target_company and target_company.bus_status == 'inactive' ? 'selected' : '' }}>{{ __('company.inactive') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% if user.isSuperAdmin() or target_company and target_company.bus_id == 1 %}
|
||||
<div class="card mb-3 border-warning">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<i class="bi bi-globe me-1"></i> {{ __('company.platform_config_title') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="platform_name" class="form-label">{{ __('company.platform_name') }}</label>
|
||||
<input type="text" class="form-control" id="platform_name" name="platform_name" value="{{ target_company ? target_company.platform_name : '' }}" placeholder="{{ __('company.platform_name_placeholder') }}">
|
||||
<div class="form-text">{{ __('company.platform_name_help') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card mb-4 border-primary">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<i class="bi bi-robot me-1"></i> {{ __('company.ai_agent_config_title') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="ai_provider" class="form-label">{{ __('company.ai_provider') }}</label>
|
||||
<select class="form-select" id="ai_provider" name="ai_provider">
|
||||
<option value="openrouter" {{ target_company and target_company.ai_provider == 'openrouter' ? 'selected' : '' }}>OpenRouter ({{ __('company.recommended') }})</option>
|
||||
<option value="openai" {{ target_company and target_company.ai_provider == 'openai' ? 'selected' : '' }}>OpenAI (Direct)</option>
|
||||
<option value="gemini" {{ target_company and target_company.ai_provider == 'gemini' ? 'selected' : '' }}>Google Gemini (Direct)</option>
|
||||
<option value="zai" {{ target_company and target_company.ai_provider == 'zai' ? 'selected' : '' }}>Z.ai (Direct)</option>
|
||||
<option value="ollama" {{ target_company and target_company.ai_provider == 'ollama' ? 'selected' : '' }}>Ollama (Local)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="ai_model" class="form-label">{{ __('company.ai_model') }}</label>
|
||||
<input type="text" class="form-control" id="ai_model" name="ai_model" value="{{ target_company ? target_company.ai_model : 'google/gemini-2.0-flash-001' }}" placeholder="ex: stepfun/step-3.5-flash:free">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="ai_endpoint" class="form-label">{{ __('company.ai_endpoint') }} ({{ __('company.optional') }})</label>
|
||||
<input type="text" class="form-control" id="ai_endpoint" name="ai_endpoint" value="{{ target_company ? target_company.ai_endpoint : '' }}" placeholder="http://localhost:11434/v1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="openrouter_api_key" class="form-label">{{ __('company.api_key_secret') }}</label>
|
||||
<input type="password" class="form-control" id="openrouter_api_key" name="openrouter_api_key" value="{{ target_company ? target_company.openrouter_api_key : '' }}" placeholder="sk-...">
|
||||
<div class="form-text">{{ __('company.api_key_help') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="address" class="form-label">{{ __('company.address') }}</label>
|
||||
<input type="text" class="form-control" id="address" name="address" value="{{ target_company ? target_company.bus_address : '' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="city" class="form-label">{{ __('company.city') }}</label>
|
||||
<input type="text" class="form-control" id="city" name="city" value="{{ target_company ? target_company.bus_city : '' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="state" class="form-label">{{ __('company.state') }}</label>
|
||||
<input type="text" class="form-control" id="state" name="state" value="{{ target_company ? target_company.bus_state : '' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="country" class="form-label">{{ __('company.country') }}</label>
|
||||
<input type="text" class="form-control" id="country" name="country" value="{{ target_company ? target_company.bus_country : '' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="postal_code" class="form-label">{{ __('company.postal_code') }}</label>
|
||||
<input type="text" class="form-control" id="postal_code" name="postal_code" value="{{ target_company ? target_company.bus_postal_code : '' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if user.isSuperAdmin() %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
{{ __('company.module_visibility_title') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if modules %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __('company.module_header') }}</th>
|
||||
<th class="text-center">{{ __('company.enabled_header') }}</th>
|
||||
<th class="text-center">{{ __('company.public_navbar_header') }}</th>
|
||||
<th class="text-center">{{ __('company.dashboard_navbar_header') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for module in modules %}
|
||||
{% set isEnabled = false %}
|
||||
{% set showPublic = true %}
|
||||
{% set showDashboard = true %}
|
||||
|
||||
{% if target_company %}
|
||||
{% for cm in target_company.modules %}
|
||||
{% if cm.modules_id == module.modules_id %}
|
||||
{% set isEnabled = cm.pivot.bus_modules_is_enabled %}
|
||||
{% set showPublic = cm.pivot.bus_modules_show_public_nav %}
|
||||
{% set showDashboard = cm.pivot.bus_modules_show_dashboard_nav %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<input type="hidden" name="modules[{{ module.modules_id }}][present]" value="1">
|
||||
<strong>{{ module.modules_display_name }}</strong>
|
||||
<small class="d-block text-muted">{{ module.modules_description }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="form-check d-inline-block">
|
||||
<input class="form-check-input" type="checkbox" name="modules[{{ module.modules_id }}][enabled]" id="module_enabled_{{ module.modules_id }}" {{ isEnabled ? 'checked' : '' }}>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="form-check d-inline-block">
|
||||
<input class="form-check-input" type="checkbox" name="modules[{{ module.modules_id }}][public]" id="module_public_{{ module.modules_id }}" {{ showPublic ? 'checked' : '' }}>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="form-check d-inline-block">
|
||||
<input class="form-check-input" type="checkbox" name="modules[{{ module.modules_id }}][dashboard]" id="module_dashboard_{{ module.modules_id }}" {{ showDashboard ? 'checked' : '' }}>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">{{ __('company.no_active_modules') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="logo" class="form-label">{{ __('company.logo') }}</label>
|
||||
<input type="file" class="form-control" id="logo" name="logo" accept="image/*">
|
||||
<div id="logoPreview" class="mt-2"></div>
|
||||
{% if target_company and target_company.bus_logo %}
|
||||
<div class="mt-2">
|
||||
<small class="form-text text-muted">{{ __('company.current_logo') }}</small>
|
||||
<img src="{{ target_company.logo_url }}" alt="{{ __('company.alt_logo') }}" class="img-thumbnail" style="max-height: 100px;">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="business_card" class="form-label">{{ __('company.business_card') }}</label>
|
||||
<input type="file" class="form-control" id="business_card" name="business_card" accept="image/*">
|
||||
<div id="businessCardPreview" class="mt-2"></div>
|
||||
{% if target_company and target_company.bus_business_card %}
|
||||
<div class="mt-2">
|
||||
<small class="form-text text-muted">{{ __('company.current_business_card') }}</small>
|
||||
<img src="{{ target_company.business_card_url }}" alt="{{ __('company.alt_business_card') }}" class="img-thumbnail" style="max-height: 100px;">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 border-top pt-3">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-save me-1"></i> {{ target_company ? __('company.update_button') : __('company.create_button') }}</button>
|
||||
<a href="{{ url('admin/companies') }}" class="btn btn-secondary"><i class="bi bi-x-lg me-1"></i> {{ __('company.cancel') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Logo Preview
|
||||
document.getElementById('logo').addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('logoPreview').innerHTML = `<img src="${e.target.result}" class="img-thumbnail" style="max-height: 100px;">`;
|
||||
}
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Business Card Preview
|
||||
document.getElementById('business_card').addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('businessCardPreview').innerHTML = `<img src="${e.target.result}" class="img-thumbnail" style="max-height: 100px;">`;
|
||||
}
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
{% extends "layouts/dashboard.twig" %}
|
||||
|
||||
{% block title %}{{ title }} - {{ platformName }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">{{ title }}</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="{{ url('admin/dashboard') }}">{{ __('dashboard.dashboard') }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ url('admin/erp') }}">ERP</a></li>
|
||||
<li class="breadcrumb-item active">{{ __('company.title') }}</li>
|
||||
</ol>
|
||||
|
||||
<div class="mb-4 d-flex justify-content-between">
|
||||
<a href="/admin/erp" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> {{ __('erp.admin.back_to_erp') }}
|
||||
</a>
|
||||
{% if can_create_company %}
|
||||
<a href="{{ url('admin/companies/create') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus me-1"></i> {{ __('company.new_company') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-building me-1"></i>
|
||||
{{ __('company.list_title') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __('company.name') }}</th>
|
||||
<th>{{ __('company.email') }}</th>
|
||||
<th>{{ __('company.type') }}</th>
|
||||
<th>{{ __('company.status') }}</th>
|
||||
<th>{{ __('company.created_at') }}</th>
|
||||
<th>{{ __('company.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for company in companies %}
|
||||
<tr>
|
||||
<td>{{ company.bus_name }}</td>
|
||||
<td>{{ company.bus_email }}</td>
|
||||
<td>{{ __('company.' ~ company.bus_type) }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{{ company.bus_status == 'active' ? 'success' : 'secondary' }}">
|
||||
{{ __('company.' ~ company.bus_status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ company.created_at|date("d/m/Y H:i") }}</td>
|
||||
<td>
|
||||
<a href="{{ url('admin/companies/' ~ company.bus_id) }}" class="btn btn-info btn-sm" title="{{ __('company.view') }}">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a href="{{ url('admin/companies/' ~ company.bus_id ~ '/edit') }}" class="btn btn-warning btn-sm" title="{{ __('company.edit') }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% if user.isSuperAdmin() %}
|
||||
<form action="{{ url('admin/companies/' ~ company.bus_id) }}" method="POST" class="d-inline" onsubmit="return confirm('{{ __('company.confirm_delete') }}');">
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
{{ csrf_field()|raw }}
|
||||
<button type="submit" class="btn btn-danger btn-sm" title="{{ __('company.delete') }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">{{ __('company.no_companies') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Executable
+176
@@ -0,0 +1,176 @@
|
||||
{% extends "layouts/dashboard.twig" %}
|
||||
|
||||
{% block title %}{{ title }} - {{ platformName }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">{{ title }}</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="{{ url('admin/dashboard') }}">{{ __('dashboard.dashboard') }}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ url('admin/companies') }}">{{ __('company.title') }}</a></li>
|
||||
<li class="breadcrumb-item active">{{ company.bus_name }}</li>
|
||||
</ol>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-building me-1"></i>
|
||||
{{ __('company.details_title') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.company_name') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-plaintext">{{ company.bus_name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.slug') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-plaintext">{{ company.bus_slug }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.email') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-plaintext">{{ company.bus_email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.description') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-plaintext">{{ company.bus_description ?: __('company.na') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.phone') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-plaintext">{{ company.bus_phone ?: __('company.na') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.website') }}</label>
|
||||
<div class="col-sm-10">
|
||||
{% if company.website %}
|
||||
<p class="form-control-plaintext">
|
||||
<a href="{{ company.bus_website }}" target="_blank">{{ company.bus_website }}</a>
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="form-control-plaintext">{{ __('company.na') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.tax_id') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-plaintext">{{ company.bus_tax_id ?: __('company.na') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.business_type') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-plaintext">
|
||||
{% if company.bus_type == 'individual' %}
|
||||
{{ __('company.individual') }}
|
||||
{% elseif company.bus_type == 'business' %}
|
||||
{{ __('company.business') }}
|
||||
{% elseif company.bus_type == 'nonprofit' %}
|
||||
{{ __('company.nonprofit') }}
|
||||
{% else %}
|
||||
{{ company.bus_type|capitalize }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.status') }}</label>
|
||||
<div class="col-sm-10">
|
||||
{% if company.bus_status == 'active' %}
|
||||
<span class="badge bg-success">{{ __('company.active') }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ __('company.inactive') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.address') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-plaintext">
|
||||
{% if company.bus_address or company.bus_city or company.bus_state or company.bus_country or company.bus_postal_code %}
|
||||
{{ company.bus_address }}<br>
|
||||
{% if company.bus_city or company.bus_state or company.bus_postal_code %}
|
||||
{{ company.bus_city }}{{ company.bus_city and company.bus_state ? ', ' : '' }}{{ company.bus_state }} {{ company.bus_postal_code }}<br>
|
||||
{% endif %}
|
||||
{% if company.bus_country %}
|
||||
{{ company.bus_country }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ __('company.na') }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.logo') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<img src="{{ company.logo_url }}" alt="{{ __('company.alt_logo') }}" class="img-thumbnail" style="max-height: 150px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.business_card') }}</label>
|
||||
<div class="col-sm-10">
|
||||
{% if company.bus_business_card %}
|
||||
<img src="{{ company.business_card_url }}" alt="{{ __('company.alt_business_card') }}" class="img-thumbnail" style="max-height: 150px;">
|
||||
{% else %}
|
||||
<p class="form-control-plaintext">{{ __('company.na') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.document') }}</label>
|
||||
<div class="col-sm-10">
|
||||
{% if company.bus_document %}
|
||||
<a href="{{ company.document_url }}" target="_blank" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-file-earmark-text me-1"></i> {{ __('company.view_document') }}
|
||||
</a>
|
||||
{% else %}
|
||||
<p class="form-control-plaintext">{{ __('company.na') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.created_at') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-plaintext">{{ company.created_at|date("Y-m-d H:i:s") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label fw-bold">{{ __('company.updated_at') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-plaintext">{{ company.updated_at|date("Y-m-d H:i:s") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{{ url('admin/companies') }}" class="btn btn-secondary">{{ __('company.back_to_list') }}</a>
|
||||
<div>
|
||||
<a href="{{ url('admin/companies/' ~ company.bus_id ~ '/edit') }}" class="btn btn-primary">{{ __('company.edit_company') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
SysCMS Commercial License
|
||||
|
||||
Copyright (c) 2026 Marco Costa. All rights reserved.
|
||||
|
||||
This software is licensed, not sold. Permission is granted to use this software subject to the terms of the SysCMS Commercial License.
|
||||
|
||||
Redistribution, sublicensing, resale, or disclosure of this software, in whole or in part, is strictly prohibited without prior written permission from the author.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
# ConfigIndex Module for SysCMS
|
||||
|
||||
Configuration and index management module.
|
||||
|
||||
## Features
|
||||
- System configuration management
|
||||
- Index page customization
|
||||
- Settings administration
|
||||
|
||||
## Installation
|
||||
1. Upload the `configindex.zip` file through the module manager or extract it to the `modules/` directory.
|
||||
2. Run the migrations to register the module.
|
||||
3. Configure settings in the admin panel.
|
||||
|
||||
## Routes
|
||||
- Web routes: `routes/web.php`
|
||||
|
||||
## License
|
||||
Copyright (c) 2026 Marco Costa. All rights reserved.
|
||||
Licensed under the SysCMS Commercial License.
|
||||
@@ -0,0 +1,493 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Configindex\Controllers;
|
||||
|
||||
use App\Controllers\BaseAdminController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Modules\Configindex\Models\ConfigPage;
|
||||
|
||||
class ConfigIndexController extends BaseAdminController
|
||||
{
|
||||
/**
|
||||
* List pages to configure
|
||||
*/
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user) {
|
||||
return new RedirectResponse(url('login'));
|
||||
}
|
||||
|
||||
$theme = $request->query->get('theme', \Kernel\View::getActiveTheme() ?? 'default');
|
||||
|
||||
$pagesKeys = $this->getPagesKeys($theme);
|
||||
$pages = [];
|
||||
foreach ($pagesKeys as $key) {
|
||||
$pages[$key] = ucfirst($key);
|
||||
}
|
||||
|
||||
$themesDir = dirname(dirname(dirname(dirname(__DIR__)))) . '/themes';
|
||||
$availableThemes = ['default' => 'Default'];
|
||||
if (is_dir($themesDir)) {
|
||||
$folders = array_diff(scandir($themesDir), ['.', '..']);
|
||||
foreach ($folders as $folder) {
|
||||
if (is_dir($themesDir . '/' . $folder)) {
|
||||
$availableThemes[$folder] = ucfirst($folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->renderAdminView('configindex/views/admin/index.twig', [
|
||||
'title' => __('configindex.title'),
|
||||
'pages' => $pages,
|
||||
'currentTheme' => $theme,
|
||||
'availableThemes' => $availableThemes
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show create form
|
||||
*/
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user) {
|
||||
return new RedirectResponse(url('login'));
|
||||
}
|
||||
|
||||
return $this->renderAdminView('configindex/views/admin/create.twig', [
|
||||
'title' => 'Nova Página Index'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store new page
|
||||
*/
|
||||
public function store(Request $request): Response
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user) {
|
||||
return new RedirectResponse(url('login'));
|
||||
}
|
||||
|
||||
$key = $request->request->get('key');
|
||||
if (!$key) {
|
||||
return new RedirectResponse(url('admin/configindex/create?error=missing_key'));
|
||||
}
|
||||
|
||||
// Sanitiza a chave (slug)
|
||||
$key = strtolower(preg_replace('/[^a-zA-Z0-9_-]/', '', $key));
|
||||
|
||||
$busId = $user->users_bus_id ?? 1;
|
||||
|
||||
// Verifica se já existe
|
||||
$existing = ConfigPage::getByKey($key, $busId);
|
||||
if ($existing) {
|
||||
return new RedirectResponse(url('admin/configindex/create?error=already_exists'));
|
||||
}
|
||||
|
||||
// Cria o registro no banco
|
||||
$page = new ConfigPage();
|
||||
$page->config_index_bus_id = $busId;
|
||||
$page->config_index_key = $key;
|
||||
$page->config_index_title = ucfirst($key);
|
||||
$page->config_index_show_public_nav = true;
|
||||
$page->config_index_show_dashboard_nav = true;
|
||||
$page->save();
|
||||
|
||||
// Cria o arquivo físico se não existir
|
||||
$viewsPath = dirname(__DIR__, 3) . '/index/views';
|
||||
$templatePath = $viewsPath . '/' . $key . '.twig';
|
||||
|
||||
if (!is_dir($viewsPath)) {
|
||||
mkdir($viewsPath, 0755, true);
|
||||
}
|
||||
|
||||
if (!is_file($templatePath)) {
|
||||
$defaultContent = "{% extends '@index/layout.twig' %}\n\n{% block content %}\n<div class=\"container my-5\">\n <h1>{{ __('index." . $key . ".title') }}</h1>\n <p>{{ __('index." . $key . ".description') }}</p>\n</div>\n{% endblock %}\n";
|
||||
file_put_contents($templatePath, $defaultContent);
|
||||
}
|
||||
|
||||
// Cria arquivos de tradução padrão
|
||||
$localesPath = dirname(__DIR__, 3) . '/index/locales';
|
||||
$languages = ['pt_BR', 'en_US', 'es_ES', 'de_DE', 'fr_FR'];
|
||||
|
||||
foreach ($languages as $lang) {
|
||||
$savePath = $localesPath . '/' . $lang . '/index.json';
|
||||
if (!is_dir(dirname($savePath))) {
|
||||
mkdir(dirname($savePath), 0755, true);
|
||||
}
|
||||
|
||||
$base = [];
|
||||
if (is_file($savePath)) {
|
||||
$base = json_decode(file_get_contents($savePath), true) ?: [];
|
||||
}
|
||||
|
||||
if (!isset($base[$key])) {
|
||||
$base[$key] = [
|
||||
'title' => ucfirst($key),
|
||||
'description' => 'Conteúdo da página ' . $key
|
||||
];
|
||||
file_put_contents($savePath, json_encode($base, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
|
||||
return new RedirectResponse(url('admin/configindex/' . $key . '/edit?success=created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all valid page keys for a theme
|
||||
*/
|
||||
private function getPagesKeys(string $themeSlug): array
|
||||
{
|
||||
$keys = [];
|
||||
$baseDir = dirname(dirname(dirname(dirname(__DIR__))));
|
||||
|
||||
// 1. Coleta do módulo index (Padrão do Sistema)
|
||||
$indexViewsPath = $baseDir . '/modules/index/views';
|
||||
if (is_dir($indexViewsPath)) {
|
||||
$files = glob($indexViewsPath . '/*.{twig,php}', GLOB_BRACE);
|
||||
foreach ($files as $file) {
|
||||
$filename = pathinfo($file, PATHINFO_FILENAME);
|
||||
if ($filename !== 'layout') {
|
||||
$keys[] = $filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Coleta do Tema (Se houver overrides de módulos)
|
||||
$themeModuleViewsPath = $baseDir . '/themes/' . $themeSlug . '/modules/index/views';
|
||||
if (is_dir($themeModuleViewsPath)) {
|
||||
$files = glob($themeModuleViewsPath . '/*.{twig,php}', GLOB_BRACE);
|
||||
foreach ($files as $file) {
|
||||
$filename = pathinfo($file, PATHINFO_FILENAME);
|
||||
if ($filename !== 'layout') {
|
||||
$keys[] = $filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Coleta do Tema (Views genéricas na raiz do tema ou subpasta views)
|
||||
$themeRootViews = [
|
||||
$baseDir . '/themes/' . $themeSlug,
|
||||
$baseDir . '/themes/' . $themeSlug . '/views'
|
||||
];
|
||||
foreach ($themeRootViews as $path) {
|
||||
if (is_dir($path)) {
|
||||
$files = glob($path . '/*.{twig,php}', GLOB_BRACE);
|
||||
foreach ($files as $file) {
|
||||
$filename = pathinfo($file, PATHINFO_FILENAME);
|
||||
// Ignorar arquivos de sistema e layouts
|
||||
if (!in_array($filename, ['layout', 'header', 'footer', 'sidebar'])) {
|
||||
$keys[] = $filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($keys)) {
|
||||
$keys = ['index', 'about', 'contact', 'services'];
|
||||
}
|
||||
|
||||
return array_unique($keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show edit form
|
||||
*/
|
||||
public function edit(Request $request, string $key): Response
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user) {
|
||||
return new RedirectResponse(url('login'));
|
||||
}
|
||||
|
||||
$theme = $request->query->get('theme', \Kernel\View::getActiveTheme() ?? 'default');
|
||||
$busId = $user->users_bus_id ?? 1;
|
||||
$page = ConfigPage::getByKey($key, $busId);
|
||||
|
||||
$templateCode = null;
|
||||
$baseDir = dirname(dirname(dirname(dirname(__DIR__))));
|
||||
|
||||
// Prioridade de busca do arquivo para edição:
|
||||
// 1. Override do módulo no tema selecionado
|
||||
$paths = [
|
||||
$baseDir . '/themes/' . $theme . '/modules/index/views/' . $key . '.twig',
|
||||
$baseDir . '/themes/' . $theme . '/modules/index/views/' . $key . '.php',
|
||||
$baseDir . '/themes/' . $theme . '/' . $key . '.twig',
|
||||
$baseDir . '/themes/' . $theme . '/' . $key . '.php',
|
||||
$baseDir . '/themes/' . $theme . '/views/' . $key . '.twig',
|
||||
$baseDir . '/themes/' . $theme . '/views/' . $key . '.php',
|
||||
$baseDir . '/modules/index/views/' . $key . '.twig',
|
||||
$baseDir . '/modules/index/views/' . $key . '.php',
|
||||
];
|
||||
|
||||
$foundPath = null;
|
||||
foreach ($paths as $path) {
|
||||
if (is_file($path)) {
|
||||
$templateCode = file_get_contents($path);
|
||||
$foundPath = $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$languagesMap = [];
|
||||
|
||||
// Locais de tradução: Módulo index + Tema selecionado
|
||||
$localesPaths = [
|
||||
$baseDir . '/modules/index/locales',
|
||||
$baseDir . '/themes/' . $theme . '/locales'
|
||||
];
|
||||
|
||||
foreach ($localesPaths as $base) {
|
||||
if (is_dir($base)) {
|
||||
foreach (glob($base . '/*', GLOB_ONLYDIR) as $dir) {
|
||||
$languagesMap[basename($dir)] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$languages = array_keys($languagesMap);
|
||||
if (empty($languages)) {
|
||||
$languages = ['pt_BR', 'en_US', 'es_ES', 'de_DE', 'fr_FR'];
|
||||
}
|
||||
sort($languages);
|
||||
|
||||
$validKeys = $this->getPagesKeys($theme);
|
||||
|
||||
$translationsIndexSection = [];
|
||||
foreach ($languages as $lang) {
|
||||
$langData = [];
|
||||
|
||||
foreach ($localesPaths as $base) {
|
||||
$path = $base . '/' . $lang . '/index.json';
|
||||
if (is_file($path)) {
|
||||
$decoded = json_decode(file_get_contents($path), true);
|
||||
if (is_array($decoded)) {
|
||||
$langData = array_merge($langData, $decoded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($key === 'index') {
|
||||
$indexData = [];
|
||||
foreach ($langData as $k => $v) {
|
||||
if (!in_array($k, $validKeys) || $k === 'index') {
|
||||
$indexData[$k] = $v;
|
||||
}
|
||||
}
|
||||
$translationsIndexSection[$lang] = json_encode($indexData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
} elseif (isset($langData[$key])) {
|
||||
$translationsIndexSection[$lang] = json_encode($langData[$key], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
} else {
|
||||
$translationsIndexSection[$lang] = json_encode(new \stdClass(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->renderAdminView('configindex/views/admin/edit.twig', [
|
||||
'title' => __('configindex.edit_title', ['page' => ucfirst($key)]),
|
||||
'page_key' => $key,
|
||||
'page_data' => $page,
|
||||
'template_code' => $templateCode,
|
||||
'languages' => $languages,
|
||||
'translations_index_section' => $translationsIndexSection,
|
||||
'currentTheme' => $theme,
|
||||
'foundPath' => $foundPath,
|
||||
'base_path' => $baseDir
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update page content
|
||||
*/
|
||||
public function update(Request $request, string $key): Response
|
||||
{
|
||||
try {
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user) {
|
||||
return new RedirectResponse(url('login'));
|
||||
}
|
||||
|
||||
$busId = $user->users_bus_id ?? 1;
|
||||
|
||||
$data = $request->request->all();
|
||||
|
||||
$page = ConfigPage::getByKey($key, $busId);
|
||||
|
||||
if (!$page) {
|
||||
$page = new ConfigPage();
|
||||
$page->config_index_bus_id = $busId;
|
||||
$page->config_index_key = $key;
|
||||
$page->config_index_title = ucfirst($key);
|
||||
$page->config_index_meta_title = '';
|
||||
$page->config_index_meta_description = '';
|
||||
}
|
||||
|
||||
if (isset($data['content'])) {
|
||||
$page->config_index_content = $data['content'];
|
||||
}
|
||||
$page->config_index_show_public_nav = !empty($data['config_index_show_public_nav']);
|
||||
$page->config_index_show_dashboard_nav = !empty($data['config_index_show_dashboard_nav']);
|
||||
|
||||
$page->save();
|
||||
|
||||
$codeToWrite = $data['content'] ?? ($data['template_code'] ?? null);
|
||||
if ($codeToWrite !== null) {
|
||||
$theme = $request->query->get('theme', \Kernel\View::getActiveTheme() ?? 'default');
|
||||
$baseDir = dirname(dirname(dirname(dirname(__DIR__))));
|
||||
|
||||
// Determina o caminho de salvamento baseado no tema
|
||||
if ($theme === 'default') {
|
||||
$templatePath = $baseDir . '/modules/index/views/' . $key . '.twig';
|
||||
// Fallback para .php se já existir
|
||||
if (!is_file($templatePath) && is_file(str_replace('.twig', '.php', $templatePath))) {
|
||||
$templatePath = str_replace('.twig', '.php', $templatePath);
|
||||
}
|
||||
} else {
|
||||
// Para temas, salvamos no override do módulo dentro do tema
|
||||
$templatePath = $baseDir . '/themes/' . $theme . '/modules/index/views/' . $key . '.twig';
|
||||
}
|
||||
|
||||
$viewsDir = dirname($templatePath);
|
||||
if (!is_dir($viewsDir)) {
|
||||
mkdir($viewsDir, 0755, true);
|
||||
}
|
||||
file_put_contents($templatePath, $codeToWrite);
|
||||
}
|
||||
|
||||
// Update translations
|
||||
$translations = $request->request->all('translations_index_section');
|
||||
if (is_array($translations)) {
|
||||
$theme = $request->query->get('theme', \Kernel\View::getActiveTheme() ?? 'default');
|
||||
$baseDir = dirname(dirname(dirname(dirname(__DIR__))));
|
||||
|
||||
// Salva no local apropriado (Módulo ou Tema)
|
||||
if ($theme === 'default') {
|
||||
$localesBase = $baseDir . '/modules/index/locales';
|
||||
} else {
|
||||
$localesBase = $baseDir . '/themes/' . $theme . '/locales';
|
||||
}
|
||||
|
||||
foreach ($translations as $lang => $json) {
|
||||
if ($json !== null && $json !== '') {
|
||||
$section = json_decode($json, true);
|
||||
if (is_array($section)) {
|
||||
$savePath = $localesBase . '/' . $lang . '/index.json';
|
||||
if (!is_dir(dirname($savePath))) {
|
||||
mkdir(dirname($savePath), 0755, true);
|
||||
}
|
||||
|
||||
$base = [];
|
||||
if (is_file($savePath)) {
|
||||
$base = json_decode(file_get_contents($savePath), true) ?: [];
|
||||
}
|
||||
|
||||
if ($key === 'index') {
|
||||
// Mescla chaves soltas na raiz, preservando outras páginas
|
||||
$newBase = $section;
|
||||
foreach ($base as $k => $v) {
|
||||
if (is_array($v) && $k !== 'index' && $k !== 'layout' && $k !== 'menu') {
|
||||
$newBase[$k] = $v;
|
||||
}
|
||||
}
|
||||
$base = $newBase;
|
||||
} else {
|
||||
// Salva na seção da página
|
||||
$base[$key] = $section;
|
||||
}
|
||||
|
||||
file_put_contents($savePath, json_encode($base, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
\Kernel\Translator::setLocale(\Kernel\Translator::getLocale());
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
$theme = $request->query->get('theme', \Kernel\View::getActiveTheme() ?? 'default');
|
||||
return new RedirectResponse(url('admin/configindex/' . $key . '/edit?theme=' . $theme . '&success=updated'));
|
||||
} catch (\Exception $e) {
|
||||
error_log("Error in ConfigIndexController::update: " . $e->getMessage());
|
||||
return new Response("Internal Server Error: " . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle page visibility status
|
||||
*/
|
||||
public function toggleStatus(Request $request, string $key): Response
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user) {
|
||||
return new RedirectResponse(url('login'));
|
||||
}
|
||||
|
||||
$busId = $user->users_bus_id ?? 1;
|
||||
$page = ConfigPage::getByKey($key, $busId);
|
||||
|
||||
if ($page) {
|
||||
$page->config_index_show_public_nav = !$page->config_index_show_public_nav;
|
||||
$page->save();
|
||||
}
|
||||
|
||||
return new RedirectResponse(url('admin/configindex?success=status_updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete page and associated files
|
||||
*/
|
||||
public function destroy(Request $request, string $key): Response
|
||||
{
|
||||
$user = $this->getCurrentUser();
|
||||
if (!$user) {
|
||||
return new RedirectResponse(url('login'));
|
||||
}
|
||||
|
||||
// Não permitir excluir a home (index)
|
||||
if ($key === 'index') {
|
||||
return new RedirectResponse(url('admin/configindex?error=cannot_delete_home'));
|
||||
}
|
||||
|
||||
$busId = $user->users_bus_id ?? 1;
|
||||
$page = ConfigPage::getByKey($key, $busId);
|
||||
|
||||
if ($page) {
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
// Remover arquivos físicos (.twig)
|
||||
$viewsPath = dirname(__DIR__, 3) . '/index/views';
|
||||
$templatePath = $viewsPath . '/' . $key . '.twig';
|
||||
if (is_file($templatePath)) {
|
||||
unlink($templatePath);
|
||||
}
|
||||
|
||||
// Remover de .php se existir
|
||||
$phpPath = $viewsPath . '/' . $key . '.php';
|
||||
if (is_file($phpPath)) {
|
||||
unlink($phpPath);
|
||||
}
|
||||
|
||||
// Remover traduções dos arquivos index.json
|
||||
$localesPath = dirname(__DIR__, 3) . '/index/locales';
|
||||
$languages = ['pt_BR', 'en_US', 'es_ES', 'de_DE', 'fr_FR'];
|
||||
|
||||
foreach ($languages as $lang) {
|
||||
$savePath = $localesPath . '/' . $lang . '/index.json';
|
||||
if (is_file($savePath)) {
|
||||
$base = json_decode(file_get_contents($savePath), true) ?: [];
|
||||
if (isset($base[$key])) {
|
||||
unset($base[$key]);
|
||||
file_put_contents($savePath, json_encode($base, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new RedirectResponse(url('admin/configindex?success=deleted'));
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Configindex\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ConfigPage extends Model
|
||||
{
|
||||
protected $table = 'config_index';
|
||||
protected $primaryKey = 'config_index_id';
|
||||
|
||||
protected $fillable = [
|
||||
'config_index_bus_id',
|
||||
'config_index_key',
|
||||
'config_index_title',
|
||||
'config_index_content',
|
||||
'config_index_meta_title',
|
||||
'config_index_meta_description',
|
||||
'config_index_show_public_nav',
|
||||
'config_index_show_dashboard_nav'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'config_index_show_public_nav' => 'boolean',
|
||||
'config_index_show_dashboard_nav' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get content by key and bus_id
|
||||
*
|
||||
* @param string $key
|
||||
* @param int $busId
|
||||
* @return self|null
|
||||
*/
|
||||
public static function getByKey(string $key, int $busId)
|
||||
{
|
||||
return self::where('config_index_key', $key)
|
||||
->where('config_index_bus_id', $busId)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Configindex\Providers;
|
||||
|
||||
use Kernel\View;
|
||||
use Modules\Configindex\Models\ConfigPage;
|
||||
|
||||
class ConfigIndexServiceProvider
|
||||
{
|
||||
public function register()
|
||||
{
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
try {
|
||||
$busId = isset($_SESSION['current_company_id']) ? (int) $_SESSION['current_company_id'] : 1;
|
||||
$visibility = [];
|
||||
|
||||
if (class_exists(ConfigPage::class)) {
|
||||
$baseDir = dirname(dirname(dirname(dirname(__DIR__))));
|
||||
$activeTheme = \Kernel\View::getActiveTheme() ?? 'default';
|
||||
$indexViewsDir = $baseDir . '/modules/index/views';
|
||||
$themeViewsDir = $baseDir . '/themes/' . $activeTheme . '/modules/index/views';
|
||||
|
||||
$pages = ConfigPage::where('config_index_bus_id', $busId)->get();
|
||||
foreach ($pages as $page) {
|
||||
$key = $page->config_index_key;
|
||||
|
||||
// Verifica se o arquivo físico existe
|
||||
$fileExists = false;
|
||||
if ($key === 'index') {
|
||||
$fileExists = true;
|
||||
} else {
|
||||
$possiblePaths = [
|
||||
$indexViewsDir . '/' . $key . '.twig',
|
||||
$indexViewsDir . '/' . $key . '.php',
|
||||
$themeViewsDir . '/' . $key . '.twig',
|
||||
$themeViewsDir . '/' . $key . '.php',
|
||||
$baseDir . '/themes/' . $activeTheme . '/' . $key . '.twig',
|
||||
$baseDir . '/themes/' . $activeTheme . '/' . $key . '.php',
|
||||
$baseDir . '/modules/' . $key . '/views/index.twig',
|
||||
];
|
||||
foreach ($possiblePaths as $path) {
|
||||
if (is_file($path)) {
|
||||
$fileExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$visibility[$key] = [
|
||||
'public' => (bool) $page->config_index_show_public_nav && $fileExists,
|
||||
'dashboard' => (bool) $page->config_index_show_dashboard_nav && $fileExists,
|
||||
'title' => $page->config_index_title ?: ucfirst($key)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Garante que as páginas padrão tenham uma configuração inicial, mas respeita o banco de dados
|
||||
foreach (['about', 'contact', 'services'] as $key) {
|
||||
if (!isset($visibility[$key])) {
|
||||
// Se não estiver no banco, o padrão é VISÍVEL (true) para manter compatibilidade
|
||||
// Mas agora as alterações via admin/configindex PERSISTEM no banco
|
||||
$visibility[$key] = ['public' => true, 'dashboard' => true, 'title' => ucfirst($key)];
|
||||
}
|
||||
}
|
||||
|
||||
$view = new View();
|
||||
$view->getEngine()->addGlobal('config_pages_visibility', $visibility);
|
||||
} catch (\Throwable $e) {
|
||||
$view = new View();
|
||||
$view->getEngine()->addGlobal('config_pages_visibility', [
|
||||
'about' => ['public' => false, 'dashboard' => false, 'title' => 'About'],
|
||||
'contact' => ['public' => false, 'dashboard' => false, 'title' => 'Contact'],
|
||||
'services' => ['public' => false, 'dashboard' => false, 'title' => 'Services'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateConfigPagesTable
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
if (!Capsule::schema()->hasTable('config_index')) {
|
||||
Capsule::schema()->create('config_index', function (Blueprint $table) {
|
||||
$table->increments('config_index_id');
|
||||
$table->integer('config_index_bus_id')->unsigned()->default(1);
|
||||
$table->string('config_index_key')->index();
|
||||
$table->string('config_index_title');
|
||||
$table->longText('config_index_content')->nullable();
|
||||
$table->string('config_index_meta_title')->nullable();
|
||||
$table->text('config_index_meta_description')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['config_index_bus_id', 'config_index_key'], 'unique_page_per_bus');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Capsule::schema()->dropIfExists('config_index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use App\Models\Module;
|
||||
use Modules\Company\Models\Company;
|
||||
|
||||
class InsertModuleRecord
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
// Rename existing 'configpages' record to 'configindex' if present
|
||||
$existingOld = Module::where('modules_name', 'configpages')->first();
|
||||
if ($existingOld) {
|
||||
$settings = $existingOld->modules_settings ?? [];
|
||||
// Update menu route inside settings if structured as expected
|
||||
if (is_array($settings) && isset($settings['menu']['dashboard'])) {
|
||||
$settings['menu']['dashboard']['route'] = 'admin.configindex.index';
|
||||
$settings['menu']['dashboard']['label'] = 'Páginas Index';
|
||||
}
|
||||
|
||||
$existingOld->modules_name = 'configindex';
|
||||
$existingOld->modules_permissions = ['configindex.view', 'configindex.edit'];
|
||||
$existingOld->modules_path = 'modules/configindex';
|
||||
$existingOld->modules_namespace = 'Modules\\Configindex';
|
||||
$existingOld->modules_settings = $settings;
|
||||
$existingOld->modules_update_date = date('Y-m-d H:i:s');
|
||||
$existingOld->save();
|
||||
return; // Updated existing record; skip creation below
|
||||
}
|
||||
$moduleData = [
|
||||
'modules_name' => 'configindex',
|
||||
'modules_display_name' => 'Configuração de Páginas',
|
||||
'modules_description' => 'Gerenciamento de conteúdo das páginas Sobre, Contato e Serviços',
|
||||
'modules_version' => '1.0.0',
|
||||
'modules_author' => 'SysCMS Team',
|
||||
'modules_ative' => true,
|
||||
'modules_public' => false,
|
||||
'modules_blocked' => false,
|
||||
'modules_dependencies' => [],
|
||||
'modules_permissions' => ['configindex.view', 'configindex.edit'],
|
||||
'modules_settings' => [
|
||||
'menu' => [
|
||||
'dashboard' => [
|
||||
'icon' => 'bi bi-file-earmark-text',
|
||||
'label' => 'Páginas Index',
|
||||
'route' => 'admin.configindex.index',
|
||||
'order' => 20
|
||||
]
|
||||
]
|
||||
],
|
||||
'modules_path' => 'modules/configindex',
|
||||
'modules_namespace' => 'Modules\\Configindex',
|
||||
'modules_icon' => 'bi bi-file-earmark-text',
|
||||
'menu_side_bar' => true,
|
||||
'menu_nav_bar' => false,
|
||||
'modules_order' => 20,
|
||||
'modules_install_date' => date('Y-m-d H:i:s'),
|
||||
'modules_update_date' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
// Check if module already exists
|
||||
$existingModule = Module::where('modules_name', 'configindex')->first();
|
||||
|
||||
if (!$existingModule) {
|
||||
$module = Module::create($moduleData);
|
||||
|
||||
// Associate with all existing companies (bus_modules)
|
||||
$companies = Company::all();
|
||||
foreach ($companies as $company) {
|
||||
// Check if already associated
|
||||
$exists = Capsule::table('bus_modules')
|
||||
->where('bus_modules_bus_id', $company->bus_id)
|
||||
->where('bus_modules_module_id', $module->modules_id)
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
Capsule::table('bus_modules')->insert([
|
||||
'bus_modules_bus_id' => $company->bus_id,
|
||||
'bus_modules_module_id' => $module->modules_id,
|
||||
'bus_modules_is_enabled' => true,
|
||||
'bus_modules_settings' => json_encode([]),
|
||||
'bus_modules_enabled_by' => 1, // System or Admin
|
||||
'bus_modules_enabled_at' => date('Y-m-d H:i:s'),
|
||||
'bus_modules_show_public_nav' => false,
|
||||
'bus_modules_show_dashboard_nav' => true,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$module = Module::where('modules_name', 'configindex')->first();
|
||||
if ($module) {
|
||||
// Remove associations
|
||||
Capsule::table('bus_modules')->where('bus_modules_module_id', $module->modules_id)->delete();
|
||||
// Delete module
|
||||
$module->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class AddNavVisibilityToConfigPages
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
if (Capsule::schema()->hasTable('config_index')) {
|
||||
if (!Capsule::schema()->hasColumn('config_index', 'config_index_show_public_nav')) {
|
||||
Capsule::schema()->table('config_index', function (Blueprint $table) {
|
||||
$table->boolean('config_index_show_public_nav')->default(true)->after('config_index_meta_description');
|
||||
});
|
||||
}
|
||||
if (!Capsule::schema()->hasColumn('config_index', 'config_index_show_dashboard_nav')) {
|
||||
Capsule::schema()->table('config_index', function (Blueprint $table) {
|
||||
$table->boolean('config_index_show_dashboard_nav')->default(true)->after('config_index_show_public_nav');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if (Capsule::schema()->hasTable('config_index')) {
|
||||
Capsule::schema()->table('config_index', function (Blueprint $table) {
|
||||
if (Capsule::schema()->hasColumn('config_index', 'config_index_show_public_nav')) {
|
||||
$table->dropColumn('config_index_show_public_nav');
|
||||
}
|
||||
if (Capsule::schema()->hasColumn('config_index', 'config_index_show_dashboard_nav')) {
|
||||
$table->dropColumn('config_index_show_dashboard_nav');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Seitenkonfiguration"
|
||||
},
|
||||
"title": "Seitenkonfiguration",
|
||||
"list_title": "Konfigurierbare Seiten",
|
||||
"page_name": "Seitenname",
|
||||
"actions": "Aktionen",
|
||||
"edit": "Bearbeiten",
|
||||
"edit_title": "Seite bearbeiten: :page",
|
||||
"form_title": "Titel",
|
||||
"form_content": "Inhalt",
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"show_public_nav": "In öffentlicher Nav-Bar anzeigen",
|
||||
"show_dashboard_nav": "In Dashboard Nav-Bar anzeigen",
|
||||
"template_translations": "Template-Übersetzungen",
|
||||
"index_json_section": "index.json (Sektion"
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Page Configuration"
|
||||
},
|
||||
"title": "Page Configuration",
|
||||
"list_title": "Configurable Pages",
|
||||
"page_name": "Page Name",
|
||||
"actions": "Actions",
|
||||
"edit": "Edit",
|
||||
"edit_title": "Edit Page: :page",
|
||||
"form_title": "Title",
|
||||
"form_content": "Content",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"show_public_nav": "Show in Public Nav-Bar",
|
||||
"show_dashboard_nav": "Show in Dashboard Nav-Bar",
|
||||
"template_translations": "Template Translations",
|
||||
"index_json_section": "index.json (section"
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Configuración de Páginas"
|
||||
},
|
||||
"title": "Configuración de Páginas",
|
||||
"list_title": "Páginas Configurables",
|
||||
"page_name": "Nombre de la Página",
|
||||
"actions": "Acciones",
|
||||
"edit": "Editar",
|
||||
"edit_title": "Editar Página: :page",
|
||||
"form_title": "Título",
|
||||
"form_content": "Contenido",
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
"show_public_nav": "Mostrar en Nav-Bar Público",
|
||||
"show_dashboard_nav": "Mostrar en Nav-Bar del Dashboard",
|
||||
"template_translations": "Traducciones de Plantilla",
|
||||
"index_json_section": "index.json (sección"
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Configuration des Pages"
|
||||
},
|
||||
"title": "Configuration de la Page",
|
||||
"list_title": "Pages Configurables",
|
||||
"page_name": "Nom de la Page",
|
||||
"actions": "Actions",
|
||||
"edit": "Éditer",
|
||||
"edit_title": "Éditer la Page: :page",
|
||||
"form_title": "Titre",
|
||||
"form_content": "Contenu",
|
||||
"save": "Enregistrer",
|
||||
"cancel": "Annuler",
|
||||
"show_public_nav": "Afficher dans la barre de navigation publique",
|
||||
"show_dashboard_nav": "Afficher dans la barre de navigation du tableau de bord",
|
||||
"template_translations": "Traductions de modèles",
|
||||
"index_json_section": "index.json (section"
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Páginas Index"
|
||||
},
|
||||
"title": "Configuração de Páginas Index",
|
||||
"list_title": "Páginas Configuráveis",
|
||||
"page_name": "Nome da Página",
|
||||
"actions": "Ações",
|
||||
"edit": "Editar",
|
||||
"edit_title": "Editar Página: :page",
|
||||
"form_title": "Título",
|
||||
"form_content": "Conteúdo",
|
||||
"save": "Salvar",
|
||||
"cancel": "Cancelar",
|
||||
"show_public_nav": "Mostrar no Nav-Bar Público",
|
||||
"show_dashboard_nav": "Mostrar no Nav-Bar do Dashboard",
|
||||
"template_translations": "Traduções do Template",
|
||||
"index_json_section": "index.json (seção"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user