Commit 56b6d45d authored by Administrator's avatar Administrator

Update 3 files via Son of Anton

parent c74a9e19
...@@ -7,16 +7,20 @@ final class App ...@@ -7,16 +7,20 @@ final class App
{ {
private static ?App $instance = null; private static ?App $instance = null;
private Config $config; private ?Database $db = null;
private Database $database; private ?Session $session = null;
private Session $session; private ?Router $router = null;
private Router $router; private array $config = [];
private ?object $currentEmployee = null; private ?object $currentEmployee = null;
private ?array $currentBranch = null; private ?array $currentBranch = null;
private array $bindings = []; private array $bindings = [];
private string $basePath;
private bool $booted = false; private bool $booted = false;
private function __construct() {} private function __construct()
{
$this->basePath = dirname(__DIR__, 2);
}
public static function getInstance(): self public static function getInstance(): self
{ {
...@@ -32,84 +36,200 @@ final class App ...@@ -32,84 +36,200 @@ final class App
return $this; return $this;
} }
// Set timezone
date_default_timezone_set('Africa/Cairo'); date_default_timezone_set('Africa/Cairo');
$this->config = new Config(); // Load .env
$this->config->loadAll(); $this->loadEnv();
$this->database = new Database( // Load config files
$this->config->get('database.host', '127.0.0.1'), $this->loadConfig();
(int) $this->config->get('database.port', '3306'),
$this->config->get('database.name', 'the_club_erp'),
$this->config->get('database.user', 'root'),
$this->config->get('database.pass', ''),
$this->config->get('database.charset', 'utf8mb4')
);
$this->session = new Session( // Initialize error handler
(int) $this->config->get('app.session_lifetime', 30) ExceptionHandler::register();
);
$this->session->start();
$this->config->loadFromDatabase($this->database); // Initialize database
$this->initDatabase();
$this->router = new Router(); // Initialize session
$this->initSession();
// Load DB config overrides
$this->loadDbConfigOverrides();
$this->discoverModuleBootstraps(); // Auto-discover and load module bootstraps
$this->discoverModuleRoutes(); $this->loadModuleBootstraps();
// Auto-discover and load module routes
$this->router = new Router();
$this->loadModuleRoutes();
$this->booted = true; $this->booted = true;
return $this; return $this;
} }
private function discoverModuleBootstraps(): void private function loadEnv(): void
{ {
$pattern = $this->basePath() . '/app/Modules/*/bootstrap.php'; $envFile = $this->basePath . '/.env';
$files = glob($pattern); if (!file_exists($envFile)) {
if ($files === false) {
return; return;
} }
sort($files);
foreach ($files as $file) { $lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
require_once $file; foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (str_contains($line, '=')) {
[$key, $value] = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
if (!isset($_ENV[$key])) {
$_ENV[$key] = $value;
putenv("{$key}={$value}");
}
}
} }
} }
private function discoverModuleRoutes(): void private function loadConfig(): void
{ {
$pattern = $this->basePath() . '/app/Modules/*/Routes.php'; $configDir = $this->basePath . '/config';
$files = glob($pattern); if (!is_dir($configDir)) {
return;
}
$files = glob($configDir . '/*.php');
if ($files === false) { if ($files === false) {
return; return;
} }
sort($files);
foreach ($files as $file) { foreach ($files as $file) {
$routes = require $file; $key = basename($file, '.php');
if (is_array($routes)) { $this->config[$key] = require $file;
foreach ($routes as $route) { }
$this->router->addRoute( }
$route[0],
$route[1], private function initDatabase(): void
$route[2], {
$route[3] ?? [], $this->db = new Database(
$route[4] ?? null (string) ($this->config['database']['host'] ?? env('DB_HOST', '127.0.0.1')),
); (int) ($this->config['database']['port'] ?? env('DB_PORT', 3306)),
(string) ($this->config['database']['name'] ?? env('DB_NAME', 'the_club_erp')),
(string) ($this->config['database']['user'] ?? env('DB_USER', 'root')),
(string) ($this->config['database']['pass'] ?? env('DB_PASS', '')),
(string) ($this->config['database']['charset'] ?? 'utf8mb4')
);
}
private function initSession(): void
{
$this->session = new Session();
}
private function loadDbConfigOverrides(): void
{
try {
if (!$this->db->tableExists('system_config')) {
return;
}
$rows = $this->db->select("SELECT config_key, config_value, config_type FROM system_config");
foreach ($rows as $row) {
$value = $row['config_value'];
switch ($row['config_type']) {
case 'integer':
$value = (int) $value;
break;
case 'float':
$value = (float) $value;
break;
case 'boolean':
$value = in_array(strtolower((string) $value), ['1', 'true', 'yes']);
break;
case 'json':
$value = json_decode($value, true) ?? $value;
break;
} }
// Store as dot notation override
$this->config['db_overrides'][$row['config_key']] = $value;
} }
} catch (\Throwable $e) {
// DB might not be ready yet — ignore
} }
} }
public function config(string $key = null, $default = null) /**
* Auto-discover and require all Modules/*/bootstrap.php files.
* This is where modules register their menu items, permissions, event listeners, etc.
*/
private function loadModuleBootstraps(): void
{ {
if ($key === null) { $modulesDir = $this->basePath . '/app/Modules';
return $this->config; if (!is_dir($modulesDir)) {
return;
}
$bootstrapFiles = glob($modulesDir . '/*/bootstrap.php');
if ($bootstrapFiles === false) {
return;
}
sort($bootstrapFiles); // Alphabetical order for deterministic loading
foreach ($bootstrapFiles as $file) {
try {
require_once $file;
} catch (\Throwable $e) {
Logger::warning('Failed to load bootstrap: ' . $file . ' — ' . $e->getMessage());
}
}
}
/**
* Auto-discover and merge all Modules/*/Routes.php files.
*/
private function loadModuleRoutes(): void
{
$modulesDir = $this->basePath . '/app/Modules';
if (!is_dir($modulesDir)) {
return;
}
$routeFiles = glob($modulesDir . '/*/Routes.php');
if ($routeFiles === false) {
return;
}
sort($routeFiles);
foreach ($routeFiles as $file) {
try {
$routes = require $file;
if (is_array($routes)) {
foreach ($routes as $route) {
if (is_array($route) && count($route) >= 3) {
$this->router->addRoute(
$route[0], // method
$route[1], // path
$route[2], // controller@action
$route[3] ?? [], // middleware
$route[4] ?? null // permission
);
}
}
}
} catch (\Throwable $e) {
Logger::warning('Failed to load routes: ' . $file . ' — ' . $e->getMessage());
}
} }
return $this->config->get($key, $default);
} }
// ── Accessors ──
public function db(): Database public function db(): Database
{ {
return $this->database; return $this->db;
} }
public function session(): Session public function session(): Session
...@@ -122,53 +242,78 @@ final class App ...@@ -122,53 +242,78 @@ final class App
return $this->router; return $this->router;
} }
public function setCurrentEmployee(?object $employee): void public function config(string $key = null, $default = null)
{ {
$this->currentEmployee = $employee; if ($key === null) {
return $this->config;
}
// Check DB overrides first
if (isset($this->config['db_overrides'][$key])) {
return $this->config['db_overrides'][$key];
}
// Dot notation: 'app.name' → $this->config['app']['name']
$parts = explode('.', $key);
$value = $this->config;
foreach ($parts as $part) {
if (is_array($value) && array_key_exists($part, $value)) {
$value = $value[$part];
} else {
return $default;
}
}
return $value;
} }
public function currentEmployee(): ?object public function basePath(): string
{ {
return $this->currentEmployee; return $this->basePath;
} }
public function setCurrentBranch(?array $branch): void public function publicPath(): string
{ {
$this->currentBranch = $branch; return $this->basePath . '/public';
} }
public function currentBranch(): ?array public function storagePath(): string
{ {
return $this->currentBranch; return $this->basePath . '/storage';
} }
public function bind(string $key, $value): void // ── Employee Context ──
public function setCurrentEmployee(object $employee): void
{ {
$this->bindings[$key] = $value; $this->currentEmployee = $employee;
} }
public function resolve(string $key, $default = null) public function currentEmployee(): ?object
{ {
return $this->bindings[$key] ?? $default; return $this->currentEmployee;
} }
public function basePath(): string public function setCurrentBranch(?array $branch): void
{ {
return Autoloader::basePath(); $this->currentBranch = $branch;
} }
public function publicPath(): string public function currentBranch(): ?array
{ {
return $this->basePath() . '/public'; return $this->currentBranch;
} }
public function storagePath(): string // ── Simple Service Container ──
public function bind(string $key, $value): void
{ {
return $this->basePath() . '/storage'; $this->bindings[$key] = $value;
} }
public function isDebug(): bool public function resolve(string $key, $default = null)
{ {
return (bool) $this->config->get('app.debug', false); return $this->bindings[$key] ?? $default;
} }
} }
\ No newline at end of file
<?php
/**
* Main application layout — RTL Arabic-first.
* All authenticated pages use this layout.
*/
use App\Core\App;
use App\Core\CSRF;
$app = App::getInstance();
$employee = $app->currentEmployee();
$employeeName = $employee ? ($employee->full_name_ar ?? 'مستخدم') : 'زائر';
$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ar" dir="rtl"> <html lang="ar" dir="rtl">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="<?= \App\Core\CSRF::token() ?>"> <meta name="csrf-token" content="<?= e(CSRF::token()) ?>">
<title><?= $__template->yield('title', 'نادي النادي شيراتون') ?></title> <title><?= $__template->yield('title', 'لوحة التحكم') ?> — نادي النادي شيراتون</title>
<link rel="stylesheet" href="<?= url('assets/css/main.css') ?>"> <link rel="stylesheet" href="<?= url('assets/css/main.css') ?>">
<?= $__template->yield('styles', '') ?> <?= $__template->yield('styles', '') ?>
</head> </head>
<body> <body>
<aside class="sidebar" id="sidebar">
<?php $__template->include('Shared.Components.sidebar', ['app' => $app]); ?>
</aside>
<div class="main-wrapper">
<header class="topbar">
<?php $__template->include('Shared.Components.header', ['app' => $app]); ?>
</header>
<div class="content-area">
<div class="breadcrumb-area">
<?= $__template->yield('breadcrumbs', '') ?>
</div>
<div class="page-header-row"> <!-- Sidebar -->
<h1 class="page-title"><?= $__template->yield('title', '') ?></h1> <aside class="sidebar" id="sidebar">
<div class="page-actions"> <?php $__template->include('Shared.Components.sidebar'); ?>
<?= $__template->yield('page_actions', '') ?> </aside>
</div>
</div>
<?php $__template->include('Shared.Components.alerts', ['app' => $app]); ?> <!-- Main Wrapper -->
<div class="main-wrapper" id="main-wrapper">
<main class="main-content"> <!-- Top Header -->
<?= $__template->yield('content', '') ?> <header class="top-header">
</main> <div class="header-right">
<button class="sidebar-toggle-btn" onclick="toggleSidebar()"></button>
<div class="header-title">
<h1><?= $__template->yield('title', 'لوحة التحكم') ?></h1>
</div>
</div>
<div class="header-left">
<?= $__template->yield('page_actions', '') ?>
<div class="header-user">
<span class="header-user-name"><?= e($employeeName) ?></span>
<a href="/logout" class="header-logout" title="تسجيل الخروج">🚪</a>
</div>
</div> </div>
</header>
<!-- Alerts -->
<?php $__template->include('Shared.Components.alerts'); ?>
<footer class="footer"> <!-- Page Content -->
<span>نادي النادي شيراتون &copy; <?= date('Y') ?></span> <main class="page-content">
<span>الإصدار <?= e($app->config('app.version', '1.0.0')) ?></span> <?= $__template->yield('content', '') ?>
<span><?= arabic_date(today()) ?></span> </main>
</footer>
</div>
<div id="modal-container"></div> <!-- Footer -->
<div id="toast-container" class="toast-container"></div> <footer class="page-footer">
<span>نادي النادي شيراتون &copy; <?= date('Y') ?></span>
<span>الإصدار 1.0.0</span>
<span><?= arabic_date(date('Y-m-d')) ?></span>
</footer>
</div>
<script src="<?= url('assets/js/app.js') ?>"></script> <script src="<?= url('assets/js/app.js') ?>"></script>
<?= $__template->yield('scripts', '') ?> <script>
<?= $__template->stack('scripts') ?> function toggleSidebar() {
var sb = document.getElementById('sidebar');
var mw = document.getElementById('main-wrapper');
sb.classList.toggle('collapsed');
mw.classList.toggle('sidebar-collapsed');
}
function toggleSubmenu(el) {
var parent = el.parentElement;
var submenu = parent.querySelector('.sidebar-submenu');
if (submenu) {
var isOpen = submenu.style.display === 'block';
submenu.style.display = isOpen ? 'none' : 'block';
parent.classList.toggle('open', !isOpen);
}
}
</script>
<?= $__template->yield('scripts', '') ?>
</body> </body>
</html> </html>
\ No newline at end of file
/* ════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════
THE CLUB ERP — Complete RTL-First Design System ADD THESE TO THE END OF YOUR main.css
════════════════════════════════════════════════════════════ */ IF SIDEBAR STYLES ARE MISSING
═══════════════════════════════════════════ */
/* ── CSS Custom Properties ── */
:root {
--primary: #0D7377;
--primary-light: #14A3A8;
--primary-dark: #095355;
--bg: #FFFFFF;
--surface: #F5F7FA;
--text: #1A1A2E;
--text-secondary: #6B7280;
--border: #E5E7EB;
--success: #059669;
--warning: #D97706;
--error: #DC2626;
--info: #0284C7;
--shadow: 0 1px 3px rgba(0,0,0,0.1);
--shadow-md: 0 4px 6px rgba(0,0,0,0.1);
--radius: 8px;
--radius-sm: 4px;
--sidebar-w: 260px;
--topbar-h: 56px;
--font: 'Cairo', 'Segoe UI', Tahoma, sans-serif;
--font-mono: 'Courier New', monospace;
}
/* ── Reset & Base ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 14px; }
body { font-family: var(--font); color: var(--text); background: var(--surface); direction: rtl; line-height: 1.6; min-height: 100vh; }
a { color: var(--primary); text-decoration: none; }
a:hover { color: var(--primary-dark); }
img { max-width: 100%; }
h1 { font-size: 1.75rem; font-weight: 700; }
h2 { font-size: 1.5rem; font-weight: 700; }
h3 { font-size: 1.25rem; font-weight: 600; }
h4 { font-size: 1.1rem; font-weight: 600; }
h5 { font-size: 1rem; font-weight: 600; }
h6 { font-size: .875rem; font-weight: 600; }
/* ── Sidebar ── */ /* ── Sidebar ── */
.sidebar { position: fixed; top: 0; right: 0; width: var(--sidebar-w); height: 100vh; background: var(--primary-dark); color: #fff; overflow-y: auto; z-index: 100; transition: transform .3s ease; } .sidebar {
.sidebar-header { display: flex; align-items: center; justify-content: space-between; padding: 16px; border-bottom: 1px solid rgba(255,255,255,.1); } position: fixed;
.sidebar-brand { font-size: 1.2rem; font-weight: 700; color: #fff; } top: 0;
.sidebar-toggle { background: none; border: none; color: #fff; font-size: 1.2rem; cursor: pointer; display: none; } right: 0;
.sidebar-nav { padding: 8px 0; } width: 260px;
.sidebar-menu { list-style: none; } height: 100vh;
.sidebar-item { border-bottom: 1px solid rgba(255,255,255,.05); } background: #1A1A2E;
.sidebar-link { display: flex; align-items: center; padding: 10px 16px; color: rgba(255,255,255,.8); transition: all .2s; gap: 10px; } color: #E5E7EB;
.sidebar-link:hover, .sidebar-link.active { background: rgba(255,255,255,.1); color: #fff; } overflow-y: auto;
.sidebar-icon { width: 24px; text-align: center; font-size: 1rem; } overflow-x: hidden;
.sidebar-text { flex: 1; } z-index: 1000;
.sidebar-arrow { font-size: .7rem; transition: transform .2s; } transition: width 0.3s ease, transform 0.3s ease;
.sidebar-item.open .sidebar-arrow { transform: rotate(-90deg); } display: flex;
.sidebar-submenu { list-style: none; background: rgba(0,0,0,.15); } flex-direction: column;
.sidebar-sublink { display: block; padding: 8px 40px; color: rgba(255,255,255,.7); font-size: .9rem; transition: all .2s; } }
.sidebar-sublink:hover, .sidebar-sublink.active { color: #fff; background: rgba(255,255,255,.08); }
.sidebar.collapsed {
width: 0;
transform: translateX(260px);
}
.sidebar-header {
padding: 20px 15px;
border-bottom: 1px solid rgba(255,255,255,0.1);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.sidebar-brand {
font-size: 18px;
font-weight: 700;
color: #14b8a6;
letter-spacing: 1px;
}
.sidebar-toggle {
background: none;
border: none;
color: #9CA3AF;
font-size: 18px;
cursor: pointer;
padding: 5px;
}
.sidebar-toggle:hover {
color: #fff;
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 10px 0;
}
.sidebar-menu {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar-item {
margin: 2px 0;
}
.sidebar-link {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 20px;
color: #D1D5DB;
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
border-radius: 0;
position: relative;
}
.sidebar-link:hover {
background: rgba(255,255,255,0.08);
color: #fff;
}
.sidebar-link.active {
background: rgba(13, 115, 119, 0.3);
color: #14b8a6;
border-left: 3px solid #14b8a6;
}
.sidebar-icon {
font-size: 16px;
width: 24px;
text-align: center;
flex-shrink: 0;
}
.sidebar-text {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-arrow {
font-size: 10px;
transition: transform 0.2s ease;
color: #6B7280;
}
.sidebar-item.open > .sidebar-link .sidebar-arrow {
transform: rotate(-90deg);
}
.sidebar-submenu {
list-style: none;
padding: 0;
margin: 0;
background: rgba(0,0,0,0.15);
}
.sidebar-sublink {
display: block;
padding: 8px 20px 8px 45px;
color: #9CA3AF;
text-decoration: none;
font-size: 13px;
transition: all 0.2s ease;
}
.sidebar-sublink:hover {
color: #fff;
background: rgba(255,255,255,0.05);
}
.sidebar-sublink.active {
color: #14b8a6;
font-weight: 600;
}
/* ── Main Wrapper ── */ /* ── Main Wrapper ── */
.main-wrapper { margin-right: var(--sidebar-w); min-height: 100vh; display: flex; flex-direction: column; } .main-wrapper {
margin-right: 260px;
/* ── Topbar ── */ min-height: 100vh;
.topbar { position: sticky; top: 0; height: var(--topbar-h); background: var(--bg); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 20px; z-index: 50; box-shadow: var(--shadow); } display: flex;
.topbar-right, .topbar-left { display: flex; align-items: center; gap: 12px; } flex-direction: column;
.topbar-search-input { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 6px 12px; width: 280px; font-family: var(--font); } transition: margin-right 0.3s ease;
.topbar-btn { background: none; border: none; cursor: pointer; font-size: 1rem; padding: 4px 8px; border-radius: var(--radius-sm); color: var(--text-secondary); } background: #F3F4F6;
.topbar-btn:hover { background: var(--surface); color: var(--text); } }
.topbar-date { color: var(--text-secondary); font-size: .85rem; }
.topbar-branch { background: var(--primary); color: #fff; padding: 2px 10px; border-radius: 12px; font-size: .8rem; } .main-wrapper.sidebar-collapsed {
.topbar-username { font-weight: 600; color: var(--text); } margin-right: 0;
.topbar-logout { color: var(--error) !important; font-weight: 600; } }
.notif-badge { background: var(--error); color: #fff; border-radius: 50%; padding: 0 5px; font-size: .7rem; position: relative; top: -8px; }
.sidebar-mobile-toggle { display: none; } /* ── Top Header ── */
.top-header {
/* ── Content Area ── */ background: #fff;
.content-area { flex: 1; padding: 20px; } border-bottom: 1px solid #E5E7EB;
.page-header-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; } padding: 12px 25px;
.page-title { color: var(--text); margin: 0; } display: flex;
.page-actions { display: flex; gap: 8px; } justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.header-right {
display: flex;
align-items: center;
gap: 15px;
}
.header-left {
display: flex;
align-items: center;
gap: 15px;
}
.header-title h1 {
margin: 0;
font-size: 18px;
font-weight: 700;
color: #1A1A2E;
}
.header-user {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
color: #4B5563;
}
.header-user-name {
font-weight: 600;
}
.header-logout {
text-decoration: none;
font-size: 18px;
padding: 4px;
}
.sidebar-toggle-btn {
background: none;
border: 1px solid #E5E7EB;
border-radius: 6px;
padding: 6px 10px;
cursor: pointer;
font-size: 16px;
color: #4B5563;
display: none;
}
/* ── Page Content ── */
.page-content {
flex: 1;
padding: 25px;
}
/* ── Footer ── */ /* ── Footer ── */
.footer { padding: 12px 20px; background: var(--bg); border-top: 1px solid var(--border); display: flex; justify-content: space-between; font-size: .8rem; color: var(--text-secondary); } .page-footer {
padding: 15px 25px;
background: #fff;
border-top: 1px solid #E5E7EB;
display: flex;
justify-content: space-between;
font-size: 12px;
color: #9CA3AF;
}
/* ── Cards ── */ /* ── Cards ── */
.card { background: var(--bg); border-radius: var(--radius); box-shadow: var(--shadow); margin-bottom: 20px; overflow: hidden; } .card {
.card-header { padding: 14px 20px; border-bottom: 1px solid var(--border); font-weight: 600; display: flex; align-items: center; justify-content: space-between; } background: #fff;
.card-body { padding: 20px; } border-radius: 8px;
.card-footer { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; } border: 1px solid #E5E7EB;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
margin-bottom: 0;
overflow: hidden;
}
/* ── Tables ── */ /* ── Tables ── */
.table-responsive { overflow-x: auto; } .table-responsive {
.data-table { width: 100%; border-collapse: collapse; } overflow-x: auto;
.data-table th { background: var(--primary-dark); color: #fff; padding: 10px 14px; text-align: right; font-weight: 600; white-space: nowrap; } }
.data-table td { padding: 10px 14px; border-bottom: 1px solid var(--border); }
.data-table tbody tr:nth-child(even) { background: #F9FAFB; }
.data-table tbody tr:hover { background: #E6F4F4; }
.actions-col { width: 120px; text-align: center; }
.action-buttons { display: flex; gap: 4px; justify-content: center; }
/* ── Forms ── */ .data-table {
.form-group { margin-bottom: 16px; } width: 100%;
.form-label { display: block; margin-bottom: 4px; font-weight: 600; color: var(--text); font-size: .9rem; } border-collapse: collapse;
.required-mark { color: var(--error); } font-size: 14px;
.form-input, .form-select, .form-textarea { width: 100%; padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: .95rem; color: var(--text); background: var(--bg); transition: border-color .2s; } }
.form-input:focus, .form-select:focus, .form-textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(13,115,119,.15); }
.form-textarea { min-height: 100px; resize: vertical; } .data-table thead {
.form-error { color: var(--error); font-size: .8rem; margin-top: 2px; } background: #0D7377;
.form-help { color: var(--text-secondary); font-size: .8rem; margin-top: 2px; } color: #fff;
.has-error .form-input, .has-error .form-select { border-color: var(--error); } }
.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; }
.checkbox-label, .radio-label { display: flex; align-items: center; gap: 6px; cursor: pointer; } .data-table th {
.radio-group { display: flex; gap: 16px; flex-wrap: wrap; } padding: 12px 15px;
text-align: right;
font-weight: 600;
white-space: nowrap;
}
.data-table td {
padding: 10px 15px;
border-bottom: 1px solid #F3F4F6;
text-align: right;
}
.data-table tbody tr:hover {
background: #F9FAFB;
}
/* ── Buttons ── */ /* ── Buttons ── */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 18px; border: 1px solid transparent; border-radius: var(--radius-sm); font-family: var(--font); font-size: .9rem; font-weight: 600; cursor: pointer; transition: all .2s; text-decoration: none; } .btn {
.btn-primary { background: var(--primary); color: #fff; } display: inline-flex;
.btn-primary:hover { background: var(--primary-dark); color: #fff; } align-items: center;
.btn-secondary { background: var(--surface); color: var(--text); border-color: var(--border); } justify-content: center;
.btn-secondary:hover { background: var(--border); } gap: 6px;
.btn-danger { background: var(--error); color: #fff; } padding: 8px 16px;
.btn-danger:hover { background: #B91C1C; color: #fff; } border-radius: 6px;
.btn-success { background: var(--success); color: #fff; } font-size: 14px;
.btn-success:hover { background: #047857; color: #fff; } font-weight: 600;
.btn-warning { background: var(--warning); color: #fff; } text-decoration: none;
.btn-outline { background: transparent; color: var(--primary); border-color: var(--primary); } cursor: pointer;
.btn-outline:hover { background: var(--primary); color: #fff; } border: 1px solid transparent;
.btn-sm { padding: 4px 10px; font-size: .8rem; } transition: all 0.2s ease;
.btn-lg { padding: 12px 28px; font-size: 1rem; } white-space: nowrap;
.btn:disabled { opacity: .5; cursor: not-allowed; } font-family: inherit;
}
.btn-primary {
background: #0D7377;
color: #fff;
border-color: #0D7377;
}
.btn-primary:hover {
background: #0a5c5f;
}
.btn-outline {
background: transparent;
color: #0D7377;
border-color: #D1D5DB;
}
.btn-outline:hover {
background: #F3F4F6;
border-color: #0D7377;
}
.btn-sm {
padding: 4px 10px;
font-size: 12px;
}
/* ── Forms ── */
.form-group {
margin-bottom: 0;
}
.form-label {
display: block;
margin-bottom: 5px;
font-size: 13px;
font-weight: 600;
color: #374151;
}
.form-input,
.form-select,
.form-textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #D1D5DB;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
background: #fff;
color: #1A1A2E;
transition: border-color 0.2s;
box-sizing: border-box;
}
.form-input:focus,
.form-select:focus,
.form-textarea:focus {
outline: none;
border-color: #0D7377;
box-shadow: 0 0 0 3px rgba(13, 115, 119, 0.1);
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
/* ── Alerts ── */ /* ── Alerts ── */
.alerts-wrapper { margin-bottom: 16px; } .alert {
.alert { padding: 12px 16px; border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; animation: slideIn .3s ease; } padding: 12px 20px;
.alert-success { background: #ECFDF5; color: #065F46; border: 1px solid #A7F3D0; } border-radius: 8px;
.alert-error { background: #FEF2F2; color: #991B1B; border: 1px solid #FECACA; } margin: 0 25px 15px;
.alert-warning { background: #FFFBEB; color: #92400E; border: 1px solid #FDE68A; } font-size: 14px;
.alert-info { background: #EFF6FF; color: #1E40AF; border: 1px solid #BFDBFE; } font-weight: 500;
.alert-close { background: none; border: none; cursor: pointer; font-size: 1rem; opacity: .6; } display: flex;
.alert-close:hover { opacity: 1; } justify-content: space-between;
align-items: center;
/* ── Badges ── */ }
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: .75rem; font-weight: 600; }
.badge-primary { background: #E0F2F1; color: var(--primary-dark); } .alert-success {
.badge-success { background: #ECFDF5; color: var(--success); } background: #F0FDF4;
.badge-danger { background: #FEF2F2; color: var(--error); } color: #059669;
.badge-warning { background: #FFFBEB; color: var(--warning); } border: 1px solid #BBF7D0;
.badge-info { background: #EFF6FF; color: var(--info); } }
/* ── Modals ── */ .alert-error {
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); display: flex; align-items: center; justify-content: center; z-index: 200; animation: fadeIn .2s; } background: #FEF2F2;
.modal { background: var(--bg); border-radius: var(--radius); width: 90%; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; } color: #DC2626;
.modal-small { max-width: 400px; } border: 1px solid #FECACA;
.modal-medium { max-width: 600px; } }
.modal-large { max-width: 900px; }
.modal-fullscreen { max-width: 95vw; max-height: 95vh; } .alert-warning {
.modal-header { padding: 14px 20px; background: var(--primary-dark); color: #fff; display: flex; align-items: center; justify-content: space-between; } background: #FFF7ED;
.modal-title { font-weight: 600; } color: #D97706;
.modal-close { background: none; border: none; color: #fff; font-size: 1.2rem; cursor: pointer; } border: 1px solid #FED7AA;
.modal-body { padding: 20px; overflow-y: auto; flex: 1; } }
.modal-footer { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; }
.alert-info {
/* ── Toast Notifications ── */ background: #EFF6FF;
.toast-container { position: fixed; bottom: 20px; left: 20px; z-index: 300; display: flex; flex-direction: column-reverse; gap: 8px; } color: #0284C7;
.toast { padding: 12px 18px; border-radius: var(--radius-sm); color: #fff; font-weight: 500; min-width: 280px; box-shadow: var(--shadow-md); animation: slideUp .3s ease; display: flex; justify-content: space-between; align-items: center; } border: 1px solid #BFDBFE;
.toast-success { background: var(--success); } }
.toast-error { background: var(--error); }
.toast-warning { background: var(--warning); } .alert-close {
.toast-info { background: var(--info); } background: none;
.toast-close { background: none; border: none; color: #fff; cursor: pointer; margin-right: 10px; } border: none;
cursor: pointer;
/* ── Breadcrumbs ── */ font-size: 18px;
.breadcrumbs { display: flex; align-items: center; gap: 4px; margin-bottom: 10px; font-size: .85rem; flex-wrap: wrap; } color: inherit;
.breadcrumb-item { color: var(--text-secondary); } opacity: 0.6;
.breadcrumb-item.current { color: var(--text); font-weight: 600; } padding: 0 5px;
.breadcrumb-separator { color: var(--border); } }
/* ── Pagination ── */ .alert-close:hover {
.pagination-wrapper { display: flex; align-items: center; justify-content: space-between; margin-top: 16px; flex-wrap: wrap; gap: 12px; } opacity: 1;
.pagination-info { color: var(--text-secondary); font-size: .85rem; } }
.pagination { display: flex; list-style: none; gap: 4px; }
.page-link { display: inline-block; padding: 6px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text); font-size: .85rem; } /* ── Responsive ── */
.page-link:hover { background: var(--surface); color: var(--text); } @media (max-width: 1024px) {
.page-link.active { background: var(--primary); color: #fff; border-color: var(--primary); } .sidebar {
.page-ellipsis { padding: 6px 8px; color: var(--text-secondary); } transform: translateX(260px);
width: 260px;
/* ── Stats Cards ── */ }
.stats-card { background: var(--bg); border-radius: var(--radius); box-shadow: var(--shadow); padding: 16px; display: flex; align-items: center; gap: 16px; position: relative; overflow: hidden; } .sidebar.show {
.stats-card::after { content: ''; position: absolute; top: 0; right: 0; width: 4px; height: 100%; } transform: translateX(0);
.stats-card-primary::after { background: var(--primary); } }
.stats-card-success::after { background: var(--success); } .main-wrapper {
.stats-card-danger::after { background: var(--error); } margin-right: 0;
.stats-card-warning::after { background: var(--warning); } }
.stats-card-icon { font-size: 2rem; } .sidebar-toggle-btn {
.stats-card-title { color: var(--text-secondary); font-size: .85rem; } display: block;
.stats-card-value { font-size: 1.5rem; font-weight: 700; color: var(--text); } }
.stats-card-change { font-size: .8rem; color: var(--success); } }
.stats-card-link { position: absolute; bottom: 8px; left: 16px; font-size: .8rem; }
/* ── Empty State ── */
.empty-state { text-align: center; padding: 40px; }
.empty-state-icon { font-size: 3rem; margin-bottom: 12px; }
.empty-state-message { color: var(--text-secondary); font-size: 1rem; margin-bottom: 16px; }
/* ── Tabs ── */
.tabs { display: flex; border-bottom: 2px solid var(--border); margin-bottom: 16px; gap: 0; overflow-x: auto; }
.tab-link { padding: 10px 20px; color: var(--text-secondary); font-weight: 600; border-bottom: 2px solid transparent; margin-bottom: -2px; cursor: pointer; white-space: nowrap; transition: all .2s; background: none; border-top: none; border-left: none; border-right: none; font-family: var(--font); }
.tab-link:hover { color: var(--primary); }
.tab-link.active { color: var(--primary); border-bottom-color: var(--primary); }
.tab-content { display: none; }
.tab-content.active { display: block; }
/* ── Status Dots ── */
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-left: 6px; }
.status-dot-success { background: var(--success); }
.status-dot-warning { background: var(--warning); }
.status-dot-danger { background: var(--error); }
.status-dot-info { background: var(--info); }
/* ── Loading Spinner ── */
.spinner { display: inline-block; width: 24px; height: 24px; border: 3px solid var(--border); border-top-color: var(--primary); border-radius: 50%; animation: spin .7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Utilities ── */
.d-none { display: none !important; }
.d-block { display: block !important; }
.d-flex { display: flex !important; }
.d-grid { display: grid !important; }
.text-right { text-align: right !important; }
.text-left { text-align: left !important; }
.text-center { text-align: center !important; }
.text-bold { font-weight: 700 !important; }
.text-muted { color: var(--text-secondary) !important; }
.text-success { color: var(--success) !important; }
.text-danger { color: var(--error) !important; }
.text-warning { color: var(--warning) !important; }
.text-primary { color: var(--primary) !important; }
.m-0 { margin: 0 !important; } .m-1 { margin: 4px !important; } .m-2 { margin: 8px !important; } .m-3 { margin: 16px !important; } .m-4 { margin: 24px !important; } .m-5 { margin: 32px !important; }
.mb-0 { margin-bottom: 0 !important; } .mb-1 { margin-bottom: 4px !important; } .mb-2 { margin-bottom: 8px !important; } .mb-3 { margin-bottom: 16px !important; } .mb-4 { margin-bottom: 24px !important; }
.mt-0 { margin-top: 0 !important; } .mt-2 { margin-top: 8px !important; } .mt-3 { margin-top: 16px !important; }
.p-0 { padding: 0 !important; } .p-1 { padding: 4px !important; } .p-2 { padding: 8px !important; } .p-3 { padding: 16px !important; } .p-4 { padding: 24px !important; } .p-5 { padding: 32px !important; }
.gap-1 { gap: 4px; } .gap-2 { gap: 8px; } .gap-3 { gap: 16px; } .gap-4 { gap: 24px; }
.flex-wrap { flex-wrap: wrap; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.justify-end { justify-content: flex-end; }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
.w-full { width: 100%; }
/* ── Animations ── */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideIn { from { transform: translateY(-10px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
/* ── Print ── */ /* ── Print ── */
@media print { @media print {
.sidebar, .topbar, .footer, .page-actions, .btn, .alerts-wrapper, .pagination-wrapper, .toast-container { display: none !important; } .sidebar, .top-header, .page-footer, .btn, .sidebar-toggle-btn {
.main-wrapper { margin-right: 0 !important; } display: none !important;
.content-area { padding: 0 !important; } }
body { background: #fff; } .main-wrapper {
margin-right: 0 !important;
}
.page-content {
padding: 0 !important;
}
} }
.d-print-none { }
.d-print-block { display: none; }
@media print { .d-print-none { display: none !important; } .d-print-block { display: block !important; } }
/* ── Responsive ── */ /* ── Body Reset ── */
@media (max-width: 1279px) { * {
.sidebar { transform: translateX(100%); } box-sizing: border-box;
.sidebar.open { transform: translateX(0); } }
.main-wrapper { margin-right: 0; }
.sidebar-toggle, .sidebar-mobile-toggle { display: block; } body {
} margin: 0;
@media (max-width: 767px) { padding: 0;
.topbar-search-input { width: 150px; } font-family: 'Cairo', 'Segoe UI', Tahoma, Arial, sans-serif;
.form-row { grid-template-columns: 1fr; } font-size: 14px;
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } color: #1A1A2E;
.footer { flex-direction: column; gap: 4px; text-align: center; } background: #F3F4F6;
direction: rtl;
line-height: 1.6;
}
a {
color: #0D7377;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
code {
background: #F3F4F6;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
font-family: 'Courier New', monospace;
} }
\ No newline at end of file
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