Commit 520392ae authored by Mahmoud Aglan's avatar Mahmoud Aglan

THE CODE UPGRADE

parent 91ab2d74
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.php]
indent_style = space
indent_size = 4
[*.{js,css}]
indent_style = space
indent_size = 2
[Makefile]
indent_style = tab
/vendor/
/node_modules/
/composer
.php-cs-fixer.cache
.phpunit.result.cache
/storage/logs/*.log
/storage/cache/*
.DS_Store
<?php
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'no_unused_imports' => true,
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'single_quote' => true,
'trailing_comma_in_multiline' => true,
])
->setFinder(
PhpCsFixer\Finder::create()
->in(__DIR__ . '/app')
->name('*.php')
->notPath('Modules/*/Views/')
);
...@@ -12,8 +12,12 @@ RUN apt-get update && apt-get install -y \ ...@@ -12,8 +12,12 @@ RUN apt-get update && apt-get install -y \
unzip \ unzip \
default-mysql-client \ default-mysql-client \
dos2unix \ dos2unix \
git \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# ── Install Composer ──
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# ── PHP extensions ── # ── PHP extensions ──
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \ && docker-php-ext-install -j$(nproc) \
...@@ -38,6 +42,10 @@ COPY docker/000-default.conf /etc/apache2/sites-available/000-default.conf ...@@ -38,6 +42,10 @@ COPY docker/000-default.conf /etc/apache2/sites-available/000-default.conf
# ── Copy application ── # ── Copy application ──
COPY . /var/www/html/ COPY . /var/www/html/
# ── Install PHP dependencies (production only, no dev) ──
WORKDIR /var/www/html
RUN composer install --no-dev --optimize-autoloader --no-interaction --no-scripts
# ── Create storage directories ── # ── Create storage directories ──
RUN mkdir -p \ RUN mkdir -p \
/var/www/html/storage/logs \ /var/www/html/storage/logs \
......
.PHONY: test analyse fix fix-dry migrate seed
test:
vendor/bin/phpunit
analyse:
vendor/bin/phpstan analyse
fix:
vendor/bin/php-cs-fixer fix
fix-dry:
vendor/bin/php-cs-fixer fix --dry-run --diff
migrate:
php cli.php migrate
seed:
php cli.php seed
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Core;
final class ApiResponse
{
public static function success($data, array $meta = []): array
{
return ['success' => true, 'data' => $data, 'meta' => $meta, 'errors' => null];
}
public static function error(string $message, int $code = 400, array $errors = []): array
{
return ['success' => false, 'data' => null, 'meta' => [], 'errors' => $errors ?: [$message]];
}
public static function paginated(array $data, array $pagination, array $meta = []): array
{
return [
'success' => true,
'data' => $data,
'meta' => array_merge($meta, ['pagination' => $pagination]),
'errors' => null,
];
}
}
...@@ -14,6 +14,7 @@ final class App ...@@ -14,6 +14,7 @@ final class App
private ?object $currentEmployee = null; private ?object $currentEmployee = null;
private ?array $currentBranch = null; private ?array $currentBranch = null;
private array $bindings = []; private array $bindings = [];
private array $factories = [];
private string $basePath; private string $basePath;
private bool $booted = false; private bool $booted = false;
...@@ -231,7 +232,7 @@ final class App ...@@ -231,7 +232,7 @@ final class App
return $this->router; return $this->router;
} }
public function config(string $key = null, $default = null) public function config(?string $key = null, $default = null)
{ {
if ($key === null) { if ($key === null) {
return $this->config; return $this->config;
...@@ -300,8 +301,21 @@ final class App ...@@ -300,8 +301,21 @@ final class App
$this->bindings[$key] = $value; $this->bindings[$key] = $value;
} }
public function singleton(string $key, callable $factory): void
{
$this->factories[$key] = $factory;
unset($this->bindings[$key]);
}
public function resolve(string $key, $default = null) public function resolve(string $key, $default = null)
{ {
return $this->bindings[$key] ?? $default; if (isset($this->bindings[$key])) {
return $this->bindings[$key];
}
if (isset($this->factories[$key])) {
$this->bindings[$key] = ($this->factories[$key])($this);
return $this->bindings[$key];
}
return $default;
} }
} }
\ No newline at end of file
...@@ -5,45 +5,6 @@ namespace App\Core; ...@@ -5,45 +5,6 @@ namespace App\Core;
final class Autoloader final class Autoloader
{ {
private static bool $registered = false;
private static array $prefixes = [
'App\\Core\\' => 'app/Core/',
'App\\Modules\\' => 'app/Modules/',
'App\\Middleware\\' => 'app/Middleware/',
'App\\Shared\\' => 'app/Shared/',
];
public static function register(): void
{
if (self::$registered) {
return;
}
spl_autoload_register([self::class, 'load']);
self::$registered = true;
$helpersPath = self::basePath() . '/app/Core/Helpers.php';
if (file_exists($helpersPath)) {
require_once $helpersPath;
}
}
public static function load(string $class): void
{
foreach (self::$prefixes as $prefix => $dir) {
if (strpos($class, $prefix) === 0) {
$relativeClass = substr($class, strlen($prefix));
$file = self::basePath() . '/' . $dir . str_replace('\\', '/', $relativeClass) . '.php';
if (file_exists($file)) {
require_once $file;
return;
}
}
}
}
public static function basePath(): string public static function basePath(): string
{ {
return dirname(__DIR__, 2); return dirname(__DIR__, 2);
......
<?php
declare(strict_types=1);
namespace App\Core;
final class Cache
{
private static ?string $dir = null;
private static function dir(): string
{
if (self::$dir === null) {
self::$dir = Autoloader::basePath() . '/storage/cache';
if (!is_dir(self::$dir)) {
@mkdir(self::$dir, 0775, true);
}
}
return self::$dir;
}
public static function remember(string $key, int $ttl, callable $factory): mixed
{
$file = self::dir() . '/' . md5($key) . '.cache';
if (file_exists($file) && (time() - filemtime($file)) < $ttl) {
return unserialize(file_get_contents($file));
}
$value = $factory();
file_put_contents($file, serialize($value), LOCK_EX);
return $value;
}
public static function forget(string $key): void
{
@unlink(self::dir() . '/' . md5($key) . '.cache');
}
public static function flush(): void
{
array_map('unlink', glob(self::dir() . '/*.cache') ?: []);
}
}
...@@ -58,6 +58,25 @@ abstract class Controller ...@@ -58,6 +58,25 @@ abstract class Controller
return $data; return $data;
} }
protected function validated(array $data, array $rules): array
{
$validator = new Validator();
$result = $validator->validate($data, $rules);
if ($result->fails()) {
$session = App::getInstance()->session();
$session->flash('_old_input', $data);
$alerts = [];
foreach ($result->errors() as $field => $fieldErrors) {
foreach ($fieldErrors as $error) {
$alerts[] = ['type' => 'error', 'message' => $error];
}
}
$session->flash('_alerts', $alerts);
throw new Exceptions\ValidationException($result->errors());
}
return $result->validated();
}
protected function authorize(string $permission): void protected function authorize(string $permission): void
{ {
$employee = App::getInstance()->currentEmployee(); $employee = App::getInstance()->currentEmployee();
......
...@@ -11,6 +11,7 @@ class Database ...@@ -11,6 +11,7 @@ class Database
private array $afterUpdateHooks = []; private array $afterUpdateHooks = [];
private array $afterDeleteHooks = []; private array $afterDeleteHooks = [];
private array $tableCache = []; private array $tableCache = [];
private array $tableQueryCounts = [];
public function __construct(string $host = '127.0.0.1', int $port = 3306, string $dbName = '', string $user = 'root', string $pass = '', string $charset = 'utf8mb4') public function __construct(string $host = '127.0.0.1', int $port = 3306, string $dbName = '', string $user = 'root', string $pass = '', string $charset = 'utf8mb4')
{ {
...@@ -35,8 +36,25 @@ class Database ...@@ -35,8 +36,25 @@ class Database
public function query(string $sql, array $params = []): \PDOStatement public function query(string $sql, array $params = []): \PDOStatement
{ {
$start = microtime(true);
$stmt = $this->pdo->prepare($sql); $stmt = $this->pdo->prepare($sql);
$stmt->execute($params); $stmt->execute($params);
$elapsed = microtime(true) - $start;
DebugBar::recordQuery($elapsed);
if ($elapsed > 0.1) {
Logger::warning("Slow query ({$elapsed}s)", ['sql' => substr($sql, 0, 500)]);
}
if (preg_match('/(?:FROM|INTO|UPDATE)\s+`?(\w+)`?/i', $sql, $m)) {
$table = $m[1];
$this->tableQueryCounts[$table] = ($this->tableQueryCounts[$table] ?? 0) + 1;
if ($this->tableQueryCounts[$table] === 11) {
Logger::warning("N+1 detected: table '{$table}' queried 11+ times in single request");
}
}
return $stmt; return $stmt;
} }
......
<?php
declare(strict_types=1);
namespace App\Core;
final class DebugBar
{
private static float $startTime;
private static int $queryCount = 0;
private static float $queryTime = 0.0;
public static function init(): void
{
self::$startTime = $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true);
}
public static function recordQuery(float $elapsed): void
{
self::$queryCount++;
self::$queryTime += $elapsed;
}
public static function render(): string
{
$totalTime = round((microtime(true) - self::$startTime) * 1000);
$memory = round(memory_get_peak_usage(true) / 1024 / 1024, 1);
$queryTime = round(self::$queryTime * 1000);
$app = App::getInstance();
$route = '-';
$permission = '-';
$employee = '-';
try {
$emp = $app->currentEmployee();
if ($emp) {
$employee = $emp->full_name_ar ?? ($emp->id ?? '-');
}
} catch (\Throwable $e) {
}
return <<<HTML
<div id="debug-bar" style="position:fixed;bottom:0;left:0;right:0;background:#1a1a2e;color:#e0e0e0;font-family:monospace;font-size:11px;padding:6px 15px;z-index:99999;direction:ltr;text-align:left;display:flex;gap:20px;align-items:center;border-top:2px solid #0D7377;">
<span style="color:#4ecdc4;font-weight:bold;">DEBUG</span>
<span>Time: <b>{$totalTime}ms</b></span>
<span>Queries: <b>{self::$queryCount}</b> ({$queryTime}ms)</span>
<span>Memory: <b>{$memory}MB</b></span>
<span>Employee: <b>{$employee}</b></span>
</div>
HTML;
}
}
...@@ -6,6 +6,8 @@ namespace App\Core; ...@@ -6,6 +6,8 @@ namespace App\Core;
final class EventBus final class EventBus
{ {
private static array $listeners = []; private static array $listeners = [];
private static bool $testMode = false;
private static array $dispatched = [];
public static function listen(string $event, callable $handler, int $priority = 0): void public static function listen(string $event, callable $handler, int $priority = 0): void
{ {
...@@ -17,6 +19,11 @@ final class EventBus ...@@ -17,6 +19,11 @@ final class EventBus
public static function dispatch(string $event, array $data = []): array public static function dispatch(string $event, array $data = []): array
{ {
if (self::$testMode) {
self::$dispatched[] = ['event' => $event, 'data' => $data];
return [];
}
if (!isset(self::$listeners[$event])) { if (!isset(self::$listeners[$event])) {
return []; return [];
} }
...@@ -50,6 +57,23 @@ final class EventBus ...@@ -50,6 +57,23 @@ final class EventBus
unset(self::$listeners[$event]); unset(self::$listeners[$event]);
} }
public static function enableTestMode(): void
{
self::$testMode = true;
self::$dispatched = [];
}
public static function disableTestMode(): void
{
self::$testMode = false;
self::$dispatched = [];
}
public static function getDispatched(): array
{
return self::$dispatched;
}
public static function dispatchAsync(string $event, array $data = []): void public static function dispatchAsync(string $event, array $data = []): void
{ {
try { try {
......
...@@ -13,6 +13,23 @@ final class ExceptionHandler ...@@ -13,6 +13,23 @@ final class ExceptionHandler
public static function handleException(\Throwable $e): void public static function handleException(\Throwable $e): void
{ {
if ($e instanceof Exceptions\ValidationException) {
$referer = $_SERVER['HTTP_REFERER'] ?? '/';
header('Location: ' . $referer);
exit;
}
if ($e instanceof Exceptions\BusinessLogicException) {
try {
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'error', 'message' => $e->getMessage()]]);
} catch (\Throwable $t) {
}
$referer = $_SERVER['HTTP_REFERER'] ?? '/';
header('Location: ' . $referer);
exit;
}
Logger::error($e->getMessage(), [ Logger::error($e->getMessage(), [
'file' => $e->getFile(), 'file' => $e->getFile(),
'line' => $e->getLine(), 'line' => $e->getLine(),
......
<?php
declare(strict_types=1);
namespace App\Core\Exceptions;
class BusinessLogicException extends \RuntimeException
{
public function __construct(string $message = 'Business logic error')
{
parent::__construct($message, 409);
}
}
<?php
declare(strict_types=1);
namespace App\Core\Exceptions;
class ForbiddenException extends \RuntimeException
{
public function __construct(string $message = 'Forbidden')
{
parent::__construct($message, 403);
}
}
<?php
declare(strict_types=1);
namespace App\Core\Exceptions;
class NotFoundException extends \RuntimeException
{
public function __construct(string $message = 'Not found')
{
parent::__construct($message, 404);
}
}
<?php
declare(strict_types=1);
namespace App\Core\Exceptions;
class UnauthorizedException extends \RuntimeException
{
public function __construct(string $message = 'Unauthorized')
{
parent::__construct($message, 401);
}
}
<?php
declare(strict_types=1);
namespace App\Core\Exceptions;
class ValidationException extends \RuntimeException
{
private array $errors;
public function __construct(array $errors)
{
$this->errors = $errors;
parent::__construct('Validation failed', 422);
}
public function errors(): array
{
return $this->errors;
}
}
...@@ -36,7 +36,7 @@ if (!function_exists('db')) { ...@@ -36,7 +36,7 @@ if (!function_exists('db')) {
} }
if (!function_exists('config')) { if (!function_exists('config')) {
function config(string $key = null, $default = null) function config(?string $key = null, $default = null)
{ {
$app = \App\Core\App::getInstance(); $app = \App\Core\App::getInstance();
if ($key === null) { if ($key === null) {
...@@ -47,7 +47,7 @@ if (!function_exists('config')) { ...@@ -47,7 +47,7 @@ if (!function_exists('config')) {
} }
if (!function_exists('session')) { if (!function_exists('session')) {
function session(string $key = null, $default = null) function session(?string $key = null, $default = null)
{ {
$sess = \App\Core\App::getInstance()->session(); $sess = \App\Core\App::getInstance()->session();
if ($key === null) { if ($key === null) {
......
...@@ -27,6 +27,12 @@ final class Logger ...@@ -27,6 +27,12 @@ final class Logger
private static function write(string $level, string $message, array $context): void private static function write(string $level, string $message, array $context): void
{ {
$levels = ['DEBUG' => 0, 'INFO' => 1, 'WARNING' => 2, 'ERROR' => 3];
$minLevel = $_ENV['LOG_LEVEL'] ?? 'DEBUG';
if (($levels[$level] ?? 0) < ($levels[$minLevel] ?? 0)) {
return;
}
$logDir = Autoloader::basePath() . '/storage/logs'; $logDir = Autoloader::basePath() . '/storage/logs';
if (!is_dir($logDir)) { if (!is_dir($logDir)) {
@mkdir($logDir, 0775, true); @mkdir($logDir, 0775, true);
...@@ -44,6 +50,10 @@ final class Logger ...@@ -44,6 +50,10 @@ final class Logger
// ignore // ignore
} }
$context['_url'] = $_SERVER['REQUEST_URI'] ?? 'cli';
$context['_method'] = $_SERVER['REQUEST_METHOD'] ?? 'CLI';
$context['_ip'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
$contextStr = !empty($context) ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : ''; $contextStr = !empty($context) ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
$line = sprintf( $line = sprintf(
"[%s] [%s] [employee_id:%d] %s%s\n", "[%s] [%s] [employee_id:%d] %s%s\n",
......
...@@ -200,7 +200,7 @@ final class QueryBuilder ...@@ -200,7 +200,7 @@ final class QueryBuilder
return $this; return $this;
} }
private function buildSql(string $selectOverride = null): array private function buildSql(?string $selectOverride = null): array
{ {
$select = $selectOverride ?? implode(', ', $this->selects); $select = $selectOverride ?? implode(', ', $this->selects);
$sql = "SELECT {$select} FROM `{$this->table}`"; $sql = "SELECT {$select} FROM `{$this->table}`";
......
<?php
declare(strict_types=1);
namespace App\Core;
abstract class Repository
{
protected Database $db;
protected static string $table = '';
public function __construct(?Database $db = null)
{
$this->db = $db ?? App::getInstance()->db();
}
public function find(int $id): ?array
{
return $this->db->selectOne(
"SELECT * FROM `" . static::$table . "` WHERE id = ? AND is_archived = 0",
[$id]
);
}
public function findOrFail(int $id): array
{
$row = $this->find($id);
if ($row === null) {
throw new \RuntimeException('Record not found', 404);
}
return $row;
}
protected function paginate(string $countSql, string $dataSql, array $params, int $page, int $perPage): array
{
$total = (int) ($this->db->selectOne($countSql, $params)['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$data = $this->db->select($dataSql . " LIMIT {$perPage} OFFSET {$offset}", $params);
return [
'data' => $data,
'pagination' => Pagination::paginate($total, $perPage, $page),
];
}
}
...@@ -105,6 +105,7 @@ final class Router ...@@ -105,6 +105,7 @@ final class Router
$middlewareMap = [ $middlewareMap = [
'csrf' => \App\Middleware\CSRFMiddleware::class, 'csrf' => \App\Middleware\CSRFMiddleware::class,
'auth' => \App\Middleware\AuthMiddleware::class, 'auth' => \App\Middleware\AuthMiddleware::class,
'api_auth' => \App\Middleware\ApiAuthMiddleware::class,
'permission' => \App\Middleware\PermissionMiddleware::class, 'permission' => \App\Middleware\PermissionMiddleware::class,
'audit' => \App\Middleware\AuditMiddleware::class, 'audit' => \App\Middleware\AuditMiddleware::class,
'rate_limit' => \App\Middleware\RateLimitMiddleware::class, 'rate_limit' => \App\Middleware\RateLimitMiddleware::class,
......
<?php
declare(strict_types=1);
namespace App\Middleware;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Users\Models\Employee;
final class ApiAuthMiddleware
{
public function handle(Request $request, callable $next): Response
{
$token = $request->bearerToken();
if (!$token) {
return (new Response())->json([
'success' => false,
'data' => null,
'meta' => [],
'errors' => ['Authentication required'],
], 401);
}
$db = App::getInstance()->db();
$tokenRow = $db->selectOne(
"SELECT employee_id, expires_at FROM api_tokens WHERE token = ? AND is_revoked = 0",
[$token]
);
if (!$tokenRow) {
return (new Response())->json([
'success' => false,
'data' => null,
'meta' => [],
'errors' => ['Invalid token'],
], 401);
}
if ($tokenRow['expires_at'] && $tokenRow['expires_at'] < date('Y-m-d H:i:s')) {
return (new Response())->json([
'success' => false,
'data' => null,
'meta' => [],
'errors' => ['Token expired'],
], 401);
}
$employee = Employee::find((int) $tokenRow['employee_id']);
if (!$employee || !$employee->is_active) {
return (new Response())->json([
'success' => false,
'data' => null,
'meta' => [],
'errors' => ['Account inactive'],
], 401);
}
App::getInstance()->setCurrentEmployee($employee);
$db->update('api_tokens', [
'last_used_at' => date('Y-m-d H:i:s'),
], '`token` = ?', [$token]);
return $next($request);
}
}
...@@ -51,17 +51,17 @@ class BankAccountController extends Controller ...@@ -51,17 +51,17 @@ class BankAccountController extends Controller
]); ]);
} }
public function store(): Response public function store(Request $request): Response
{ {
$this->authorize('accounting.bank_account.manage'); $this->authorize('accounting.bank_account.manage');
$data = $this->validate($_POST, [ $data = $this->validate($request->all(), [
'account_name_ar' => 'required', 'account_name_ar' => 'required',
'bank_name_ar' => 'required', 'bank_name_ar' => 'required',
'account_number' => 'required', 'account_number' => 'required',
]); ]);
$isDefault = (int) ($_POST['is_default'] ?? 0); $isDefault = (int) ($request->post('is_default', 0));
if ($isDefault) { if ($isDefault) {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$db->execute("UPDATE bank_accounts SET is_default = 0 WHERE is_default = 1"); $db->execute("UPDATE bank_accounts SET is_default = 0 WHERE is_default = 1");
...@@ -69,29 +69,29 @@ class BankAccountController extends Controller ...@@ -69,29 +69,29 @@ class BankAccountController extends Controller
$ba = BankAccount::create([ $ba = BankAccount::create([
'account_name_ar' => $data['account_name_ar'], 'account_name_ar' => $data['account_name_ar'],
'account_name_en' => $_POST['account_name_en'] ?? '', 'account_name_en' => $request->post('account_name_en', ''),
'bank_name_ar' => $data['bank_name_ar'], 'bank_name_ar' => $data['bank_name_ar'],
'bank_name_en' => $_POST['bank_name_en'] ?? null, 'bank_name_en' => $request->post('bank_name_en'),
'account_number' => $data['account_number'], 'account_number' => $data['account_number'],
'iban' => $_POST['iban'] ?? null, 'iban' => $request->post('iban'),
'swift_code' => $_POST['swift_code'] ?? null, 'swift_code' => $request->post('swift_code'),
'branch_name' => $_POST['branch_name'] ?? null, 'branch_name' => $request->post('branch_name'),
'currency' => $_POST['currency'] ?? 'EGP', 'currency' => $request->post('currency', 'EGP'),
'gl_account_id' => !empty($_POST['gl_account_id']) ? (int) $_POST['gl_account_id'] : null, 'gl_account_id' => $request->post('gl_account_id') ? (int) $request->post('gl_account_id') : null,
'opening_balance' => $_POST['opening_balance'] ?? '0.00', 'opening_balance' => $request->post('opening_balance', '0.00'),
'current_balance' => $_POST['opening_balance'] ?? '0.00', 'current_balance' => $request->post('opening_balance', '0.00'),
'is_default' => $isDefault, 'is_default' => $isDefault,
'is_active' => 1, 'is_active' => 1,
'notes' => $_POST['notes'] ?? null, 'notes' => $request->post('notes'),
]); ]);
// Link GL account // Link GL account
if (!empty($_POST['gl_account_id'])) { if ($request->post('gl_account_id')) {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$db->update('chart_of_accounts', [ $db->update('chart_of_accounts', [
'is_bank_account' => 1, 'is_bank_account' => 1,
'bank_account_id' => $ba->id, 'bank_account_id' => $ba->id,
], '`id` = ?', [(int) $_POST['gl_account_id']]); ], '`id` = ?', [(int) $request->post('gl_account_id')]);
} }
$session = App::getInstance()->session(); $session = App::getInstance()->session();
...@@ -124,21 +124,21 @@ class BankAccountController extends Controller ...@@ -124,21 +124,21 @@ class BankAccountController extends Controller
$account = BankAccount::findOrFail((int) $id); $account = BankAccount::findOrFail((int) $id);
$data = $this->validate($_POST, [ $data = $this->validate($request->all(), [
'account_name_ar' => 'required', 'account_name_ar' => 'required',
'bank_name_ar' => 'required', 'bank_name_ar' => 'required',
]); ]);
$account->update([ $account->update([
'account_name_ar' => $data['account_name_ar'], 'account_name_ar' => $data['account_name_ar'],
'account_name_en' => $_POST['account_name_en'] ?? '', 'account_name_en' => $request->post('account_name_en', ''),
'bank_name_ar' => $data['bank_name_ar'], 'bank_name_ar' => $data['bank_name_ar'],
'bank_name_en' => $_POST['bank_name_en'] ?? null, 'bank_name_en' => $request->post('bank_name_en'),
'iban' => $_POST['iban'] ?? null, 'iban' => $request->post('iban'),
'swift_code' => $_POST['swift_code'] ?? null, 'swift_code' => $request->post('swift_code'),
'branch_name' => $_POST['branch_name'] ?? null, 'branch_name' => $request->post('branch_name'),
'is_active' => (int) ($_POST['is_active'] ?? 1), 'is_active' => (int) ($request->post('is_active', 1)),
'notes' => $_POST['notes'] ?? null, 'notes' => $request->post('notes'),
]); ]);
$session = App::getInstance()->session(); $session = App::getInstance()->session();
......
...@@ -13,13 +13,13 @@ use App\Modules\Accounting\Services\BankReconciliationService; ...@@ -13,13 +13,13 @@ use App\Modules\Accounting\Services\BankReconciliationService;
class BankReconciliationController extends Controller class BankReconciliationController extends Controller
{ {
public function index(): Response public function index(Request $request): Response
{ {
$this->authorize('accounting.bank_recon.view'); $this->authorize('accounting.bank_recon.view');
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$statusFilter = $_GET['status'] ?? ''; $statusFilter = $request->get('status', '');
$bankAccountFilter = !empty($_GET['bank_account_id']) ? (int) $_GET['bank_account_id'] : 0; $bankAccountFilter = $request->get('bank_account_id') ? (int) $request->get('bank_account_id') : 0;
$where = 'br.is_archived = 0'; $where = 'br.is_archived = 0';
$params = []; $params = [];
...@@ -63,17 +63,17 @@ class BankReconciliationController extends Controller ...@@ -63,17 +63,17 @@ class BankReconciliationController extends Controller
]); ]);
} }
public function store(): Response public function store(Request $request): Response
{ {
$this->authorize('accounting.bank_recon.manage'); $this->authorize('accounting.bank_recon.manage');
$this->validate($_POST, [ $this->validate($request->all(), [
'bank_account_id' => 'required|numeric', 'bank_account_id' => 'required|numeric',
'statement_date' => 'required', 'statement_date' => 'required',
'statement_balance' => 'required|numeric', 'statement_balance' => 'required|numeric',
]); ]);
$result = BankReconciliationService::create($_POST); $result = BankReconciliationService::create($request->all());
$session = App::getInstance()->session(); $session = App::getInstance()->session();
if ($result['success']) { if ($result['success']) {
...@@ -110,13 +110,13 @@ class BankReconciliationController extends Controller ...@@ -110,13 +110,13 @@ class BankReconciliationController extends Controller
{ {
$this->authorize('accounting.bank_recon.manage'); $this->authorize('accounting.bank_recon.manage');
$this->validate($_POST, [ $this->validate($request->all(), [
'item_type' => 'required', 'item_type' => 'required',
'description_ar' => 'required', 'description_ar' => 'required',
'amount' => 'required|numeric', 'amount' => 'required|numeric',
]); ]);
$result = BankReconciliationService::addItem((int) $id, $_POST); $result = BankReconciliationService::addItem((int) $id, $request->all());
$session = App::getInstance()->session(); $session = App::getInstance()->session();
if ($result['success']) { if ($result['success']) {
......
...@@ -12,12 +12,12 @@ use App\Modules\Accounting\Models\CostCenter; ...@@ -12,12 +12,12 @@ use App\Modules\Accounting\Models\CostCenter;
class ChartOfAccountsController extends Controller class ChartOfAccountsController extends Controller
{ {
public function index(): Response public function index(Request $request): Response
{ {
$this->authorize('accounting.coa.view'); $this->authorize('accounting.coa.view');
$search = trim($_GET['search'] ?? ''); $search = trim($request->get('search', ''));
$typeFilter = $_GET['type'] ?? ''; $typeFilter = $request->get('type', '');
$tree = Account::getTree(); $tree = Account::getTree();
...@@ -46,11 +46,11 @@ class ChartOfAccountsController extends Controller ...@@ -46,11 +46,11 @@ class ChartOfAccountsController extends Controller
]); ]);
} }
public function store(): Response public function store(Request $request): Response
{ {
$this->authorize('accounting.coa.manage'); $this->authorize('accounting.coa.manage');
$data = $this->validate($_POST, [ $data = $this->validate($request->all(), [
'account_code' => 'required', 'account_code' => 'required',
'name_ar' => 'required', 'name_ar' => 'required',
'name_en' => 'required', 'name_en' => 'required',
...@@ -65,7 +65,7 @@ class ChartOfAccountsController extends Controller ...@@ -65,7 +65,7 @@ class ChartOfAccountsController extends Controller
return $this->redirect('/accounting/chart-of-accounts/create'); return $this->redirect('/accounting/chart-of-accounts/create');
} }
$parentId = !empty($_POST['parent_id']) ? (int) $_POST['parent_id'] : null; $parentId = $request->post('parent_id') ? (int) $request->post('parent_id') : null;
$level = 1; $level = 1;
if ($parentId) { if ($parentId) {
$parent = Account::find($parentId); $parent = Account::find($parentId);
...@@ -82,12 +82,12 @@ class ChartOfAccountsController extends Controller ...@@ -82,12 +82,12 @@ class ChartOfAccountsController extends Controller
'account_nature' => $data['account_nature'], 'account_nature' => $data['account_nature'],
'parent_id' => $parentId, 'parent_id' => $parentId,
'level' => $level, 'level' => $level,
'is_header' => (int) ($_POST['is_header'] ?? 0), 'is_header' => (int) ($request->post('is_header', 0)),
'is_active' => 1, 'is_active' => 1,
'description_ar' => $_POST['description_ar'] ?? null, 'description_ar' => $request->post('description_ar'),
'description_en' => $_POST['description_en'] ?? null, 'description_en' => $request->post('description_en'),
'opening_balance' => $_POST['opening_balance'] ?? '0.00', 'opening_balance' => $request->post('opening_balance', '0.00'),
'cost_center_id' => !empty($_POST['cost_center_id']) ? (int) $_POST['cost_center_id'] : null, 'cost_center_id' => $request->post('cost_center_id') ? (int) $request->post('cost_center_id') : null,
]); ]);
$session = App::getInstance()->session(); $session = App::getInstance()->session();
...@@ -123,7 +123,7 @@ class ChartOfAccountsController extends Controller ...@@ -123,7 +123,7 @@ class ChartOfAccountsController extends Controller
$account = Account::findOrFail((int) $id); $account = Account::findOrFail((int) $id);
$data = $this->validate($_POST, [ $data = $this->validate($request->all(), [
'name_ar' => 'required', 'name_ar' => 'required',
'name_en' => 'required', 'name_en' => 'required',
'account_type' => 'required|in:asset,liability,equity,revenue,expense', 'account_type' => 'required|in:asset,liability,equity,revenue,expense',
...@@ -131,7 +131,7 @@ class ChartOfAccountsController extends Controller ...@@ -131,7 +131,7 @@ class ChartOfAccountsController extends Controller
]); ]);
// Cannot change code of system accounts // Cannot change code of system accounts
if ((int) $account->is_system === 1 && ($_POST['account_code'] ?? '') !== $account->account_code) { if ((int) $account->is_system === 1 && ($request->post('account_code', '') !== $account->account_code)) {
$session = App::getInstance()->session(); $session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'error', 'message' => 'لا يمكن تعديل كود حساب نظامي']]); $session->flash('_alerts', [['type' => 'error', 'message' => 'لا يمكن تعديل كود حساب نظامي']]);
return $this->redirect('/accounting/chart-of-accounts/' . (int) $id . '/edit'); return $this->redirect('/accounting/chart-of-accounts/' . (int) $id . '/edit');
...@@ -142,11 +142,11 @@ class ChartOfAccountsController extends Controller ...@@ -142,11 +142,11 @@ class ChartOfAccountsController extends Controller
'name_en' => $data['name_en'], 'name_en' => $data['name_en'],
'account_type' => $data['account_type'], 'account_type' => $data['account_type'],
'account_nature' => $data['account_nature'], 'account_nature' => $data['account_nature'],
'is_header' => (int) ($_POST['is_header'] ?? 0), 'is_header' => (int) ($request->post('is_header', 0)),
'is_active' => (int) ($_POST['is_active'] ?? 1), 'is_active' => (int) ($request->post('is_active', 1)),
'description_ar' => $_POST['description_ar'] ?? null, 'description_ar' => $request->post('description_ar'),
'description_en' => $_POST['description_en'] ?? null, 'description_en' => $request->post('description_en'),
'cost_center_id' => !empty($_POST['cost_center_id']) ? (int) $_POST['cost_center_id'] : null, 'cost_center_id' => $request->post('cost_center_id') ? (int) $request->post('cost_center_id') : null,
]); ]);
$session = App::getInstance()->session(); $session = App::getInstance()->session();
...@@ -154,11 +154,11 @@ class ChartOfAccountsController extends Controller ...@@ -154,11 +154,11 @@ class ChartOfAccountsController extends Controller
return $this->redirect('/accounting/chart-of-accounts'); return $this->redirect('/accounting/chart-of-accounts');
} }
public function search(): Response public function search(Request $request): Response
{ {
$this->authorize('accounting.coa.view'); $this->authorize('accounting.coa.view');
$term = $_GET['q'] ?? ''; $term = $request->get('q', '');
$results = Account::search($term); $results = Account::search($term);
return $this->json($results); return $this->json($results);
......
...@@ -48,11 +48,11 @@ class CostCenterController extends Controller ...@@ -48,11 +48,11 @@ class CostCenterController extends Controller
]); ]);
} }
public function store(): Response public function store(Request $request): Response
{ {
$this->authorize('accounting.cost_center.manage'); $this->authorize('accounting.cost_center.manage');
$data = $this->validate($_POST, [ $data = $this->validate($request->all(), [
'code' => 'required', 'code' => 'required',
'name_ar' => 'required', 'name_ar' => 'required',
'name_en' => 'required', 'name_en' => 'required',
...@@ -70,10 +70,10 @@ class CostCenterController extends Controller ...@@ -70,10 +70,10 @@ class CostCenterController extends Controller
'name_ar' => $data['name_ar'], 'name_ar' => $data['name_ar'],
'name_en' => $data['name_en'], 'name_en' => $data['name_en'],
'type' => $data['type'], 'type' => $data['type'],
'branch_id' => !empty($_POST['branch_id']) ? (int) $_POST['branch_id'] : null, 'branch_id' => $request->post('branch_id') ? (int) $request->post('branch_id') : null,
'parent_id' => !empty($_POST['parent_id']) ? (int) $_POST['parent_id'] : null, 'parent_id' => $request->post('parent_id') ? (int) $request->post('parent_id') : null,
'is_active' => 1, 'is_active' => 1,
'description' => $_POST['description'] ?? null, 'description' => $request->post('description'),
]); ]);
$session = App::getInstance()->session(); $session = App::getInstance()->session();
...@@ -109,7 +109,7 @@ class CostCenterController extends Controller ...@@ -109,7 +109,7 @@ class CostCenterController extends Controller
$center = CostCenter::findOrFail((int) $id); $center = CostCenter::findOrFail((int) $id);
$data = $this->validate($_POST, [ $data = $this->validate($request->all(), [
'name_ar' => 'required', 'name_ar' => 'required',
'name_en' => 'required', 'name_en' => 'required',
'type' => 'required|in:cost_center,profit_center', 'type' => 'required|in:cost_center,profit_center',
...@@ -119,10 +119,10 @@ class CostCenterController extends Controller ...@@ -119,10 +119,10 @@ class CostCenterController extends Controller
'name_ar' => $data['name_ar'], 'name_ar' => $data['name_ar'],
'name_en' => $data['name_en'], 'name_en' => $data['name_en'],
'type' => $data['type'], 'type' => $data['type'],
'branch_id' => !empty($_POST['branch_id']) ? (int) $_POST['branch_id'] : null, 'branch_id' => $request->post('branch_id') ? (int) $request->post('branch_id') : null,
'parent_id' => !empty($_POST['parent_id']) ? (int) $_POST['parent_id'] : null, 'parent_id' => $request->post('parent_id') ? (int) $request->post('parent_id') : null,
'is_active' => (int) ($_POST['is_active'] ?? 1), 'is_active' => (int) ($request->post('is_active', 1)),
'description' => $_POST['description'] ?? null, 'description' => $request->post('description'),
]); ]);
$session = App::getInstance()->session(); $session = App::getInstance()->session();
......
...@@ -40,11 +40,11 @@ class FiscalYearController extends Controller ...@@ -40,11 +40,11 @@ class FiscalYearController extends Controller
]); ]);
} }
public function store(): Response public function store(Request $request): Response
{ {
$this->authorize('accounting.fiscal_year.manage'); $this->authorize('accounting.fiscal_year.manage');
$data = $this->validate($_POST, [ $data = $this->validate($request->all(), [
'name_ar' => 'required', 'name_ar' => 'required',
'name_en' => 'required', 'name_en' => 'required',
'start_date' => 'required', 'start_date' => 'required',
...@@ -73,7 +73,7 @@ class FiscalYearController extends Controller ...@@ -73,7 +73,7 @@ class FiscalYearController extends Controller
return $this->redirect('/accounting/fiscal-years/create'); return $this->redirect('/accounting/fiscal-years/create');
} }
$isCurrent = (int) ($_POST['is_current'] ?? 0); $isCurrent = (int) ($request->post('is_current', 0));
if ($isCurrent) { if ($isCurrent) {
$db->execute("UPDATE fiscal_years SET is_current = 0 WHERE is_current = 1"); $db->execute("UPDATE fiscal_years SET is_current = 0 WHERE is_current = 1");
} }
...@@ -85,7 +85,7 @@ class FiscalYearController extends Controller ...@@ -85,7 +85,7 @@ class FiscalYearController extends Controller
'end_date' => $data['end_date'], 'end_date' => $data['end_date'],
'is_current' => $isCurrent, 'is_current' => $isCurrent,
'status' => 'open', 'status' => 'open',
'notes' => $_POST['notes'] ?? null, 'notes' => $request->post('notes'),
]); ]);
$session = App::getInstance()->session(); $session = App::getInstance()->session();
......
...@@ -16,18 +16,18 @@ use App\Modules\Accounting\Services\JournalService; ...@@ -16,18 +16,18 @@ use App\Modules\Accounting\Services\JournalService;
class JournalEntryController extends Controller class JournalEntryController extends Controller
{ {
public function index(): Response public function index(Request $request): Response
{ {
$this->authorize('accounting.journal.view'); $this->authorize('accounting.journal.view');
$page = max(1, (int) ($_GET['page'] ?? 1)); $page = max(1, (int) ($request->get('page', 1)));
$filters = [ $filters = [
'status' => $_GET['status'] ?? null, 'status' => $request->get('status'),
'date_from' => $_GET['date_from'] ?? null, 'date_from' => $request->get('date_from'),
'date_to' => $_GET['date_to'] ?? null, 'date_to' => $request->get('date_to'),
'source_module' => $_GET['source_module'] ?? null, 'source_module' => $request->get('source_module'),
'fiscal_year_id' => $_GET['fiscal_year_id'] ?? null, 'fiscal_year_id' => $request->get('fiscal_year_id'),
'search' => $_GET['search'] ?? null, 'search' => $request->get('search'),
]; ];
$result = JournalEntry::search($filters, $page); $result = JournalEntry::search($filters, $page);
...@@ -66,21 +66,24 @@ class JournalEntryController extends Controller ...@@ -66,21 +66,24 @@ class JournalEntryController extends Controller
]); ]);
} }
public function store(): Response public function store(Request $request): Response
{ {
$this->authorize('accounting.journal.create'); $this->authorize('accounting.journal.create');
$data = $this->validate($_POST, [ $data = $this->validate($request->all(), [
'entry_date' => 'required', 'entry_date' => 'required',
'description_ar' => 'required', 'description_ar' => 'required',
]); ]);
// Parse lines from POST // Parse lines from POST
$lines = []; $lines = [];
$lineAccounts = $_POST['line_account_id'] ?? []; $lineAccounts = $request->post('line_account_id', []);
$lineDebits = $_POST['line_debit'] ?? []; $lineDebits = $request->post('line_debit', []);
$lineCredits = $_POST['line_credit'] ?? []; $lineCredits = $request->post('line_credit', []);
$lineDescriptions = $_POST['line_description'] ?? []; $lineDescriptions = $request->post('line_description', []);
$lineCostCenterIds = $request->post('line_cost_center_id', []);
$lineBranchIds = $request->post('line_branch_id', []);
for ($i = 0; $i < count($lineAccounts); $i++) { for ($i = 0; $i < count($lineAccounts); $i++) {
if (empty($lineAccounts[$i])) { if (empty($lineAccounts[$i])) {
...@@ -91,20 +94,20 @@ class JournalEntryController extends Controller ...@@ -91,20 +94,20 @@ class JournalEntryController extends Controller
'debit' => $lineDebits[$i] ?? '0.00', 'debit' => $lineDebits[$i] ?? '0.00',
'credit' => $lineCredits[$i] ?? '0.00', 'credit' => $lineCredits[$i] ?? '0.00',
'description_ar' => $lineDescriptions[$i] ?? null, 'description_ar' => $lineDescriptions[$i] ?? null,
'cost_center_id' => !empty($_POST['line_cost_center_id'][$i]) ? (int) $_POST['line_cost_center_id'][$i] : null, 'cost_center_id' => !empty($lineCostCenterIds[$i]) ? (int) $lineCostCenterIds[$i] : null,
'branch_id' => !empty($_POST['line_branch_id'][$i]) ? (int) $_POST['line_branch_id'][$i] : null, 'branch_id' => !empty($lineBranchIds[$i]) ? (int) $lineBranchIds[$i] : null,
]; ];
} }
$result = JournalService::createEntry([ $result = JournalService::createEntry([
'entry_date' => $data['entry_date'], 'entry_date' => $data['entry_date'],
'description_ar' => $data['description_ar'], 'description_ar' => $data['description_ar'],
'description_en' => $_POST['description_en'] ?? null, 'description_en' => $request->post('description_en'),
'reference_type' => 'manual', 'reference_type' => 'manual',
'reference_number' => $_POST['reference_number'] ?? null, 'reference_number' => $request->post('reference_number'),
'branch_id' => !empty($_POST['branch_id']) ? (int) $_POST['branch_id'] : null, 'branch_id' => $request->post('branch_id') ? (int) $request->post('branch_id') : null,
'cost_center_id' => !empty($_POST['cost_center_id']) ? (int) $_POST['cost_center_id'] : null, 'cost_center_id' => $request->post('cost_center_id') ? (int) $request->post('cost_center_id') : null,
'notes' => $_POST['notes'] ?? null, 'notes' => $request->post('notes'),
], $lines, false); ], $lines, false);
$session = App::getInstance()->session(); $session = App::getInstance()->session();
...@@ -177,11 +180,11 @@ class JournalEntryController extends Controller ...@@ -177,11 +180,11 @@ class JournalEntryController extends Controller
return $this->redirect('/accounting/journal-entries/' . (int) $id); return $this->redirect('/accounting/journal-entries/' . (int) $id);
} }
public function searchAccounts(): Response public function searchAccounts(Request $request): Response
{ {
$this->authorize('accounting.journal.view'); $this->authorize('accounting.journal.view');
$term = $_GET['q'] ?? ''; $term = $request->get('q', '');
$results = Account::search($term); $results = Account::search($term);
return $this->json($results); return $this->json($results);
......
...@@ -4,6 +4,7 @@ declare(strict_types=1); ...@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Modules\Accounting\Controllers; namespace App\Modules\Accounting\Controllers;
use App\Core\Controller; use App\Core\Controller;
use App\Core\Request;
use App\Core\App; use App\Core\App;
use App\Core\Response; use App\Core\Response;
use App\Modules\Accounting\Models\FiscalYear; use App\Modules\Accounting\Models\FiscalYear;
...@@ -11,12 +12,12 @@ use App\Modules\Accounting\Services\PeriodClosingService; ...@@ -11,12 +12,12 @@ use App\Modules\Accounting\Services\PeriodClosingService;
class PeriodClosingController extends Controller class PeriodClosingController extends Controller
{ {
public function index(): Response public function index(Request $request): Response
{ {
$this->authorize('accounting.period.view'); $this->authorize('accounting.period.view');
$currentFY = FiscalYear::current(); $currentFY = FiscalYear::current();
$fiscalYearId = (int) ($_GET['fiscal_year_id'] ?? ($currentFY ? $currentFY->id : 0)); $fiscalYearId = (int) ($request->get('fiscal_year_id') ?? ($currentFY ? $currentFY->id : 0));
$periods = []; $periods = [];
$fiscalYear = null; $fiscalYear = null;
...@@ -38,12 +39,12 @@ class PeriodClosingController extends Controller ...@@ -38,12 +39,12 @@ class PeriodClosingController extends Controller
]); ]);
} }
public function closeMonth(): Response public function closeMonth(Request $request): Response
{ {
$this->authorize('accounting.period.close'); $this->authorize('accounting.period.close');
$fiscalYearId = (int) ($_POST['fiscal_year_id'] ?? 0); $fiscalYearId = (int) ($request->post('fiscal_year_id', 0));
$period = $_POST['period'] ?? ''; $period = $request->post('period', '');
if ($fiscalYearId <= 0 || empty($period)) { if ($fiscalYearId <= 0 || empty($period)) {
$session = App::getInstance()->session(); $session = App::getInstance()->session();
...@@ -63,13 +64,13 @@ class PeriodClosingController extends Controller ...@@ -63,13 +64,13 @@ class PeriodClosingController extends Controller
return $this->redirect('/accounting/period-closing?fiscal_year_id=' . $fiscalYearId); return $this->redirect('/accounting/period-closing?fiscal_year_id=' . $fiscalYearId);
} }
public function reopenMonth(): Response public function reopenMonth(Request $request): Response
{ {
$this->authorize('accounting.period.reopen'); $this->authorize('accounting.period.reopen');
$fiscalYearId = (int) ($_POST['fiscal_year_id'] ?? 0); $fiscalYearId = (int) ($request->post('fiscal_year_id', 0));
$period = $_POST['period'] ?? ''; $period = $request->post('period', '');
$reason = $_POST['reason'] ?? ''; $reason = $request->post('reason', '');
if ($fiscalYearId <= 0 || empty($period) || empty($reason)) { if ($fiscalYearId <= 0 || empty($period) || empty($reason)) {
$session = App::getInstance()->session(); $session = App::getInstance()->session();
......
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services\Reports;
use App\Core\App;
use App\Core\Database;
use App\Modules\Accounting\Models\FiscalYear;
final class RevenueAnalysisService
{
private Database $db;
public function __construct(?Database $db = null)
{
$this->db = $db ?? App::getInstance()->db();
}
public function generate(array $filters): array
{
$currentFY = FiscalYear::current();
$dateFrom = $filters['date_from'] ?? ($currentFY ? $currentFY->start_date : date('Y-01-01'));
$dateTo = $filters['date_to'] ?? date('Y-m-d');
$branchId = !empty($filters['branch_id']) ? (int) $filters['branch_id'] : null;
$branchWhere = '';
$params = [$dateFrom, $dateTo];
if ($branchId !== null) {
$branchWhere = ' AND m.branch_id = ?';
$params[] = $branchId;
}
$byType = $this->db->select(
"SELECT p.payment_type, COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
WHERE p.payment_date >= ? AND p.payment_date <= ? AND p.is_voided = 0 {$branchWhere}
GROUP BY p.payment_type ORDER BY total DESC",
$params
);
$byMethod = $this->db->select(
"SELECT p.payment_method, COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
WHERE p.payment_date >= ? AND p.payment_date <= ? AND p.is_voided = 0 {$branchWhere}
GROUP BY p.payment_method ORDER BY total DESC",
$params
);
$monthly = $this->db->select(
"SELECT DATE_FORMAT(p.payment_date, '%Y-%m') as month,
COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
WHERE p.payment_date >= ? AND p.payment_date <= ? AND p.is_voided = 0 {$branchWhere}
GROUP BY month ORDER BY month ASC",
$params
);
$monthStart = date('Y-m-01');
$dailyParams = [$monthStart, date('Y-m-d')];
$dailyBranchWhere = '';
if ($branchId !== null) {
$dailyBranchWhere = ' AND m.branch_id = ?';
$dailyParams[] = $branchId;
}
$daily = $this->db->select(
"SELECT p.payment_date as day, COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
WHERE p.payment_date >= ? AND p.payment_date <= ? AND p.is_voided = 0 {$dailyBranchWhere}
GROUP BY p.payment_date ORDER BY p.payment_date ASC",
$dailyParams
);
$grandTotal = '0.00';
$grandCount = 0;
foreach ($byType as $t) {
$grandTotal = bcadd($grandTotal, (string) $t['total'], 2);
$grandCount += (int) $t['cnt'];
}
$byBranch = [];
if ($branchId === null) {
$byBranch = $this->db->select(
"SELECT COALESCE(b.name_ar, 'بدون فرع') as branch_name, b.id as branch_id,
COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
LEFT JOIN branches b ON b.id = m.branch_id
WHERE p.payment_date >= ? AND p.payment_date <= ? AND p.is_voided = 0
GROUP BY b.id, b.name_ar ORDER BY total DESC",
[$dateFrom, $dateTo]
);
}
$voidedParams = [$dateFrom, $dateTo];
if ($branchId !== null) {
$voidedParams[] = $branchId;
}
$voidedRow = $this->db->selectOne(
"SELECT COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
WHERE p.payment_date >= ? AND p.payment_date <= ? AND p.is_voided = 1" .
($branchId !== null ? ' AND m.branch_id = ?' : ''),
$voidedParams
);
$branches = $this->db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
return [
'by_type' => $byType,
'by_method' => $byMethod,
'by_branch' => $byBranch,
'monthly' => $monthly,
'daily' => $daily,
'grand_total' => $grandTotal,
'grand_count' => $grandCount,
'voided_total' => $voidedRow['total'] ?? '0.00',
'voided_count' => (int) ($voidedRow['cnt'] ?? 0),
'branches' => $branches,
];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Accounting\Services\Reports;
use App\Core\App;
use App\Core\Database;
final class TreasuryReportService
{
private Database $db;
public function __construct(?Database $db = null)
{
$this->db = $db ?? App::getInstance()->db();
}
public function generate(array $filters): array
{
$dateFrom = $filters['date_from'] ?? date('Y-m-01');
$dateTo = $filters['date_to'] ?? date('Y-m-d');
$paymentType = $filters['payment_type'] ?? '';
$paymentMethod = $filters['payment_method'] ?? '';
$search = trim($filters['search'] ?? '');
$voidedFilter = $filters['voided'] ?? '0';
$where = 'p.payment_date >= ? AND p.payment_date <= ?';
$params = [$dateFrom, $dateTo];
if ($voidedFilter === '0') {
$where .= ' AND p.is_voided = 0';
} elseif ($voidedFilter === '1') {
$where .= ' AND p.is_voided = 1';
}
if ($paymentType !== '') {
$where .= ' AND p.payment_type = ?';
$params[] = $paymentType;
}
if ($paymentMethod !== '') {
$where .= ' AND p.payment_method = ?';
$params[] = $paymentMethod;
}
if ($search !== '') {
$where .= ' AND (m.full_name_ar LIKE ? OR m.form_number LIKE ? OR r.receipt_number LIKE ? OR p.notes LIKE ?)';
$searchTerm = '%' . $search . '%';
$params = array_merge($params, [$searchTerm, $searchTerm, $searchTerm, $searchTerm]);
}
$totalsRow = $this->db->selectOne(
"SELECT COALESCE(SUM(p.amount), 0) as total_amount, COUNT(*) as total_count
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
LEFT JOIN receipts r ON r.id = p.receipt_id
WHERE {$where}",
$params
);
$page = max(1, (int) ($filters['page'] ?? 1));
$perPage = 50;
$offset = ($page - 1) * $perPage;
$totalCount = (int) ($totalsRow['total_count'] ?? 0);
$totalPages = max(1, (int) ceil($totalCount / $perPage));
$payments = $this->db->select(
"SELECT p.*, r.receipt_number, m.full_name_ar as member_name, m.form_number,
e.full_name_ar as received_by_name
FROM payments p
LEFT JOIN receipts r ON r.id = p.receipt_id
LEFT JOIN members m ON m.id = p.member_id
LEFT JOIN employees e ON e.id = p.received_by_employee_id
WHERE {$where}
ORDER BY p.id DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
$byMethod = $this->db->select(
"SELECT p.payment_method, COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
LEFT JOIN receipts r ON r.id = p.receipt_id
WHERE {$where}
GROUP BY p.payment_method ORDER BY total DESC",
$params
);
$byType = $this->db->select(
"SELECT p.payment_type, COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
LEFT JOIN receipts r ON r.id = p.receipt_id
WHERE {$where}
GROUP BY p.payment_type ORDER BY total DESC",
$params
);
return [
'payments' => $payments,
'total_amount' => $totalsRow['total_amount'] ?? '0.00',
'total_count' => $totalCount,
'by_method' => $byMethod,
'by_type' => $byType,
'page' => $page,
'total_pages' => $totalPages,
];
}
}
...@@ -60,8 +60,8 @@ class EnrollWizardController extends Controller ...@@ -60,8 +60,8 @@ class EnrollWizardController extends Controller
public function getAvailability(Request $request): Response public function getAvailability(Request $request): Response
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$disciplineId = (int) ($_GET['discipline_id'] ?? 0); $disciplineId = (int) ($request->get('discipline_id', '0'));
$groupType = $_GET['group_type'] ?? 'group'; $groupType = $request->get('group_type', 'group');
if (!$disciplineId) { if (!$disciplineId) {
return $this->json(['success' => false]); return $this->json(['success' => false]);
...@@ -85,7 +85,7 @@ class EnrollWizardController extends Controller ...@@ -85,7 +85,7 @@ class EnrollWizardController extends Controller
public function getLevels(Request $request): Response public function getLevels(Request $request): Response
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$disciplineId = (int) ($_GET['discipline_id'] ?? 0); $disciplineId = (int) ($request->get('discipline_id', '0'));
if (!$disciplineId) { if (!$disciplineId) {
return $this->json(['success' => false]); return $this->json(['success' => false]);
...@@ -106,9 +106,9 @@ class EnrollWizardController extends Controller ...@@ -106,9 +106,9 @@ class EnrollWizardController extends Controller
public function getPrice(Request $request): Response public function getPrice(Request $request): Response
{ {
$disciplineId = (int) ($_GET['discipline_id'] ?? 0); $disciplineId = (int) ($request->get('discipline_id', '0'));
$groupType = $_GET['group_type'] ?? 'group'; $groupType = $request->get('group_type', 'group');
$isMember = ($_GET['is_member'] ?? '1') === '1'; $isMember = ($request->get('is_member', '1')) === '1';
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$pricing = $db->selectOne( $pricing = $db->selectOne(
...@@ -136,10 +136,10 @@ class EnrollWizardController extends Controller ...@@ -136,10 +136,10 @@ class EnrollWizardController extends Controller
$this->authorize('academy.enroll'); $this->authorize('academy.enroll');
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$playerId = (int) ($_POST['player_id'] ?? 0); $playerId = (int) ($request->post('player_id') ?? 0);
$disciplineId = (int) ($_POST['discipline_id'] ?? 0); $disciplineId = (int) ($request->post('discipline_id') ?? 0);
$levelId = (int) ($_POST['level_id'] ?? 0); $levelId = (int) ($request->post('level_id') ?? 0);
$groupType = $_POST['group_type'] ?? 'group'; $groupType = $request->post('group_type') ?? 'group';
if (!$playerId || !$disciplineId) { if (!$playerId || !$disciplineId) {
return $this->redirect('/activity-subscriptions/enroll')->withError('يجب تحديد اللاعب والنشاط'); return $this->redirect('/activity-subscriptions/enroll')->withError('يجب تحديد اللاعب والنشاط');
......
...@@ -12,7 +12,7 @@ class PlayerSearchController extends Controller ...@@ -12,7 +12,7 @@ class PlayerSearchController extends Controller
{ {
public function search(Request $request): Response public function search(Request $request): Response
{ {
$q = trim((string) ($_GET['q'] ?? '')); $q = trim((string) ($request->get('q', '')));
if (mb_strlen($q) < 2) { if (mb_strlen($q) < 2) {
return $this->json(['success' => true, 'players' => []]); return $this->json(['success' => true, 'players' => []]);
} }
......
...@@ -4,8 +4,8 @@ declare(strict_types=1); ...@@ -4,8 +4,8 @@ declare(strict_types=1);
return [ return [
['GET', '/carnets', 'Carnets\Controllers\CarnetController@index', ['auth'], 'carnet.view'], ['GET', '/carnets', 'Carnets\Controllers\CarnetController@index', ['auth'], 'carnet.view'],
['GET', '/carnets/issue/{memberId}', 'Carnets\Controllers\CarnetController@issue', ['auth'], 'carnet.issue'], ['GET', '/carnets/issue/{memberId}', 'Carnets\Controllers\CarnetController@issue', ['auth'], 'carnet.issue'],
['POST', '/carnets/issue/{memberId}', 'Carnets\Controllers\CarnetController@issue', ['auth'], 'carnet.issue'], ['POST', '/carnets/issue/{memberId}', 'Carnets\Controllers\CarnetController@issue', ['auth', 'csrf'], 'carnet.issue'],
['GET', '/carnets/{id}/print', 'Carnets\Controllers\CarnetController@print', ['auth'], 'carnet.print'], ['GET', '/carnets/{id}/print', 'Carnets\Controllers\CarnetController@print', ['auth'], 'carnet.print'],
['POST', '/carnets/{id}/deactivate', 'Carnets\Controllers\CarnetController@deactivate', ['auth'], 'carnet.deactivate'], ['POST', '/carnets/{id}/deactivate', 'Carnets\Controllers\CarnetController@deactivate', ['auth', 'csrf'], 'carnet.deactivate'],
['GET', '/carnets/replace/{memberId}', 'Carnets\Controllers\CarnetController@replace', ['auth'], 'carnet.issue'], ['GET', '/carnets/replace/{memberId}', 'Carnets\Controllers\CarnetController@replace', ['auth'], 'carnet.issue'],
]; ];
\ No newline at end of file
...@@ -4,6 +4,6 @@ declare(strict_types=1); ...@@ -4,6 +4,6 @@ declare(strict_types=1);
return [ return [
['GET', '/cashier', 'Cashier\Controllers\CashierController@queue', ['auth'], 'cashier.view_queue'], ['GET', '/cashier', 'Cashier\Controllers\CashierController@queue', ['auth'], 'cashier.view_queue'],
['GET', '/cashier/{id}', 'Cashier\Controllers\CashierController@process', ['auth'], 'cashier.process_payment'], ['GET', '/cashier/{id}', 'Cashier\Controllers\CashierController@process', ['auth'], 'cashier.process_payment'],
['POST', '/cashier/{id}/complete', 'Cashier\Controllers\CashierController@complete', ['auth'], 'cashier.process_payment'], ['POST', '/cashier/{id}/complete', 'Cashier\Controllers\CashierController@complete', ['auth', 'csrf'], 'cashier.process_payment'],
['POST', '/cashier/{id}/cancel', 'Cashier\Controllers\CashierController@cancel', ['auth'], 'cashier.cancel_request'], ['POST', '/cashier/{id}/cancel', 'Cashier\Controllers\CashierController@cancel', ['auth', 'csrf'], 'cashier.cancel_request'],
]; ];
...@@ -3,11 +3,11 @@ declare(strict_types=1); ...@@ -3,11 +3,11 @@ declare(strict_types=1);
return [ return [
['GET', '/members/{memberId}/children/create', 'Children\Controllers\ChildController@create', ['auth'], 'child.add'], ['GET', '/members/{memberId}/children/create', 'Children\Controllers\ChildController@create', ['auth'], 'child.add'],
['POST', '/members/{memberId}/children', 'Children\Controllers\ChildController@store', ['auth'], 'child.add'], ['POST', '/members/{memberId}/children', 'Children\Controllers\ChildController@store', ['auth', 'csrf'], 'child.add'],
['GET', '/members/{memberId}/children/{id}', 'Children\Controllers\ChildController@show', ['auth'], 'child.view'], ['GET', '/members/{memberId}/children/{id}', 'Children\Controllers\ChildController@show', ['auth'], 'child.view'],
['GET', '/members/{memberId}/children/{id}/edit', 'Children\Controllers\ChildController@edit', ['auth'], 'child.edit'], ['GET', '/members/{memberId}/children/{id}/edit', 'Children\Controllers\ChildController@edit', ['auth'], 'child.edit'],
['POST', '/members/{memberId}/children/{id}', 'Children\Controllers\ChildController@update', ['auth'], 'child.edit'], ['POST', '/members/{memberId}/children/{id}', 'Children\Controllers\ChildController@update', ['auth', 'csrf'], 'child.edit'],
['POST', '/members/{memberId}/children/{id}/archive', 'Children\Controllers\ChildController@archive', ['auth'], 'child.remove'], ['POST', '/members/{memberId}/children/{id}/archive', 'Children\Controllers\ChildController@archive', ['auth', 'csrf'], 'child.remove'],
['POST', '/members/{memberId}/children/{id}/freeze', 'Children\Controllers\ChildController@freeze', ['auth'], 'child.freeze'], ['POST', '/members/{memberId}/children/{id}/freeze', 'Children\Controllers\ChildController@freeze', ['auth', 'csrf'], 'child.freeze'],
['POST', '/api/children/calculate-fee', 'Children\Controllers\ChildController@calculateFee', ['auth'], 'child.add'], ['POST', '/api/children/calculate-fee', 'Children\Controllers\ChildController@calculateFee', ['auth'], 'child.add'],
]; ];
\ No newline at end of file
...@@ -4,8 +4,8 @@ declare(strict_types=1); ...@@ -4,8 +4,8 @@ declare(strict_types=1);
return [ return [
['GET', '/death', 'Death\Controllers\DeathController@index', ['auth'], 'transfer.view'], ['GET', '/death', 'Death\Controllers\DeathController@index', ['auth'], 'transfer.view'],
['GET', '/death/create/{memberId}', 'Death\Controllers\DeathController@create', ['auth'], 'transfer.initiate'], ['GET', '/death/create/{memberId}', 'Death\Controllers\DeathController@create', ['auth'], 'transfer.initiate'],
['POST', '/death/store/{memberId}', 'Death\Controllers\DeathController@store', ['auth'], 'transfer.initiate'], ['POST', '/death/store/{memberId}', 'Death\Controllers\DeathController@store', ['auth', 'csrf'], 'transfer.initiate'],
['GET', '/death/{id}', 'Death\Controllers\DeathController@show', ['auth'], 'transfer.view'], ['GET', '/death/{id}', 'Death\Controllers\DeathController@show', ['auth'], 'transfer.view'],
['POST', '/death/{id}/pay', 'Death\Controllers\DeathController@pay', ['auth'], 'payment.collect'], ['POST', '/death/{id}/pay', 'Death\Controllers\DeathController@pay', ['auth', 'csrf'], 'payment.collect'],
['POST', '/death/{id}/complete', 'Death\Controllers\DeathController@complete', ['auth'], 'transfer.approve'], ['POST', '/death/{id}/complete', 'Death\Controllers\DeathController@complete', ['auth', 'csrf'], 'transfer.approve'],
]; ];
\ No newline at end of file
...@@ -4,8 +4,8 @@ declare(strict_types=1); ...@@ -4,8 +4,8 @@ declare(strict_types=1);
return [ return [
['GET', '/divorce', 'Divorce\Controllers\DivorceController@index', ['auth'], 'transfer.view'], ['GET', '/divorce', 'Divorce\Controllers\DivorceController@index', ['auth'], 'transfer.view'],
['GET', '/divorce/create/{memberId}', 'Divorce\Controllers\DivorceController@create', ['auth'], 'transfer.initiate'], ['GET', '/divorce/create/{memberId}', 'Divorce\Controllers\DivorceController@create', ['auth'], 'transfer.initiate'],
['POST', '/divorce/store/{memberId}', 'Divorce\Controllers\DivorceController@store', ['auth'], 'transfer.initiate'], ['POST', '/divorce/store/{memberId}', 'Divorce\Controllers\DivorceController@store', ['auth', 'csrf'], 'transfer.initiate'],
['GET', '/divorce/{id}', 'Divorce\Controllers\DivorceController@show', ['auth'], 'transfer.view'], ['GET', '/divorce/{id}', 'Divorce\Controllers\DivorceController@show', ['auth'], 'transfer.view'],
['POST', '/divorce/{id}/pay', 'Divorce\Controllers\DivorceController@pay', ['auth'], 'payment.collect'], ['POST', '/divorce/{id}/pay', 'Divorce\Controllers\DivorceController@pay', ['auth', 'csrf'], 'payment.collect'],
['POST', '/divorce/{id}/complete', 'Divorce\Controllers\DivorceController@complete',['auth'], 'transfer.approve'], ['POST', '/divorce/{id}/complete', 'Divorce\Controllers\DivorceController@complete',['auth', 'csrf'], 'transfer.approve'],
]; ];
\ No newline at end of file
...@@ -4,9 +4,9 @@ declare(strict_types=1); ...@@ -4,9 +4,9 @@ declare(strict_types=1);
return [ return [
['GET', '/documents', 'Documents\Controllers\DocumentController@index', ['auth'], 'document.view'], ['GET', '/documents', 'Documents\Controllers\DocumentController@index', ['auth'], 'document.view'],
['GET', '/documents/upload/{memberId}', 'Documents\Controllers\DocumentController@upload', ['auth'], 'document.upload'], ['GET', '/documents/upload/{memberId}', 'Documents\Controllers\DocumentController@upload', ['auth'], 'document.upload'],
['POST', '/documents/upload/{memberId}', 'Documents\Controllers\DocumentController@upload', ['auth'], 'document.upload'], ['POST', '/documents/upload/{memberId}', 'Documents\Controllers\DocumentController@upload', ['auth', 'csrf'], 'document.upload'],
['GET', '/documents/{id}/download', 'Documents\Controllers\DocumentController@download', ['auth'], 'document.view'], ['GET', '/documents/{id}/download', 'Documents\Controllers\DocumentController@download', ['auth'], 'document.view'],
['GET', '/documents/{id}/preview', 'Documents\Controllers\DocumentController@preview', ['auth'], 'document.view'], ['GET', '/documents/{id}/preview', 'Documents\Controllers\DocumentController@preview', ['auth'], 'document.view'],
['POST', '/documents/{id}/archive', 'Documents\Controllers\DocumentController@archive', ['auth'], 'document.delete'], ['POST', '/documents/{id}/archive', 'Documents\Controllers\DocumentController@archive', ['auth', 'csrf'], 'document.delete'],
['GET', '/documents/member/{memberId}', 'Documents\Controllers\DocumentController@memberDocuments', ['auth'], 'document.view'], ['GET', '/documents/member/{memberId}', 'Documents\Controllers\DocumentController@memberDocuments', ['auth'], 'document.view'],
]; ];
\ No newline at end of file
...@@ -41,25 +41,23 @@ class FacilityGridController extends Controller ...@@ -41,25 +41,23 @@ class FacilityGridController extends Controller
'cols_count' => 'required|numeric|min:1|max:50', 'cols_count' => 'required|numeric|min:1|max:50',
]; ];
if (!$this->validate($_POST, $rules)) { $data = $this->validated($request->all(), $rules);
return $this->redirect('/facility-grids/create');
}
$grid = FacilityGrid::create([ $grid = FacilityGrid::create([
'facility_id' => (int) $_POST['facility_id'], 'facility_id' => (int) $data['facility_id'],
'name_ar' => $_POST['name_ar'], 'name_ar' => $data['name_ar'],
'name_en' => $_POST['name_en'] ?? null, 'name_en' => $request->post('name_en'),
'grid_type' => $_POST['grid_type'], 'grid_type' => $data['grid_type'],
'rows_count' => (int) $_POST['rows_count'], 'rows_count' => (int) $data['rows_count'],
'cols_count' => (int) $_POST['cols_count'], 'cols_count' => (int) $data['cols_count'],
'row_label_ar' => $_POST['row_label_ar'] ?? 'حارة', 'row_label_ar' => $request->post('row_label_ar') ?? 'حارة',
'col_label_ar' => $_POST['col_label_ar'] ?? 'قسم', 'col_label_ar' => $request->post('col_label_ar') ?? 'قسم',
'physical_length'=> !empty($_POST['physical_length']) ? (float) $_POST['physical_length'] : null, 'physical_length'=> $request->post('physical_length') ? (float) $request->post('physical_length') : null,
'physical_width' => !empty($_POST['physical_width']) ? (float) $_POST['physical_width'] : null, 'physical_width' => $request->post('physical_width') ? (float) $request->post('physical_width') : null,
'is_active' => 1, 'is_active' => 1,
]); ]);
$this->generateZones((int) $grid->id, (int) $_POST['rows_count'], (int) $_POST['cols_count']); $this->generateZones((int) $grid->id, (int) $data['rows_count'], (int) $data['cols_count']);
return $this->redirect('/facility-grids/' . $grid->id)->withSuccess('تم إنشاء الشبكة بنجاح'); return $this->redirect('/facility-grids/' . $grid->id)->withSuccess('تم إنشاء الشبكة بنجاح');
} }
...@@ -71,9 +69,9 @@ class FacilityGridController extends Controller ...@@ -71,9 +69,9 @@ class FacilityGridController extends Controller
$grid = FacilityGrid::find((int) $id); $grid = FacilityGrid::find((int) $id);
if (!$grid) return $this->redirect('/facility-grids')->withError('الشبكة غير موجودة'); if (!$grid) return $this->redirect('/facility-grids')->withError('الشبكة غير موجودة');
$date = $_GET['date'] ?? date('Y-m-d'); $date = $request->get('date', date('Y-m-d'));
$time = $_GET['time'] ?? null; $time = $request->get('time');
$planMonth = $_GET['month'] ?? substr($date, 0, 7); $planMonth = $request->get('month', substr($date, 0, 7));
$state = GridStateService::getState((int) $id, $date, $time); $state = GridStateService::getState((int) $id, $date, $time);
return $this->view('FacilityGrids.Views.show', [ return $this->view('FacilityGrids.Views.show', [
...@@ -112,31 +110,29 @@ class FacilityGridController extends Controller ...@@ -112,31 +110,29 @@ class FacilityGridController extends Controller
'cols_count' => 'required|numeric|min:1|max:50', 'cols_count' => 'required|numeric|min:1|max:50',
]; ];
if (!$this->validate($_POST, $rules)) { $data = $this->validated($request->all(), $rules);
return $this->redirect('/facility-grids/' . $id . '/edit');
}
$grid->update([ $grid->update([
'name_ar' => $_POST['name_ar'], 'name_ar' => $data['name_ar'],
'name_en' => $_POST['name_en'] ?? null, 'name_en' => $request->post('name_en'),
'grid_type' => $_POST['grid_type'], 'grid_type' => $data['grid_type'],
'rows_count' => (int) $_POST['rows_count'], 'rows_count' => (int) $data['rows_count'],
'cols_count' => (int) $_POST['cols_count'], 'cols_count' => (int) $data['cols_count'],
'row_label_ar' => $_POST['row_label_ar'] ?? 'حارة', 'row_label_ar' => $request->post('row_label_ar') ?? 'حارة',
'col_label_ar' => $_POST['col_label_ar'] ?? 'قسم', 'col_label_ar' => $request->post('col_label_ar') ?? 'قسم',
'physical_length'=> !empty($_POST['physical_length']) ? (float) $_POST['physical_length'] : null, 'physical_length'=> $request->post('physical_length') ? (float) $request->post('physical_length') : null,
'physical_width' => !empty($_POST['physical_width']) ? (float) $_POST['physical_width'] : null, 'physical_width' => $request->post('physical_width') ? (float) $request->post('physical_width') : null,
]); ]);
$this->regenerateZones((int) $id, (int) $_POST['rows_count'], (int) $_POST['cols_count']); $this->regenerateZones((int) $id, (int) $data['rows_count'], (int) $data['cols_count']);
return $this->redirect('/facility-grids/' . $id)->withSuccess('تم تحديث الشبكة'); return $this->redirect('/facility-grids/' . $id)->withSuccess('تم تحديث الشبكة');
} }
public function apiState(Request $request, string $id): Response public function apiState(Request $request, string $id): Response
{ {
$date = $_GET['date'] ?? date('Y-m-d'); $date = $request->get('date', date('Y-m-d'));
$time = $_GET['time'] ?? null; $time = $request->get('time');
$state = GridStateService::getState((int) $id, $date, $time); $state = GridStateService::getState((int) $id, $date, $time);
return $this->json(['success' => true, 'data' => $state]); return $this->json(['success' => true, 'data' => $state]);
} }
......
...@@ -16,10 +16,10 @@ class GridDashboardController extends Controller ...@@ -16,10 +16,10 @@ class GridDashboardController extends Controller
$this->authorize('facility_grid.view'); $this->authorize('facility_grid.view');
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$gridId = (int) ($_GET['grid_id'] ?? 0); $gridId = (int) ($request->get('grid_id', '0'));
$filter = $_GET['filter'] ?? 'day'; $filter = $request->get('filter', 'day');
$from = $_GET['from'] ?? date('Y-m-d'); $from = $request->get('from', date('Y-m-d'));
$to = $_GET['to'] ?? date('Y-m-d'); $to = $request->get('to', date('Y-m-d'));
switch ($filter) { switch ($filter) {
case 'week': case 'week':
...@@ -35,8 +35,8 @@ class GridDashboardController extends Controller ...@@ -35,8 +35,8 @@ class GridDashboardController extends Controller
$to = date('Y-12-31'); $to = date('Y-12-31');
break; break;
case 'custom': case 'custom':
$from = $_GET['from'] ?? date('Y-m-d'); $from = $request->get('from', date('Y-m-d'));
$to = $_GET['to'] ?? date('Y-m-d'); $to = $request->get('to', date('Y-m-d'));
break; break;
} }
......
...@@ -16,10 +16,10 @@ class ZoneTraineeController extends Controller ...@@ -16,10 +16,10 @@ class ZoneTraineeController extends Controller
$gridId = (int) $gridId; $gridId = (int) $gridId;
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$zoneId = (int) ($_POST['zone_id'] ?? 0); $zoneId = (int) ($request->post('zone_id') ?? 0);
$playerId = !empty($_POST['player_id']) ? (int) $_POST['player_id'] : null; $playerId = $request->post('player_id') ? (int) $request->post('player_id') : null;
$traineeName = $_POST['trainee_name'] ?? null; $traineeName = $request->post('trainee_name');
$scheduleId = !empty($_POST['schedule_id']) ? (int) $_POST['schedule_id'] : null; $scheduleId = $request->post('schedule_id') ? (int) $request->post('schedule_id') : null;
if (!$zoneId || (!$playerId && !$traineeName)) { if (!$zoneId || (!$playerId && !$traineeName)) {
return $this->json(['success' => false, 'message' => 'بيانات غير كاملة']); return $this->json(['success' => false, 'message' => 'بيانات غير كاملة']);
...@@ -67,7 +67,7 @@ class ZoneTraineeController extends Controller ...@@ -67,7 +67,7 @@ class ZoneTraineeController extends Controller
$traineeId = (int) $traineeId; $traineeId = (int) $traineeId;
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$targetZoneId = (int) ($_POST['target_zone_id'] ?? 0); $targetZoneId = (int) ($request->post('target_zone_id') ?? 0);
if (!$targetZoneId) { if (!$targetZoneId) {
return $this->json(['success' => false, 'message' => 'لم يتم تحديد المنطقة']); return $this->json(['success' => false, 'message' => 'لم يتم تحديد المنطقة']);
} }
......
...@@ -4,11 +4,11 @@ declare(strict_types=1); ...@@ -4,11 +4,11 @@ declare(strict_types=1);
return [ return [
['GET', '/violations', 'Fines\Controllers\ViolationController@index', ['auth'], 'fine.view'], ['GET', '/violations', 'Fines\Controllers\ViolationController@index', ['auth'], 'fine.view'],
['GET', '/violations/create/{memberId}', 'Fines\Controllers\ViolationController@create', ['auth'], 'fine.impose'], ['GET', '/violations/create/{memberId}', 'Fines\Controllers\ViolationController@create', ['auth'], 'fine.impose'],
['POST', '/violations/store/{memberId}', 'Fines\Controllers\ViolationController@store', ['auth'], 'fine.impose'], ['POST', '/violations/store/{memberId}', 'Fines\Controllers\ViolationController@store', ['auth', 'csrf'], 'fine.impose'],
['GET', '/fines', 'Fines\Controllers\FineController@index', ['auth'], 'fine.view'], ['GET', '/fines', 'Fines\Controllers\FineController@index', ['auth'], 'fine.view'],
['POST', '/fines/impose/{violationId}', 'Fines\Controllers\FineController@impose', ['auth'], 'fine.impose'], ['POST', '/fines/impose/{violationId}', 'Fines\Controllers\FineController@impose', ['auth', 'csrf'], 'fine.impose'],
['POST', '/fines/{id}/pay', 'Fines\Controllers\FineController@pay', ['auth'], 'fine.collect'], ['POST', '/fines/{id}/pay', 'Fines\Controllers\FineController@pay', ['auth', 'csrf'], 'fine.collect'],
['POST', '/fines/{id}/appeal', 'Fines\Controllers\FineController@submitAppeal', ['auth'], 'fine.view'], ['POST', '/fines/{id}/appeal', 'Fines\Controllers\FineController@submitAppeal', ['auth', 'csrf'], 'fine.view'],
['POST', '/fines/{id}/appeal-decide', 'Fines\Controllers\FineController@decideAppeal', ['auth'], 'fine.impose'], ['POST', '/fines/{id}/appeal-decide', 'Fines\Controllers\FineController@decideAppeal', ['auth', 'csrf'], 'fine.impose'],
['POST', '/fines/{id}/waive', 'Fines\Controllers\FineController@waive', ['auth'], 'fine.waive'], ['POST', '/fines/{id}/waive', 'Fines\Controllers\FineController@waive', ['auth', 'csrf'], 'fine.waive'],
]; ];
\ No newline at end of file
...@@ -4,5 +4,5 @@ declare(strict_types=1); ...@@ -4,5 +4,5 @@ declare(strict_types=1);
return [ return [
['GET', '/foreign', 'Foreign\Controllers\ForeignController@index', ['auth'], 'member.view'], ['GET', '/foreign', 'Foreign\Controllers\ForeignController@index', ['auth'], 'member.view'],
['GET', '/members/{memberId}/foreign/create', 'Foreign\Controllers\ForeignController@create', ['auth'], 'member.create'], ['GET', '/members/{memberId}/foreign/create', 'Foreign\Controllers\ForeignController@create', ['auth'], 'member.create'],
['POST', '/members/{memberId}/foreign', 'Foreign\Controllers\ForeignController@store', ['auth'], 'member.create'], ['POST', '/members/{memberId}/foreign', 'Foreign\Controllers\ForeignController@store', ['auth', 'csrf'], 'member.create'],
]; ];
\ No newline at end of file
...@@ -4,5 +4,5 @@ declare(strict_types=1); ...@@ -4,5 +4,5 @@ declare(strict_types=1);
return [ return [
['GET', '/honorary', 'Honorary\Controllers\HonoraryController@index', ['auth'], 'member.view'], ['GET', '/honorary', 'Honorary\Controllers\HonoraryController@index', ['auth'], 'member.view'],
['GET', '/members/{memberId}/honorary/create', 'Honorary\Controllers\HonoraryController@create', ['auth'], 'member.change_status'], ['GET', '/members/{memberId}/honorary/create', 'Honorary\Controllers\HonoraryController@create', ['auth'], 'member.change_status'],
['POST', '/members/{memberId}/honorary', 'Honorary\Controllers\HonoraryController@store', ['auth'], 'member.change_status'], ['POST', '/members/{memberId}/honorary', 'Honorary\Controllers\HonoraryController@store', ['auth', 'csrf'], 'member.change_status'],
]; ];
\ No newline at end of file
...@@ -5,5 +5,5 @@ return [ ...@@ -5,5 +5,5 @@ return [
['GET', '/installments', 'Installments\Controllers\InstallmentController@index', ['auth'], 'installment.view'], ['GET', '/installments', 'Installments\Controllers\InstallmentController@index', ['auth'], 'installment.view'],
['GET', '/installments/create/{memberId}', 'Installments\Controllers\InstallmentController@create', ['auth'], 'installment.create'], ['GET', '/installments/create/{memberId}', 'Installments\Controllers\InstallmentController@create', ['auth'], 'installment.create'],
['GET', '/installments/{id}', 'Installments\Controllers\InstallmentController@show', ['auth'], 'installment.view'], ['GET', '/installments/{id}', 'Installments\Controllers\InstallmentController@show', ['auth'], 'installment.view'],
['POST', '/installments/{planId}/pay/{scheduleId}', 'Installments\Controllers\InstallmentController@payInstallment', ['auth'], 'installment.pay'], ['POST', '/installments/{planId}/pay/{scheduleId}', 'Installments\Controllers\InstallmentController@payInstallment', ['auth', 'csrf'], 'installment.pay'],
]; ];
\ No newline at end of file
...@@ -4,9 +4,9 @@ declare(strict_types=1); ...@@ -4,9 +4,9 @@ declare(strict_types=1);
return [ return [
['GET', '/interviews', 'Interviews\Controllers\InterviewController@index', ['auth'], 'interview.view'], ['GET', '/interviews', 'Interviews\Controllers\InterviewController@index', ['auth'], 'interview.view'],
['GET', '/interviews/schedule/{memberId}', 'Interviews\Controllers\InterviewController@schedule', ['auth'], 'interview.schedule'], ['GET', '/interviews/schedule/{memberId}', 'Interviews\Controllers\InterviewController@schedule', ['auth'], 'interview.schedule'],
['POST', '/interviews/schedule/{memberId}', 'Interviews\Controllers\InterviewController@storeSchedule', ['auth'], 'interview.schedule'], ['POST', '/interviews/schedule/{memberId}', 'Interviews\Controllers\InterviewController@storeSchedule', ['auth', 'csrf'], 'interview.schedule'],
['POST', '/interviews/{id}/reschedule', 'Interviews\Controllers\InterviewController@reschedule', ['auth'], 'interview.schedule'], ['POST', '/interviews/{id}/reschedule', 'Interviews\Controllers\InterviewController@reschedule', ['auth', 'csrf'], 'interview.schedule'],
['GET', '/interviews/{id}/decide', 'Interviews\Controllers\InterviewController@decide', ['auth'], 'interview.decide'], ['GET', '/interviews/{id}/decide', 'Interviews\Controllers\InterviewController@decide', ['auth'], 'interview.decide'],
['POST', '/interviews/{id}/decide', 'Interviews\Controllers\InterviewController@decide', ['auth'], 'interview.decide'], ['POST', '/interviews/{id}/decide', 'Interviews\Controllers\InterviewController@decide', ['auth', 'csrf'], 'interview.decide'],
['GET', '/interviews/member/{memberId}', 'Interviews\Controllers\InterviewController@memberInterviews', ['auth'], 'interview.view'], ['GET', '/interviews/member/{memberId}', 'Interviews\Controllers\InterviewController@memberInterviews', ['auth'], 'interview.view'],
]; ];
\ No newline at end of file
...@@ -4,17 +4,17 @@ declare(strict_types=1); ...@@ -4,17 +4,17 @@ declare(strict_types=1);
return [ return [
['GET', '/members', 'Members\Controllers\MemberController@index', ['auth'], 'member.view'], ['GET', '/members', 'Members\Controllers\MemberController@index', ['auth'], 'member.view'],
['GET', '/members/create', 'Members\Controllers\MemberController@create', ['auth'], 'member.create'], ['GET', '/members/create', 'Members\Controllers\MemberController@create', ['auth'], 'member.create'],
['POST', '/members', 'Members\Controllers\MemberController@store', ['auth'], 'member.create'], ['POST', '/members', 'Members\Controllers\MemberController@store', ['auth', 'csrf'], 'member.create'],
['GET', '/members/search', 'Members\Controllers\MemberController@search', ['auth'], 'member.view'], ['GET', '/members/search', 'Members\Controllers\MemberController@search', ['auth'], 'member.view'],
['GET', '/members/{id}', 'Members\Controllers\MemberController@show', ['auth'], 'member.view'], ['GET', '/members/{id}', 'Members\Controllers\MemberController@show', ['auth'], 'member.view'],
['GET', '/members/{id}/edit', 'Members\Controllers\MemberController@edit', ['auth'], 'member.edit'], ['GET', '/members/{id}/edit', 'Members\Controllers\MemberController@edit', ['auth'], 'member.edit'],
['POST', '/members/{id}', 'Members\Controllers\MemberController@update', ['auth'], 'member.edit'], ['POST', '/members/{id}', 'Members\Controllers\MemberController@update', ['auth', 'csrf'], 'member.edit'],
['POST', '/members/{id}/status', 'Members\Controllers\MemberController@changeStatus', ['auth'], 'member.change_status'], ['POST', '/members/{id}/status', 'Members\Controllers\MemberController@changeStatus', ['auth', 'csrf'], 'member.change_status'],
['POST', '/members/{id}/pay-form-fee', 'Members\Controllers\MemberController@payFormFee', ['auth'], 'member.pay_form_fee'], ['POST', '/members/{id}/pay-form-fee', 'Members\Controllers\MemberController@payFormFee', ['auth', 'csrf'], 'member.pay_form_fee'],
['POST', '/members/{id}/pay-membership', 'Members\Controllers\MemberController@payMembership',['auth'], 'member.pay_membership'], ['POST', '/members/{id}/pay-membership', 'Members\Controllers\MemberController@payMembership',['auth', 'csrf'], 'member.pay_membership'],
['POST', '/members/{id}/pay-addition', 'Members\Controllers\MemberController@payAdditionFee', ['auth'], 'member.pay_membership'], ['POST', '/members/{id}/pay-addition', 'Members\Controllers\MemberController@payAdditionFee', ['auth', 'csrf'], 'member.pay_membership'],
['GET', '/members/{id}/fill-form', 'Members\Controllers\MemberController@fillForm', ['auth'], 'member.fill_form'], ['GET', '/members/{id}/fill-form', 'Members\Controllers\MemberController@fillForm', ['auth'], 'member.fill_form'],
['POST', '/members/{id}/fill-form', 'Members\Controllers\MemberController@saveFillForm', ['auth'], 'member.fill_form'], ['POST', '/members/{id}/fill-form', 'Members\Controllers\MemberController@saveFillForm', ['auth', 'csrf'], 'member.fill_form'],
['POST', '/api/members/parse-nid', 'Members\Controllers\MemberApiController@parseNid', ['auth'], 'member.create'], ['POST', '/api/members/parse-nid', 'Members\Controllers\MemberApiController@parseNid', ['auth'], 'member.create'],
['POST', '/api/members/search', 'Members\Controllers\MemberApiController@search', ['auth'], 'member.view'], ['POST', '/api/members/search', 'Members\Controllers\MemberApiController@search', ['auth'], 'member.view'],
]; ];
\ No newline at end of file
...@@ -3,9 +3,9 @@ declare(strict_types=1); ...@@ -3,9 +3,9 @@ declare(strict_types=1);
return [ return [
['GET', '/notifications/templates', 'Notifications\Controllers\NotificationController@templates', ['auth'], 'sms.view_log'], ['GET', '/notifications/templates', 'Notifications\Controllers\NotificationController@templates', ['auth'], 'sms.view_log'],
['POST', '/notifications/templates/{id}', 'Notifications\Controllers\NotificationController@updateTemplate', ['auth'], 'sms.edit_templates'], ['POST', '/notifications/templates/{id}', 'Notifications\Controllers\NotificationController@updateTemplate', ['auth', 'csrf'], 'sms.edit_templates'],
['GET', '/notifications/log', 'Notifications\Controllers\NotificationController@log', ['auth'], 'sms.view_log'], ['GET', '/notifications/log', 'Notifications\Controllers\NotificationController@log', ['auth'], 'sms.view_log'],
['GET', '/notifications/send', 'Notifications\Controllers\NotificationController@sendForm', ['auth'], 'sms.send_single'], ['GET', '/notifications/send', 'Notifications\Controllers\NotificationController@sendForm', ['auth'], 'sms.send_single'],
['POST', '/notifications/send', 'Notifications\Controllers\NotificationController@send', ['auth'], 'sms.send_single'], ['POST', '/notifications/send', 'Notifications\Controllers\NotificationController@send', ['auth', 'csrf'], 'sms.send_single'],
['POST', '/notifications/send-bulk', 'Notifications\Controllers\NotificationController@sendBulk', ['auth'], 'sms.send_bulk'], ['POST', '/notifications/send-bulk', 'Notifications\Controllers\NotificationController@sendBulk', ['auth', 'csrf'], 'sms.send_bulk'],
]; ];
\ No newline at end of file
...@@ -5,8 +5,8 @@ return [ ...@@ -5,8 +5,8 @@ return [
['GET', '/payments', 'Payments\Controllers\PaymentController@index', ['auth'], 'payment.view'], ['GET', '/payments', 'Payments\Controllers\PaymentController@index', ['auth'], 'payment.view'],
['GET', '/payments/daily-report', 'Payments\Controllers\PaymentController@dailyReport', ['auth'], 'payment.view'], ['GET', '/payments/daily-report', 'Payments\Controllers\PaymentController@dailyReport', ['auth'], 'payment.view'],
['GET', '/payments/process/{memberId}', 'Payments\Controllers\PaymentController@process', ['auth'], 'payment.create'], ['GET', '/payments/process/{memberId}', 'Payments\Controllers\PaymentController@process', ['auth'], 'payment.create'],
['POST', '/payments/process/{memberId}', 'Payments\Controllers\PaymentController@store', ['auth'], 'payment.create'], ['POST', '/payments/process/{memberId}', 'Payments\Controllers\PaymentController@store', ['auth', 'csrf'], 'payment.create'],
['GET', '/payments/member/{memberId}', 'Payments\Controllers\PaymentController@memberHistory', ['auth'], 'payment.view'], ['GET', '/payments/member/{memberId}', 'Payments\Controllers\PaymentController@memberHistory', ['auth'], 'payment.view'],
['GET', '/payments/{id}', 'Payments\Controllers\PaymentController@show', ['auth'], 'payment.view'], ['GET', '/payments/{id}', 'Payments\Controllers\PaymentController@show', ['auth'], 'payment.view'],
['POST', '/payments/{id}/void', 'Payments\Controllers\PaymentController@void', ['auth'], 'payment.void'], ['POST', '/payments/{id}/void', 'Payments\Controllers\PaymentController@void', ['auth', 'csrf'], 'payment.void'],
]; ];
\ No newline at end of file
This diff is collapsed.
...@@ -5,5 +5,5 @@ return [ ...@@ -5,5 +5,5 @@ return [
['GET', '/receipts', 'Receipts\Controllers\ReceiptController@index', ['auth'], 'payment.view'], ['GET', '/receipts', 'Receipts\Controllers\ReceiptController@index', ['auth'], 'payment.view'],
['GET', '/receipts/voided', 'Receipts\Controllers\ReceiptController@voided', ['auth'], 'payment.void_receipt'], ['GET', '/receipts/voided', 'Receipts\Controllers\ReceiptController@voided', ['auth'], 'payment.void_receipt'],
['GET', '/receipts/{id}/print', 'Receipts\Controllers\ReceiptController@printReceipt', ['auth'], 'payment.view'], ['GET', '/receipts/{id}/print', 'Receipts\Controllers\ReceiptController@printReceipt', ['auth'], 'payment.view'],
['POST', '/receipts/{id}/void', 'Receipts\Controllers\ReceiptController@voidReceipt', ['auth'], 'payment.void_receipt'], ['POST', '/receipts/{id}/void', 'Receipts\Controllers\ReceiptController@voidReceipt', ['auth', 'csrf'], 'payment.void_receipt'],
]; ];
\ No newline at end of file
...@@ -4,5 +4,5 @@ declare(strict_types=1); ...@@ -4,5 +4,5 @@ declare(strict_types=1);
return [ return [
['GET', '/seasonal', 'Seasonal\Controllers\SeasonalController@index', ['auth'], 'temp.view'], ['GET', '/seasonal', 'Seasonal\Controllers\SeasonalController@index', ['auth'], 'temp.view'],
['GET', '/members/{memberId}/seasonal/create', 'Seasonal\Controllers\SeasonalController@create', ['auth'], 'temp.add'], ['GET', '/members/{memberId}/seasonal/create', 'Seasonal\Controllers\SeasonalController@create', ['auth'], 'temp.add'],
['POST', '/members/{memberId}/seasonal', 'Seasonal\Controllers\SeasonalController@store', ['auth'], 'temp.add'], ['POST', '/members/{memberId}/seasonal', 'Seasonal\Controllers\SeasonalController@store', ['auth', 'csrf'], 'temp.add'],
]; ];
\ No newline at end of file
...@@ -5,5 +5,5 @@ return [ ...@@ -5,5 +5,5 @@ return [
['GET', '/sports', 'Sports\Controllers\SportsController@index', ['auth'], 'sports.view'], ['GET', '/sports', 'Sports\Controllers\SportsController@index', ['auth'], 'sports.view'],
['GET', '/members/{memberId}/sports/create', 'Sports\Controllers\SportsController@create', ['auth'], 'sports.add'], ['GET', '/members/{memberId}/sports/create', 'Sports\Controllers\SportsController@create', ['auth'], 'sports.add'],
['POST', '/members/{memberId}/sports', 'Sports\Controllers\SportsController@store', ['auth', 'csrf'], 'sports.add'], ['POST', '/members/{memberId}/sports', 'Sports\Controllers\SportsController@store', ['auth', 'csrf'], 'sports.add'],
['POST', '/members/{memberId}/sports/check-conversion', 'Sports\Controllers\SportsController@checkConversion', ['auth'], 'sports.convert'], ['POST', '/members/{memberId}/sports/check-conversion', 'Sports\Controllers\SportsController@checkConversion', ['auth', 'csrf'], 'sports.convert'],
]; ];
\ No newline at end of file
...@@ -3,10 +3,10 @@ declare(strict_types=1); ...@@ -3,10 +3,10 @@ declare(strict_types=1);
return [ return [
['GET', '/members/{memberId}/spouses/create', 'Spouses\Controllers\SpouseController@create', ['auth'], 'spouse.add'], ['GET', '/members/{memberId}/spouses/create', 'Spouses\Controllers\SpouseController@create', ['auth'], 'spouse.add'],
['POST', '/members/{memberId}/spouses', 'Spouses\Controllers\SpouseController@store', ['auth'], 'spouse.add'], ['POST', '/members/{memberId}/spouses', 'Spouses\Controllers\SpouseController@store', ['auth', 'csrf'], 'spouse.add'],
['GET', '/members/{memberId}/spouses/{id}', 'Spouses\Controllers\SpouseController@show', ['auth'], 'spouse.view'], ['GET', '/members/{memberId}/spouses/{id}', 'Spouses\Controllers\SpouseController@show', ['auth'], 'spouse.view'],
['GET', '/members/{memberId}/spouses/{id}/edit', 'Spouses\Controllers\SpouseController@edit', ['auth'], 'spouse.edit'], ['GET', '/members/{memberId}/spouses/{id}/edit', 'Spouses\Controllers\SpouseController@edit', ['auth'], 'spouse.edit'],
['POST', '/members/{memberId}/spouses/{id}', 'Spouses\Controllers\SpouseController@update', ['auth'], 'spouse.edit'], ['POST', '/members/{memberId}/spouses/{id}', 'Spouses\Controllers\SpouseController@update', ['auth', 'csrf'], 'spouse.edit'],
['POST', '/members/{memberId}/spouses/{id}/archive', 'Spouses\Controllers\SpouseController@archive', ['auth'], 'spouse.remove'], ['POST', '/members/{memberId}/spouses/{id}/archive', 'Spouses\Controllers\SpouseController@archive', ['auth', 'csrf'], 'spouse.remove'],
['POST', '/api/spouses/calculate-fee', 'Spouses\Controllers\SpouseController@calculateFee', ['auth'], 'spouse.add'], ['POST', '/api/spouses/calculate-fee', 'Spouses\Controllers\SpouseController@calculateFee', ['auth'], 'spouse.add'],
]; ];
\ No newline at end of file
...@@ -4,8 +4,8 @@ declare(strict_types=1); ...@@ -4,8 +4,8 @@ declare(strict_types=1);
return [ return [
['GET', '/subscriptions', 'Subscriptions\Controllers\SubscriptionController@index', ['auth'], 'subscription.view'], ['GET', '/subscriptions', 'Subscriptions\Controllers\SubscriptionController@index', ['auth'], 'subscription.view'],
['GET', '/subscriptions/batch-generate', 'Subscriptions\Controllers\SubscriptionController@batchGenerateForm', ['auth'], 'subscription.generate_batch'], ['GET', '/subscriptions/batch-generate', 'Subscriptions\Controllers\SubscriptionController@batchGenerateForm', ['auth'], 'subscription.generate_batch'],
['POST', '/subscriptions/batch-generate', 'Subscriptions\Controllers\SubscriptionController@batchGenerate', ['auth'], 'subscription.generate_batch'], ['POST', '/subscriptions/batch-generate', 'Subscriptions\Controllers\SubscriptionController@batchGenerate', ['auth', 'csrf'], 'subscription.generate_batch'],
['GET', '/members/{memberId}/subscriptions', 'Subscriptions\Controllers\SubscriptionController@memberSubscriptions',['auth'], 'subscription.view'], ['GET', '/members/{memberId}/subscriptions', 'Subscriptions\Controllers\SubscriptionController@memberSubscriptions',['auth'], 'subscription.view'],
['POST', '/subscriptions/{id}/pay', 'Subscriptions\Controllers\SubscriptionController@pay', ['auth'], 'subscription.collect'], ['POST', '/subscriptions/{id}/pay', 'Subscriptions\Controllers\SubscriptionController@pay', ['auth', 'csrf'], 'subscription.collect'],
['POST', '/subscriptions/{id}/exempt', 'Subscriptions\Controllers\SubscriptionController@exempt', ['auth'], 'subscription.exempt'], ['POST', '/subscriptions/{id}/exempt', 'Subscriptions\Controllers\SubscriptionController@exempt', ['auth', 'csrf'], 'subscription.exempt'],
]; ];
\ No newline at end of file
...@@ -4,11 +4,11 @@ declare(strict_types=1); ...@@ -4,11 +4,11 @@ declare(strict_types=1);
return [ return [
['GET', '/support', 'Support\Controllers\TicketController@index', ['auth'], 'support.view'], ['GET', '/support', 'Support\Controllers\TicketController@index', ['auth'], 'support.view'],
['GET', '/support/create', 'Support\Controllers\TicketController@create', ['auth'], 'support.create'], ['GET', '/support/create', 'Support\Controllers\TicketController@create', ['auth'], 'support.create'],
['POST', '/support', 'Support\Controllers\TicketController@store', ['auth'], 'support.create'], ['POST', '/support', 'Support\Controllers\TicketController@store', ['auth', 'csrf'], 'support.create'],
['GET', '/support/{id:\d+}', 'Support\Controllers\TicketController@show', ['auth'], 'support.view'], ['GET', '/support/{id:\d+}', 'Support\Controllers\TicketController@show', ['auth'], 'support.view'],
['POST', '/support/{id:\d+}/reply', 'Support\Controllers\TicketController@reply', ['auth'], 'support.reply'], ['POST', '/support/{id:\d+}/reply', 'Support\Controllers\TicketController@reply', ['auth', 'csrf'], 'support.reply'],
['POST', '/support/{id:\d+}/close', 'Support\Controllers\TicketController@close', ['auth'], 'support.close'], ['POST', '/support/{id:\d+}/close', 'Support\Controllers\TicketController@close', ['auth', 'csrf'], 'support.close'],
['POST', '/support/{id:\d+}/reopen', 'Support\Controllers\TicketController@reopen', ['auth'], 'support.manage'], ['POST', '/support/{id:\d+}/reopen', 'Support\Controllers\TicketController@reopen', ['auth', 'csrf'], 'support.manage'],
['POST', '/support/{id:\d+}/assign', 'Support\Controllers\TicketController@assign', ['auth'], 'support.assign'], ['POST', '/support/{id:\d+}/assign', 'Support\Controllers\TicketController@assign', ['auth', 'csrf'], 'support.assign'],
['GET', '/support/attachments/{id:\d+}', 'Support\Controllers\TicketController@download', ['auth'], 'support.view'], ['GET', '/support/attachments/{id:\d+}', 'Support\Controllers\TicketController@download', ['auth'], 'support.view'],
]; ];
...@@ -4,7 +4,7 @@ declare(strict_types=1); ...@@ -4,7 +4,7 @@ declare(strict_types=1);
return [ return [
['GET', '/temporary', 'Temporary\Controllers\TemporaryController@index', ['auth'], 'temp.view'], ['GET', '/temporary', 'Temporary\Controllers\TemporaryController@index', ['auth'], 'temp.view'],
['GET', '/members/{memberId}/temporary/create', 'Temporary\Controllers\TemporaryController@create', ['auth'], 'temp.add'], ['GET', '/members/{memberId}/temporary/create', 'Temporary\Controllers\TemporaryController@create', ['auth'], 'temp.add'],
['POST', '/members/{memberId}/temporary', 'Temporary\Controllers\TemporaryController@store', ['auth'], 'temp.add'], ['POST', '/members/{memberId}/temporary', 'Temporary\Controllers\TemporaryController@store', ['auth', 'csrf'], 'temp.add'],
['GET', '/members/{memberId}/temporary/{id}', 'Temporary\Controllers\TemporaryController@show', ['auth'], 'temp.view'], ['GET', '/members/{memberId}/temporary/{id}', 'Temporary\Controllers\TemporaryController@show', ['auth'], 'temp.view'],
['POST', '/members/{memberId}/temporary/{id}/archive', 'Temporary\Controllers\TemporaryController@archive', ['auth'], 'temp.remove'], ['POST', '/members/{memberId}/temporary/{id}/archive', 'Temporary\Controllers\TemporaryController@archive', ['auth', 'csrf'], 'temp.remove'],
]; ];
\ No newline at end of file
...@@ -4,12 +4,12 @@ declare(strict_types=1); ...@@ -4,12 +4,12 @@ declare(strict_types=1);
return [ return [
['GET', '/transfers', 'Transfers\Controllers\TransferController@index', ['auth'], 'transfer.view'], ['GET', '/transfers', 'Transfers\Controllers\TransferController@index', ['auth'], 'transfer.view'],
['GET', '/transfers/create/{memberId}', 'Transfers\Controllers\TransferController@create', ['auth'], 'transfer.initiate'], ['GET', '/transfers/create/{memberId}', 'Transfers\Controllers\TransferController@create', ['auth'], 'transfer.initiate'],
['POST', '/transfers/store/{memberId}', 'Transfers\Controllers\TransferController@store', ['auth'], 'transfer.initiate'], ['POST', '/transfers/store/{memberId}', 'Transfers\Controllers\TransferController@store', ['auth', 'csrf'], 'transfer.initiate'],
['GET', '/transfers/{id}', 'Transfers\Controllers\TransferController@show', ['auth'], 'transfer.view'], ['GET', '/transfers/{id}', 'Transfers\Controllers\TransferController@show', ['auth'], 'transfer.view'],
['POST', '/transfers/{id}/pay', 'Transfers\Controllers\TransferController@pay', ['auth'], 'payment.collect'], ['POST', '/transfers/{id}/pay', 'Transfers\Controllers\TransferController@pay', ['auth', 'csrf'], 'payment.collect'],
['POST', '/transfers/{id}/approve', 'Transfers\Controllers\TransferController@approve', ['auth'], 'transfer.approve'], ['POST', '/transfers/{id}/approve', 'Transfers\Controllers\TransferController@approve', ['auth', 'csrf'], 'transfer.approve'],
['POST', '/transfers/{id}/reject', 'Transfers\Controllers\TransferController@reject', ['auth'], 'transfer.approve'], ['POST', '/transfers/{id}/reject', 'Transfers\Controllers\TransferController@reject', ['auth', 'csrf'], 'transfer.approve'],
['POST', '/transfers/{id}/complete', 'Transfers\Controllers\TransferController@complete',['auth'], 'transfer.approve'], ['POST', '/transfers/{id}/complete', 'Transfers\Controllers\TransferController@complete',['auth', 'csrf'], 'transfer.approve'],
['GET', '/transfers/preview/{memberId}', 'Transfers\Controllers\TransferController@preview', ['auth'], 'transfer.initiate'], ['GET', '/transfers/preview/{memberId}', 'Transfers\Controllers\TransferController@preview', ['auth'], 'transfer.initiate'],
['POST', '/api/transfers/calculate-fee', 'Transfers\Controllers\TransferController@calculateFee', ['auth'], 'transfer.initiate'], ['POST', '/api/transfers/calculate-fee', 'Transfers\Controllers\TransferController@calculateFee', ['auth'], 'transfer.initiate'],
]; ];
\ No newline at end of file
...@@ -4,10 +4,10 @@ declare(strict_types=1); ...@@ -4,10 +4,10 @@ declare(strict_types=1);
return [ return [
['GET', '/waivers', 'Waiver\Controllers\WaiverController@index', ['auth'], 'waiver.view'], ['GET', '/waivers', 'Waiver\Controllers\WaiverController@index', ['auth'], 'waiver.view'],
['GET', '/waivers/create/{memberId}', 'Waiver\Controllers\WaiverController@create', ['auth'], 'waiver.initiate'], ['GET', '/waivers/create/{memberId}', 'Waiver\Controllers\WaiverController@create', ['auth'], 'waiver.initiate'],
['POST', '/waivers/store/{memberId}', 'Waiver\Controllers\WaiverController@store', ['auth'], 'waiver.initiate'], ['POST', '/waivers/store/{memberId}', 'Waiver\Controllers\WaiverController@store', ['auth', 'csrf'], 'waiver.initiate'],
['GET', '/waivers/{id}', 'Waiver\Controllers\WaiverController@show', ['auth'], 'waiver.view'], ['GET', '/waivers/{id}', 'Waiver\Controllers\WaiverController@show', ['auth'], 'waiver.view'],
['POST', '/waivers/{id}/pay', 'Waiver\Controllers\WaiverController@pay', ['auth'], 'payment.collect'], ['POST', '/waivers/{id}/pay', 'Waiver\Controllers\WaiverController@pay', ['auth', 'csrf'], 'payment.collect'],
['POST', '/waivers/{id}/approve', 'Waiver\Controllers\WaiverController@approve', ['auth'], 'waiver.approve'], ['POST', '/waivers/{id}/approve', 'Waiver\Controllers\WaiverController@approve', ['auth', 'csrf'], 'waiver.approve'],
['POST', '/waivers/{id}/reject', 'Waiver\Controllers\WaiverController@reject', ['auth'], 'waiver.approve'], ['POST', '/waivers/{id}/reject', 'Waiver\Controllers\WaiverController@reject', ['auth', 'csrf'], 'waiver.approve'],
['POST', '/waivers/{id}/complete', 'Waiver\Controllers\WaiverController@complete',['auth'], 'waiver.approve'], ['POST', '/waivers/{id}/complete', 'Waiver\Controllers\WaiverController@complete',['auth', 'csrf'], 'waiver.approve'],
]; ];
\ No newline at end of file
<?php
declare(strict_types=1);
$cfg = $config[$status] ?? ['label_ar' => $status, 'color' => '#6B7280'];
?>
<span style="color:<?= e($cfg['color']) ?>;font-weight:600;font-size:13px;"><?= e($cfg['label_ar']) ?></span>
...@@ -189,5 +189,8 @@ window.addEventListener('load', function() { ...@@ -189,5 +189,8 @@ window.addEventListener('load', function() {
}); });
</script> </script>
<?= $__template->yield('scripts', '') ?> <?= $__template->yield('scripts', '') ?>
<?php if ($app->isDebug()): ?>
<?= \App\Core\DebugBar::render() ?>
<?php endif; ?>
</body> </body>
</html> </html>
...@@ -18,8 +18,7 @@ if (PHP_SAPI !== 'cli') { ...@@ -18,8 +18,7 @@ if (PHP_SAPI !== 'cli') {
die('CLI only.'); die('CLI only.');
} }
require_once __DIR__ . '/app/Core/Autoloader.php'; require_once __DIR__ . '/vendor/autoload.php';
\App\Core\Autoloader::register();
// Load .env // Load .env
$envFile = __DIR__ . '/.env'; $envFile = __DIR__ . '/.env';
...@@ -147,6 +146,32 @@ switch ($command) { ...@@ -147,6 +146,32 @@ switch ($command) {
} }
break; break;
case 'routes':
$app = \App\Core\App::getInstance();
$app->boot();
$routes = [];
$modulesDir = __DIR__ . '/app/Modules';
$routeFiles = glob($modulesDir . '/*/Routes.php');
sort($routeFiles);
foreach ($routeFiles as $file) {
$moduleRoutes = require $file;
if (is_array($moduleRoutes)) {
foreach ($moduleRoutes as $r) {
if (is_array($r) && count($r) >= 3) {
$routes[] = $r;
}
}
}
}
echo str_pad('METHOD', 8) . str_pad('PATH', 50) . str_pad('HANDLER', 55) . str_pad('MIDDLEWARE', 20) . "PERMISSION\n";
echo str_repeat('─', 145) . "\n";
foreach ($routes as $r) {
$mw = implode(',', $r[3] ?? []);
echo str_pad($r[0], 8) . str_pad($r[1], 50) . str_pad($r[2], 55) . str_pad($mw, 20) . ($r[4] ?? '') . "\n";
}
echo "\nTotal: " . count($routes) . " routes\n";
break;
case 'help': case 'help':
default: default:
echo "THE CLUB ERP — CLI Commands\n"; echo "THE CLUB ERP — CLI Commands\n";
...@@ -157,6 +182,7 @@ switch ($command) { ...@@ -157,6 +182,7 @@ switch ($command) {
echo " php cli.php seed Run all pending seeds\n"; echo " php cli.php seed Run all pending seeds\n";
echo " php cli.php seed:run <Name> Run specific seed\n"; echo " php cli.php seed:run <Name> Run specific seed\n";
echo " php cli.php cron Run background jobs\n"; echo " php cli.php cron Run background jobs\n";
echo " php cli.php routes List all routes\n";
echo " php cli.php help Show this help\n"; echo " php cli.php help Show this help\n";
break; break;
} }
......
{
"name": "club/erp",
"type": "project",
"require": {
"php": ">=8.1"
},
"require-dev": {
"phpunit/phpunit": "^10.5",
"phpstan/phpstan": "^1.10",
"friendsofphp/php-cs-fixer": "^3.0"
},
"autoload": {
"psr-4": {
"App\\Core\\": "app/Core/",
"App\\Modules\\": "app/Modules/",
"App\\Middleware\\": "app/Middleware/",
"App\\Shared\\": "app/Shared/"
},
"files": ["app/Core/Helpers.php"]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
}
}
This diff is collapsed.
...@@ -7,8 +7,7 @@ declare(strict_types=1); ...@@ -7,8 +7,7 @@ declare(strict_types=1);
*/ */
$basePath = dirname(__DIR__); $basePath = dirname(__DIR__);
require_once $basePath . '/app/Core/Autoloader.php'; require_once $basePath . '/vendor/autoload.php';
\App\Core\Autoloader::register();
$app = \App\Core\App::getInstance(); $app = \App\Core\App::getInstance();
$app->boot(); $app->boot();
......
...@@ -61,8 +61,7 @@ echo "========================================\n"; ...@@ -61,8 +61,7 @@ echo "========================================\n";
chdir('/var/www/html'); chdir('/var/www/html');
require_once '/var/www/html/app/Core/Autoloader.php'; require_once '/var/www/html/vendor/autoload.php';
\App\Core\Autoloader::register();
// Load .env // Load .env
$envFile = '/var/www/html/.env'; $envFile = '/var/www/html/.env';
......
parameters:
level: 3
paths:
- app
excludePaths:
- app/Modules/*/Views/*
- app/Core/Autoloader.php
ignoreErrors:
- '#Access to an undefined property object::\$\w+#'
- '#Call to an undefined method object::\w+#'
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>
<php>
<env name="DB_NAME" value="club_erp_test"/>
<env name="APP_ENV" value="testing"/>
</php>
</phpunit>
...@@ -2097,3 +2097,42 @@ code { ...@@ -2097,3 +2097,42 @@ code {
display: block; display: block;
opacity: 1; opacity: 1;
} }
/* ══════════════════════════════════════════════════
UTILITY CLASSES
══════════════════════════════════════════════════ */
.flex { display: flex; }
.flex-wrap { flex-wrap: wrap; }
.gap-xs { gap: 5px; }
.gap-sm { gap: 10px; }
.gap-md { gap: 15px; }
.gap-lg { gap: 20px; }
.items-center { align-items: center; }
.items-end { align-items: flex-end; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
.text-xs { font-size: 11px; }
.text-sm { font-size: 12px; }
.text-base { font-size: 13px; }
.text-lg { font-size: 16px; }
.text-muted { color: var(--text-muted); }
.text-success { color: var(--success); }
.text-danger { color: var(--danger); }
.text-warning { color: var(--warning); }
.text-info { color: var(--info); }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
.ltr { direction: ltr; text-align: right; }
.min-w-120 { min-width: 120px; }
.min-w-250 { min-width: 250px; }
.mb-xs { margin-bottom: 5px; }
.mb-sm { margin-bottom: 10px; }
.mb-md { margin-bottom: 20px; }
.mb-lg { margin-bottom: 30px; }
.mt-sm { margin-top: 10px; }
.mt-md { margin-top: 20px; }
.p-sm { padding: 10px; }
.p-md { padding: 15px; }
.p-lg { padding: 20px; }
.w-full { width: 100%; }
.hidden { display: none; }
...@@ -38,12 +38,14 @@ if (file_exists($envFile)) { ...@@ -38,12 +38,14 @@ if (file_exists($envFile)) {
} }
} }
// ── Autoloader ── // ── Autoloader (Composer) ──
require_once BASE_PATH . '/app/Core/Autoloader.php'; require_once BASE_PATH . '/vendor/autoload.php';
App\Core\Autoloader::register();
// ── Helpers (must be loaded before Config since config files call env()) ── // ── Security headers ──
require_once BASE_PATH . '/app/Core/Helpers.php'; header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
// ── Exception handler ── // ── Exception handler ──
$exHandler = new App\Core\ExceptionHandler(); $exHandler = new App\Core\ExceptionHandler();
...@@ -51,6 +53,7 @@ $exHandler->register(); ...@@ -51,6 +53,7 @@ $exHandler->register();
// ── Boot application ── // ── Boot application ──
try { try {
App\Core\DebugBar::init();
$app = App\Core\App::getInstance(); $app = App\Core\App::getInstance();
$app->boot(); $app->boot();
......
<?php
declare(strict_types=1);
namespace Tests;
use App\Core\Database;
abstract class DatabaseTestCase extends TestCase
{
protected Database $db;
protected function setUp(): void
{
parent::setUp();
$this->db = new Database(
$_ENV['DB_HOST'] ?? '127.0.0.1',
(int) ($_ENV['DB_PORT'] ?? 3306),
$_ENV['DB_NAME'] ?? 'club_erp_test',
$_ENV['DB_USER'] ?? 'root',
$_ENV['DB_PASS'] ?? ''
);
$this->db->beginTransaction();
}
protected function tearDown(): void
{
$this->db->rollBack();
parent::tearDown();
}
}
<?php
declare(strict_types=1);
namespace Tests;
use PHPUnit\Framework\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
}
<?php
declare(strict_types=1);
namespace Tests\Unit\Core;
use App\Core\QueryBuilder;
use Tests\TestCase;
class QueryBuilderTest extends TestCase
{
private QueryBuilder $qb;
protected function setUp(): void
{
parent::setUp();
$this->qb = new QueryBuilder($this->createMockDb());
}
private function createMockDb(): \App\Core\Database
{
$mock = $this->createMock(\App\Core\Database::class);
return $mock;
}
public function testBasicSelect(): void
{
$sql = $this->qb->table('members')->noSoftDelete()->toSql();
$this->assertSame('SELECT * FROM `members`', $sql);
}
public function testSelectSpecificColumns(): void
{
$sql = $this->qb->table('members')->select(['id', 'name'])->noSoftDelete()->toSql();
$this->assertSame('SELECT id, name FROM `members`', $sql);
}
public function testWhereClause(): void
{
$sql = $this->qb->table('members')->noSoftDelete()->where('status', '=', 'active')->toSql();
$this->assertSame('SELECT * FROM `members` WHERE `status` = ?', $sql);
}
public function testMultipleWheres(): void
{
$sql = $this->qb->table('members')->noSoftDelete()
->where('status', '=', 'active')
->where('branch_id', '=', 1)
->toSql();
$this->assertSame('SELECT * FROM `members` WHERE `status` = ? AND `branch_id` = ?', $sql);
}
public function testOrWhere(): void
{
$sql = $this->qb->table('members')->noSoftDelete()
->where('status', '=', 'active')
->orWhere('status', '=', 'pending')
->toSql();
$this->assertSame('SELECT * FROM `members` WHERE `status` = ? OR `status` = ?', $sql);
}
public function testWhereIn(): void
{
$sql = $this->qb->table('members')->noSoftDelete()
->whereIn('id', [1, 2, 3])
->toSql();
$this->assertSame('SELECT * FROM `members` WHERE `id` IN (?, ?, ?)', $sql);
}
public function testWhereInEmpty(): void
{
$sql = $this->qb->table('members')->noSoftDelete()
->whereIn('id', [])
->toSql();
$this->assertSame('SELECT * FROM `members` WHERE 1 = 0', $sql);
}
public function testWhereNull(): void
{
$sql = $this->qb->table('members')->noSoftDelete()
->whereNull('deleted_at')
->toSql();
$this->assertSame('SELECT * FROM `members` WHERE `deleted_at` IS NULL', $sql);
}
public function testWhereNotNull(): void
{
$sql = $this->qb->table('members')->noSoftDelete()
->whereNotNull('email')
->toSql();
$this->assertSame('SELECT * FROM `members` WHERE `email` IS NOT NULL', $sql);
}
public function testWhereBetween(): void
{
$sql = $this->qb->table('payments')->noSoftDelete()
->whereBetween('amount', 100, 500)
->toSql();
$this->assertSame('SELECT * FROM `payments` WHERE `amount` BETWEEN ? AND ?', $sql);
}
public function testJoin(): void
{
$sql = $this->qb->table('members')->noSoftDelete()
->join('branches', 'members.branch_id = branches.id')
->toSql();
$this->assertSame('SELECT * FROM `members` INNER JOIN `branches` ON members.branch_id = branches.id', $sql);
}
public function testLeftJoin(): void
{
$sql = $this->qb->table('members')->noSoftDelete()
->leftJoin('photos', 'members.id = photos.member_id')
->toSql();
$this->assertSame('SELECT * FROM `members` LEFT JOIN `photos` ON members.id = photos.member_id', $sql);
}
public function testOrderBy(): void
{
$sql = $this->qb->table('members')->noSoftDelete()
->orderBy('name', 'ASC')
->toSql();
$this->assertSame('SELECT * FROM `members` ORDER BY `name` ASC', $sql);
}
public function testOrderByDesc(): void
{
$sql = $this->qb->table('members')->noSoftDelete()
->orderBy('created_at', 'DESC')
->toSql();
$this->assertSame('SELECT * FROM `members` ORDER BY `created_at` DESC', $sql);
}
public function testGroupBy(): void
{
$sql = $this->qb->table('payments')->noSoftDelete()
->select('branch_id, SUM(amount) as total')
->groupBy('branch_id')
->toSql();
$this->assertSame('SELECT branch_id, SUM(amount) as total FROM `payments` GROUP BY `branch_id`', $sql);
}
public function testHaving(): void
{
$sql = $this->qb->table('payments')->noSoftDelete()
->select('branch_id, SUM(amount) as total')
->groupBy('branch_id')
->having('total > 1000')
->toSql();
$this->assertSame('SELECT branch_id, SUM(amount) as total FROM `payments` GROUP BY `branch_id` HAVING total > 1000', $sql);
}
public function testLimitOffset(): void
{
$sql = $this->qb->table('members')->noSoftDelete()
->limit(10)
->offset(20)
->toSql();
$this->assertSame('SELECT * FROM `members` LIMIT 10 OFFSET 20', $sql);
}
public function testSoftDeleteFilterApplied(): void
{
$sql = $this->qb->table('members')->toSql();
$this->assertStringContainsString('is_archived', $sql);
}
public function testWithArchivedRemovesFilter(): void
{
$sql = $this->qb->table('members')->withArchived()->toSql();
$this->assertStringNotContainsString('is_archived', $sql);
}
public function testOnlyArchived(): void
{
$sql = $this->qb->table('members')->onlyArchived()->toSql();
$this->assertStringContainsString('`is_archived` = 1', $sql);
}
public function testWhenTrue(): void
{
$sql = $this->qb->table('members')->noSoftDelete()
->when(true, fn($q) => $q->where('status', '=', 'active'))
->toSql();
$this->assertStringContainsString('`status` = ?', $sql);
}
public function testWhenFalse(): void
{
$sql = $this->qb->table('members')->noSoftDelete()
->when(false, fn($q) => $q->where('status', '=', 'active'))
->toSql();
$this->assertStringNotContainsString('status', $sql);
}
public function testImmutability(): void
{
$base = $this->qb->table('members')->noSoftDelete();
$withWhere = $base->where('status', '=', 'active');
$this->assertSame('SELECT * FROM `members`', $base->toSql());
$this->assertSame('SELECT * FROM `members` WHERE `status` = ?', $withWhere->toSql());
}
}
<?php
declare(strict_types=1);
namespace Tests\Unit\Core;
use Tests\TestCase;
class RouterTest extends TestCase
{
private function matchPath(string $routePath, string $requestPath): array|false
{
$routePath = '/' . trim($routePath, '/');
$requestPath = '/' . trim($requestPath, '/');
if ($routePath === $requestPath) {
return [];
}
$pattern = preg_replace_callback('/\{(\w+)(?::([^}]+))?\}/', function ($matches) {
$name = $matches[1];
$constraint = $matches[2] ?? '[^/]+';
return '(?P<' . $name . '>' . $constraint . ')';
}, $routePath);
$pattern = '#^' . $pattern . '$#';
if (preg_match($pattern, $requestPath, $matches)) {
$params = [];
foreach ($matches as $key => $value) {
if (is_string($key)) {
$params[$key] = $value;
}
}
return $params;
}
return false;
}
public function testExactMatch(): void
{
$this->assertSame([], $this->matchPath('/dashboard', '/dashboard'));
}
public function testExactMatchWithTrailingSlash(): void
{
$this->assertSame([], $this->matchPath('/dashboard/', '/dashboard'));
}
public function testNoMatch(): void
{
$this->assertFalse($this->matchPath('/dashboard', '/members'));
}
public function testSingleParam(): void
{
$result = $this->matchPath('/members/{id}', '/members/42');
$this->assertSame(['id' => '42'], $result);
}
public function testParamWithConstraint(): void
{
$result = $this->matchPath('/members/{id:\d+}', '/members/42');
$this->assertSame(['id' => '42'], $result);
}
public function testParamConstraintRejects(): void
{
$result = $this->matchPath('/members/{id:\d+}', '/members/abc');
$this->assertFalse($result);
}
public function testMultipleParams(): void
{
$result = $this->matchPath('/branches/{branch}/members/{id}', '/branches/1/members/42');
$this->assertSame(['branch' => '1', 'id' => '42'], $result);
}
public function testRootPath(): void
{
$this->assertSame([], $this->matchPath('/', '/'));
}
public function testNestedPathNoMatch(): void
{
$this->assertFalse($this->matchPath('/members', '/members/42'));
}
public function testParamWithSlug(): void
{
$result = $this->matchPath('/reports/{type}', '/reports/daily-summary');
$this->assertSame(['type' => 'daily-summary'], $result);
}
}
<?php
declare(strict_types=1);
namespace Tests\Unit\Core;
use App\Core\Validator;
use Tests\TestCase;
class ValidatorTest extends TestCase
{
private Validator $validator;
protected function setUp(): void
{
parent::setUp();
$this->validator = new Validator();
}
public function testRequiredPassesWithValue(): void
{
$result = $this->validator->validate(['name' => 'test'], ['name' => 'required']);
$this->assertTrue($result->passes());
$this->assertSame(['name' => 'test'], $result->validated());
}
public function testRequiredFailsWithEmpty(): void
{
$result = $this->validator->validate(['name' => ''], ['name' => 'required']);
$this->assertTrue($result->fails());
$this->assertArrayHasKey('name', $result->errors());
}
public function testRequiredFailsWithNull(): void
{
$result = $this->validator->validate([], ['name' => 'required']);
$this->assertTrue($result->fails());
}
public function testStringPasses(): void
{
$result = $this->validator->validate(['name' => 'hello'], ['name' => 'string']);
$this->assertTrue($result->passes());
}
public function testIntegerPasses(): void
{
$result = $this->validator->validate(['age' => '25'], ['age' => 'integer']);
$this->assertTrue($result->passes());
}
public function testIntegerFailsWithFloat(): void
{
$result = $this->validator->validate(['age' => '25.5'], ['age' => 'integer']);
$this->assertTrue($result->fails());
}
public function testNumericPasses(): void
{
$result = $this->validator->validate(['price' => '99.99'], ['price' => 'numeric']);
$this->assertTrue($result->passes());
}
public function testEmailPasses(): void
{
$result = $this->validator->validate(['email' => 'test@example.com'], ['email' => 'email']);
$this->assertTrue($result->passes());
}
public function testEmailFails(): void
{
$result = $this->validator->validate(['email' => 'not-an-email'], ['email' => 'email']);
$this->assertTrue($result->fails());
}
public function testDatePasses(): void
{
$result = $this->validator->validate(['dob' => '1990-05-15'], ['dob' => 'date']);
$this->assertTrue($result->passes());
}
public function testDateFailsInvalid(): void
{
$result = $this->validator->validate(['dob' => '2024-13-45'], ['dob' => 'date']);
$this->assertTrue($result->fails());
}
public function testMinStringLength(): void
{
$result = $this->validator->validate(['name' => 'ab'], ['name' => 'min:3']);
$this->assertTrue($result->fails());
}
public function testMaxStringLength(): void
{
$result = $this->validator->validate(['name' => 'abcdef'], ['name' => 'max:5']);
$this->assertTrue($result->fails());
}
public function testInPasses(): void
{
$result = $this->validator->validate(['status' => 'active'], ['status' => 'in:active,inactive']);
$this->assertTrue($result->passes());
}
public function testInFails(): void
{
$result = $this->validator->validate(['status' => 'deleted'], ['status' => 'in:active,inactive']);
$this->assertTrue($result->fails());
}
public function testNullableSkipsValidation(): void
{
$result = $this->validator->validate(['phone' => ''], ['phone' => 'nullable|phone_eg']);
$this->assertTrue($result->passes());
}
public function testNullableAllowsNull(): void
{
$result = $this->validator->validate([], ['phone' => 'nullable|phone_eg']);
$this->assertTrue($result->passes());
}
public function testPhoneEgPasses(): void
{
$result = $this->validator->validate(['phone' => '01012345678'], ['phone' => 'phone_eg']);
$this->assertTrue($result->passes());
}
public function testPhoneEgFails(): void
{
$result = $this->validator->validate(['phone' => '123'], ['phone' => 'phone_eg']);
$this->assertTrue($result->fails());
}
public function testNationalIdPasses(): void
{
$result = $this->validator->validate(['nid' => '29005151234567'], ['nid' => 'national_id']);
$this->assertTrue($result->passes());
}
public function testNationalIdFailsWrongLength(): void
{
$result = $this->validator->validate(['nid' => '123'], ['nid' => 'national_id']);
$this->assertTrue($result->fails());
}
public function testBetweenPasses(): void
{
$result = $this->validator->validate(['age' => '25'], ['age' => 'between:18,60']);
$this->assertTrue($result->passes());
}
public function testBetweenFails(): void
{
$result = $this->validator->validate(['age' => '10'], ['age' => 'between:18,60']);
$this->assertTrue($result->fails());
}
public function testConfirmedPasses(): void
{
$data = ['password' => 'secret', 'password_confirmation' => 'secret'];
$result = $this->validator->validate($data, ['password' => 'confirmed']);
$this->assertTrue($result->passes());
}
public function testConfirmedFails(): void
{
$data = ['password' => 'secret', 'password_confirmation' => 'different'];
$result = $this->validator->validate($data, ['password' => 'confirmed']);
$this->assertTrue($result->fails());
}
public function testMultipleRulesAllPass(): void
{
$result = $this->validator->validate(['name' => 'Ahmed'], ['name' => 'required|string|min:2|max:50']);
$this->assertTrue($result->passes());
}
public function testMultipleFieldsValidation(): void
{
$data = ['name' => 'Ahmed', 'email' => 'bad'];
$rules = ['name' => 'required|string', 'email' => 'required|email'];
$result = $this->validator->validate($data, $rules);
$this->assertTrue($result->fails());
$this->assertArrayNotHasKey('name', $result->errors());
$this->assertArrayHasKey('email', $result->errors());
}
public function testDigitsPasses(): void
{
$result = $this->validator->validate(['code' => '1234'], ['code' => 'digits:4']);
$this->assertTrue($result->passes());
}
public function testDigitsFails(): void
{
$result = $this->validator->validate(['code' => '12'], ['code' => 'digits:4']);
$this->assertTrue($result->fails());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment