Commit de5cfc33 authored by Mahmoud Aglan's avatar Mahmoud Aglan

goooooooooo

parent 048af80b
{
"permissions": {
"allow": [
"Bash(wc -l /Users/mahmoudaglan/clubphp/app/Core/*.php)"
]
}
}
<?php
/**
* Flash alert messages component.
* Flash alert messages component — with Lucide icons and animations.
*/
$session = \App\Core\App::getInstance()->session();
$alerts = $session->getAlerts();
......@@ -8,42 +8,26 @@ $alerts = $session->getAlerts();
if (empty($alerts)) {
return;
}
$iconSvg = [
'success' => '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
'error' => '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
'warning' => '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
'info' => '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
];
$closeSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
?>
<?php foreach ($alerts as $alert): ?>
<?php
$type = $alert['type'] ?? 'info';
$message = $alert['message'] ?? '';
$bgColor = match($type) {
'success' => '#F0FDF4',
'error' => '#FEF2F2',
'warning' => '#FFF7ED',
'info' => '#EFF6FF',
default => '#F9FAFB',
};
$textColor = match($type) {
'success' => '#059669',
'error' => '#DC2626',
'warning' => '#D97706',
'info' => '#0284C7',
default => '#6B7280',
};
$borderColor = match($type) {
'success' => '#BBF7D0',
'error' => '#FECACA',
'warning' => '#FED7AA',
'info' => '#BFDBFE',
default => '#E5E7EB',
};
$icon = match($type) {
'success' => '✓',
'error' => '✗',
'warning' => '⚠',
'info' => 'ℹ',
default => '',
};
?>
<div class="alert alert-<?= $type ?>" style="background:<?= $bgColor ?>;color:<?= $textColor ?>;border:1px solid <?= $borderColor ?>;padding:12px 20px;border-radius:8px;margin:0 25px 15px;font-size:14px;display:flex;justify-content:space-between;align-items:center;">
<span><?= $icon ?> <?= e($message) ?></span>
<button onclick="this.parentElement.remove()" style="background:none;border:none;cursor:pointer;font-size:18px;color:<?= $textColor ?>;opacity:0.6;padding:0 5px;">&times;</button>
<div class="alert alert-<?= $type ?>" data-auto-dismiss="5000">
<div class="alert-content">
<span class="alert-icon"><?= $iconSvg[$type] ?? $iconSvg['info'] ?></span>
<span><?= e($message) ?></span>
</div>
<button class="alert-close" onclick="var el=this.parentElement;el.classList.add('dismissing');el.addEventListener('animationend',function(){el.remove();})"><?= $closeSvg ?></button>
</div>
<?php endforeach; ?>
\ No newline at end of file
<?php endforeach; ?>
<?php
/**
* Breadcrumbs component — with Lucide home icon and clean separators.
*
* @var array $items [['label'=>..., 'url'=>...]]
*/
$items = $items ?? [];
?>
<?php if (!empty($items)): ?>
<nav class="breadcrumbs">
<a href="/" class="breadcrumb-item">🏠 الرئيسية</a>
<a href="/" class="breadcrumb-item">
<i data-lucide="home" style="width:15px;height:15px;"></i>
الرئيسية
</a>
<?php foreach ($items as $i => $item): ?>
<span class="breadcrumb-separator"></span>
<span class="breadcrumb-separator">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
</span>
<?php if ($i < count($items) - 1 && !empty($item['url'])): ?>
<a href="<?= e($item['url']) ?>" class="breadcrumb-item"><?= e($item['label']) ?></a>
<?php else: ?>
......@@ -16,4 +23,4 @@ $items = $items ?? [];
<?php endif; ?>
<?php endforeach; ?>
</nav>
<?php endif; ?>
\ No newline at end of file
<?php endif; ?>
<?php
/**
* Data table component — with row animations and action buttons.
*
* @var array $columns [['key'=>..., 'label_ar'=>..., 'sortable'=>bool, 'class'=>'']]
* @var array $rows
* @var array $pagination (optional)
......@@ -11,7 +13,7 @@ $pagination = $pagination ?? null;
$actions = $actions ?? [];
?>
<?php if (empty($rows)): ?>
<?php $__template->include('Shared.Components.empty-state', ['message' => $emptyMessage ?? 'لا توجد بيانات']); ?>
<?php $__template->include('Shared.Components.empty-state', ['message' => $emptyMessage ?? 'لا توجد بيانات', 'icon' => $emptyIcon ?? 'inbox']); ?>
<?php else: ?>
<div class="table-responsive">
<table class="data-table">
......@@ -52,8 +54,14 @@ $actions = $actions ?? [];
if (is_callable($href)) $href = $href($row);
$label = $action['label'] ?? '';
$class = $action['class'] ?? 'btn btn-sm btn-outline';
$iconName = $action['icon'] ?? null;
?>
<a href="<?= e($href) ?>" class="<?= e($class) ?>"><?= e($label) ?></a>
<a href="<?= e($href) ?>" class="<?= e($class) ?>">
<?php if ($iconName): ?>
<i data-lucide="<?= e($iconName) ?>" style="width:14px;height:14px;"></i>
<?php endif; ?>
<?= e($label) ?>
</a>
<?php endforeach; ?>
</div>
</td>
......@@ -66,4 +74,4 @@ $actions = $actions ?? [];
<?php if ($pagination): ?>
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
<?php endif; ?>
<?php endif; ?>
\ No newline at end of file
<?php endif; ?>
<?php
/**
* Empty state component — with Lucide icon and subtle animation.
*
* @var string $message
* @var string $icon Lucide icon name (e.g. 'inbox', 'search', 'file-x')
* @var string $actionUrl (optional)
* @var string $actionLabel (optional)
*/
$message = $message ?? 'لا توجد بيانات لعرضها';
$icon = $icon ?? '📭';
$iconName = $icon ?? 'inbox';
$actionUrl = $actionUrl ?? null;
$actionLabel = $actionLabel ?? null;
?>
<div class="empty-state">
<div class="empty-state-icon"><?= $icon ?></div>
<div class="empty-state-icon">
<i data-lucide="<?= e($iconName) ?>"></i>
</div>
<p class="empty-state-message"><?= e($message) ?></p>
<?php if ($actionUrl && $actionLabel): ?>
<a href="<?= e($actionUrl) ?>" class="btn btn-primary"><?= e($actionLabel) ?></a>
<a href="<?= e($actionUrl) ?>" class="btn btn-primary">
<i data-lucide="plus" style="width:16px;height:16px;"></i>
<?= e($actionLabel) ?>
</a>
<?php endif; ?>
</div>
\ No newline at end of file
</div>
<?php
/**
* Renders a form field.
* Renders a form field — with enhanced styling and Lucide icon support.
*
* @var string $type
* @var string $name
* @var string $label_ar
......@@ -11,6 +12,7 @@
* @var array $options (for select/radio)
* @var string $placeholder
* @var string $help_text
* @var string $icon (optional) Lucide icon name
*/
$type = $type ?? 'text';
$name = $name ?? '';
......@@ -104,10 +106,13 @@ $id = 'field_' . str_replace(['[', ']', '.'], '_', $name);
<?php endif; ?>
<?php if ($error): ?>
<div class="form-error"><?= e($error) ?></div>
<div class="form-error">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
<?= e($error) ?>
</div>
<?php endif; ?>
<?php if ($help_text): ?>
<small class="form-help"><?= e($help_text) ?></small>
<?php endif; ?>
</div>
\ No newline at end of file
</div>
<?php
/**
* Top bar component — with search, notifications, and user profile.
*/
use App\Core\App;
$employee = App::getInstance()->currentEmployee();
$branch = App::getInstance()->currentBranch();
?>
<div class="topbar-right">
<button class="topbar-btn sidebar-mobile-toggle" onclick="toggleSidebar()"></button>
<button class="topbar-btn sidebar-mobile-toggle" onclick="toggleSidebar()">
<i data-lucide="menu" style="width:20px;height:20px;"></i>
</button>
<div class="topbar-search">
<input type="text" id="global-search" placeholder="بحث سريع..." class="topbar-search-input" autocomplete="off">
</div>
......@@ -12,17 +17,24 @@ $branch = App::getInstance()->currentBranch();
<div class="topbar-left">
<span class="topbar-date"><?= arabic_date(today()) ?></span>
<?php if ($branch): ?>
<span class="topbar-branch"><?= e($branch['name_ar'] ?? '') ?></span>
<span class="topbar-branch">
<i data-lucide="building-2" style="width:12px;height:12px;display:inline;vertical-align:middle;margin-left:4px;"></i>
<?= e($branch['name_ar'] ?? '') ?>
</span>
<?php endif; ?>
<div class="topbar-notifications">
<button class="topbar-btn" id="notif-bell" title="الإشعارات">
🔔 <span class="notif-badge" id="notif-badge" style="display:none;">0</span>
<i data-lucide="bell" style="width:20px;height:20px;"></i>
<span class="notif-badge" id="notif-badge" style="display:none;">0</span>
</button>
</div>
<?php if ($employee): ?>
<div class="topbar-user">
<span class="topbar-username"><?= e($employee->full_name_ar ?? ($employee['full_name_ar'] ?? 'مستخدم')) ?></span>
<a href="/logout" class="topbar-btn topbar-logout" title="تسجيل الخروج">خروج</a>
<a href="/logout" class="topbar-btn topbar-logout" title="تسجيل الخروج">
<i data-lucide="log-out" style="width:16px;height:16px;display:inline;vertical-align:middle;margin-left:4px;"></i>
خروج
</a>
</div>
<?php endif; ?>
</div>
\ No newline at end of file
</div>
<?php
/**
* Modal component — with backdrop blur, scale animation, and Lucide close icon.
*
* @var string $id
* @var string $title
* @var string $size (small|medium|large|fullscreen)
......@@ -12,7 +14,9 @@ $size = $size ?? 'medium';
<div class="modal modal-<?= e($size) ?>">
<div class="modal-header">
<h3 class="modal-title"><?= e($title) ?></h3>
<button class="modal-close" onclick="closeModal('<?= e($id) ?>')"></button>
<button class="modal-close" onclick="closeModal('<?= e($id) ?>')">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<?= $__template->yield('modal_body_' . $id, '') ?>
......@@ -21,4 +25,4 @@ $size = $size ?? 'medium';
<?= $__template->yield('modal_footer_' . $id, '') ?>
</div>
</div>
</div>
\ No newline at end of file
</div>
<?php
/**
* Pagination component — with styled links and Lucide arrow icons.
*
* @var array $pagination
* @var string $baseUrl (optional)
*/
......@@ -14,7 +16,11 @@ $separator = str_contains($baseUrl, '?') ? '&' : '?';
</div>
<ul class="pagination">
<?php if ($pagination['has_prev']): ?>
<li><a href="<?= $baseUrl . $separator ?>page=<?= $pagination['prev_page'] ?>" class="page-link">السابق</a></li>
<li>
<a href="<?= $baseUrl . $separator ?>page=<?= $pagination['prev_page'] ?>" class="page-link" title="السابق">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</a>
</li>
<?php endif; ?>
<?php foreach ($pagination['pages'] as $page): ?>
......@@ -31,7 +37,11 @@ $separator = str_contains($baseUrl, '?') ? '&' : '?';
<?php endforeach; ?>
<?php if ($pagination['has_next']): ?>
<li><a href="<?= $baseUrl . $separator ?>page=<?= $pagination['next_page'] ?>" class="page-link">التالي</a></li>
<li>
<a href="<?= $baseUrl . $separator ?>page=<?= $pagination['next_page'] ?>" class="page-link" title="التالي">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
</a>
</li>
<?php endif; ?>
</ul>
</nav>
\ No newline at end of file
</nav>
<?php
/**
* Sidebar component — reads menu items from MenuRegistry.
* Uses Lucide icons instead of emojis for a professional look.
*/
use App\Core\App;
......@@ -9,47 +10,50 @@ use App\Core\Registries\MenuRegistry;
$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$employee = App::getInstance()->currentEmployee();
// Icon name → emoji mapping
// Icon name → Lucide icon mapping
$iconMap = [
'dashboard' => '📊', 'tachometer-alt' => '📊', 'home' => '🏠',
'users' => '👥', 'user' => '👤', 'user-plus' => '👤', 'user-clock' => '⏰',
'user-tie' => '👔', 'user-shield' => '🛡️', 'user-friends' => '👥',
'clipboard' => '📋', 'clipboard-list' => '📋', 'file-alt' => '📄',
'calendar' => '📅', 'calendar-alt' => '📅', 'calendar-check' => '✅',
'money-bill' => '💰', 'wallet' => '💳', 'cash-register' => '💰',
'credit-card' => '💳', 'receipt' => '🧾', 'coins' => '🪙',
'file-invoice-dollar' => '💲', 'hand-holding-usd' => '💰',
'exchange-alt' => '🔀', 'random' => '🔀', 'transfer' => '🔀',
'gavel' => '⚖️', 'balance-scale' => '⚖️', 'exclamation-triangle' => '⚠️',
'alert' => '⚠️', 'warning' => '⚠️', 'ban' => '🚫',
'trophy' => '🏆', 'medal' => '🏅', 'award' => '🎖️', 'star' => '⭐',
'globe' => '🌍', 'globe-americas' => '🌎', 'flag' => '🏳️',
'id-card' => '🪪', 'address-card' => '🪪', 'qrcode' => '📱',
'file' => '📁', 'folder' => '📁', 'folder-open' => '📂',
'sms' => '📱', 'envelope' => '✉️', 'bell' => '🔔', 'comment' => '💬',
'chart-bar' => '📈', 'chart-line' => '📈', 'chart-pie' => '📊',
'cog' => '⚙️', 'cogs' => '⚙️', 'wrench' => '🔧', 'tools' => '🛠️',
'sliders-h' => '⚙️', 'settings' => '⚙️',
'shield-alt' => '🔐', 'lock' => '🔒', 'key' => '🔑',
'building' => '🏢', 'city' => '🏙️', 'store' => '🏪',
'book' => '📖', 'history' => '📜', 'archive' => '🗄️',
'sitemap' => '🔄', 'project-diagram' => '🔄', 'workflow' => '🔄',
'heart' => '❤️', 'heartbeat' => '💓', 'cross' => '✝️',
'ring' => '💍', 'baby' => '👶', 'child' => '👦', 'children' => '👨‍👩‍👧‍👦',
'repeat' => '🔄', 'sync' => '🔄', 'redo' => '🔄',
'print' => '🖨️', 'search' => '🔍', 'plus' => '➕', 'edit' => '✏️',
'trash' => '🗑️', 'times' => '❌', 'check' => '✅',
'dollar-sign' => '💲', 'percentage' => '💹',
'swimming-pool' => '🏊', 'running' => '🏃', 'futbol' => '⚽',
'sun' => '☀️', 'umbrella-beach' => '🏖️',
'dashboard' => 'layout-dashboard', 'tachometer-alt' => 'gauge', 'home' => 'home',
'users' => 'users', 'user' => 'user', 'user-plus' => 'user-plus', 'user-clock' => 'user-cog',
'user-tie' => 'briefcase', 'user-shield' => 'shield-check', 'user-friends' => 'users',
'clipboard' => 'clipboard-list', 'clipboard-list' => 'clipboard-list', 'file-alt' => 'file-text',
'calendar' => 'calendar', 'calendar-alt' => 'calendar-days', 'calendar-check' => 'calendar-check',
'money-bill' => 'banknote', 'wallet' => 'wallet', 'cash-register' => 'banknote',
'credit-card' => 'credit-card', 'receipt' => 'receipt', 'coins' => 'coins',
'file-invoice-dollar' => 'file-text', 'hand-holding-usd' => 'hand-coins',
'exchange-alt' => 'arrow-left-right', 'random' => 'shuffle', 'transfer' => 'arrow-left-right',
'gavel' => 'gavel', 'balance-scale' => 'scale', 'exclamation-triangle' => 'alert-triangle',
'alert' => 'alert-triangle', 'warning' => 'alert-triangle', 'ban' => 'ban',
'trophy' => 'trophy', 'medal' => 'medal', 'award' => 'award', 'star' => 'star',
'globe' => 'globe', 'globe-americas' => 'globe-2', 'flag' => 'flag',
'id-card' => 'id-card', 'address-card' => 'contact', 'qrcode' => 'qr-code',
'file' => 'folder', 'folder' => 'folder', 'folder-open' => 'folder-open',
'sms' => 'smartphone', 'envelope' => 'mail', 'bell' => 'bell', 'comment' => 'message-circle',
'chart-bar' => 'bar-chart-3', 'chart-line' => 'trending-up', 'chart-pie' => 'pie-chart',
'cog' => 'settings', 'cogs' => 'settings-2', 'wrench' => 'wrench', 'tools' => 'wrench',
'sliders-h' => 'sliders-horizontal', 'settings' => 'settings',
'shield-alt' => 'shield', 'lock' => 'lock', 'key' => 'key-round',
'building' => 'building-2', 'city' => 'building', 'store' => 'store',
'book' => 'book-open', 'history' => 'history', 'archive' => 'archive',
'sitemap' => 'git-branch', 'project-diagram' => 'workflow', 'workflow' => 'workflow',
'heart' => 'heart', 'heartbeat' => 'heart-pulse', 'cross' => 'cross',
'ring' => 'gem', 'baby' => 'baby', 'child' => 'user', 'children' => 'users',
'repeat' => 'repeat', 'sync' => 'refresh-cw', 'redo' => 'redo',
'print' => 'printer', 'search' => 'search', 'plus' => 'plus', 'edit' => 'pencil',
'trash' => 'trash-2', 'times' => 'x', 'check' => 'check',
'dollar-sign' => 'circle-dollar-sign', 'percentage' => 'percent',
'swimming-pool' => 'waves', 'running' => 'activity', 'futbol' => 'trophy',
'sun' => 'sun', 'umbrella-beach' => 'umbrella',
];
$getIcon = function(?string $icon) use ($iconMap): string {
if ($icon === null || $icon === '') return '📌';
// Already an emoji
if (mb_strlen($icon) <= 2 && !ctype_alpha($icon)) return $icon;
// Check map
return $iconMap[$icon] ?? $iconMap[strtolower($icon)] ?? '📌';
if ($icon === null || $icon === '') return 'circle';
// If it's already a lucide icon name (contains hyphen or is a known name)
if (isset($iconMap[$icon])) return $iconMap[$icon];
$lower = strtolower($icon);
if (isset($iconMap[$lower])) return $iconMap[$lower];
// Check if it looks like it might already be a lucide icon name
if (preg_match('/^[a-z][a-z0-9-]+$/', $lower)) return $lower;
return 'circle';
};
// Get all permissions for current employee
......@@ -61,18 +65,18 @@ if ($employee && method_exists($employee, 'getAllPermissions')) {
$db = App::getInstance()->db();
$empId = $employee->id ?? 0;
$perms = $db->select(
"SELECT DISTINCT rp.permission_key
FROM employee_roles er
JOIN role_permissions rp ON rp.role_id = er.role_id
"SELECT DISTINCT rp.permission_key
FROM employee_roles er
JOIN role_permissions rp ON rp.role_id = er.role_id
WHERE er.employee_id = ? AND er.is_active = 1",
[(int) $empId]
);
$employeePermissions = array_column($perms, 'permission_key');
$isSuperAdmin = $db->selectOne(
"SELECT 1 FROM employee_roles er
JOIN roles r ON r.id = er.role_id
WHERE er.employee_id = ? AND r.role_code = 'super_admin' AND er.is_active = 1
"SELECT 1 FROM employee_roles er
JOIN roles r ON r.id = er.role_id
WHERE er.employee_id = ? AND r.role_code = 'super_admin' AND er.is_active = 1
LIMIT 1",
[(int) $empId]
);
......@@ -102,15 +106,15 @@ usort($menuItems, fn($a, $b) => ($a['order'] ?? 999) <=> ($b['order'] ?? 999));
// Fallback if registry empty
if (empty($menuItems)) {
$menuItems = [
['key' => 'dashboard', 'label_ar' => 'لوحة التحكم', 'icon' => '📊', 'route' => '/dashboard', 'permission' => '', 'order' => 10, 'children' => []],
['key' => 'members', 'label_ar' => 'إدارة الأعضاء', 'icon' => '👥', 'route' => '/members', 'permission' => 'member.view', 'order' => 100, 'children' => [
['key' => 'dashboard', 'label_ar' => 'لوحة التحكم', 'icon' => 'dashboard', 'route' => '/dashboard', 'permission' => '', 'order' => 10, 'children' => []],
['key' => 'members', 'label_ar' => 'إدارة الأعضاء', 'icon' => 'users', 'route' => '/members', 'permission' => 'member.view', 'order' => 100, 'children' => [
['label_ar' => 'كل الأعضاء', 'route' => '/members', 'permission' => 'member.view'],
['label_ar' => 'عضو جديد', 'route' => '/members/create', 'permission' => 'member.create'],
]],
['key' => 'users', 'label_ar' => 'الموظفون', 'icon' => '👤', 'route' => '/users', 'permission' => 'user.view', 'order' => 910, 'children' => []],
['key' => 'roles', 'label_ar' => 'الأدوار', 'icon' => '🔐', 'route' => '/roles', 'permission' => 'role.view', 'order' => 920, 'children' => []],
['key' => 'branches', 'label_ar' => 'الفروع', 'icon' => '🏢', 'route' => '/branches', 'permission' => 'branch.view', 'order' => 930, 'children' => []],
['key' => 'settings', 'label_ar' => 'الإعدادات', 'icon' => '⚙️', 'route' => '/settings', 'permission' => 'settings.view', 'order' => 960, 'children' => []],
['key' => 'users', 'label_ar' => 'الموظفون', 'icon' => 'user', 'route' => '/users', 'permission' => 'user.view', 'order' => 910, 'children' => []],
['key' => 'roles', 'label_ar' => 'الأدوار', 'icon' => 'shield-alt', 'route' => '/roles', 'permission' => 'role.view', 'order' => 920, 'children' => []],
['key' => 'branches', 'label_ar' => 'الفروع', 'icon' => 'building', 'route' => '/branches', 'permission' => 'branch.view', 'order' => 930, 'children' => []],
['key' => 'settings', 'label_ar' => 'الإعدادات', 'icon' => 'settings', 'route' => '/settings', 'permission' => 'settings.view', 'order' => 960, 'children' => []],
];
}
?>
......@@ -124,12 +128,12 @@ if (empty($menuItems)) {
<?php foreach ($menuItems as $item): ?>
<?php
if (!empty($item['permission']) && !$hasPerm($item['permission'])) continue;
if (!empty($item['is_separator'])) {
echo '<li style="padding:15px 20px 5px;font-size:11px;color:#6B7280;text-transform:uppercase;letter-spacing:1px;">' . e($item['label_ar']) . '</li>';
echo '<li class="sidebar-section-label">' . e($item['label_ar']) . '</li>';
continue;
}
$children = $item['children'] ?? [];
$visibleChildren = [];
foreach ($children as $child) {
......@@ -137,27 +141,27 @@ if (empty($menuItems)) {
$visibleChildren[] = $child;
}
}
$hasChildren = !empty($visibleChildren);
$itemRoute = $item['route'] ?? '#';
$itemActive = $isActive($itemRoute);
$childActive = false;
foreach ($visibleChildren as $child) {
if ($isActive($child['route'] ?? '')) { $childActive = true; break; }
}
$isOpen = $itemActive || $childActive;
$icon = $getIcon($item['icon'] ?? '');
$iconName = $getIcon($item['icon'] ?? '');
?>
<li class="sidebar-item<?= $isOpen && $hasChildren ? ' open' : '' ?>">
<?php if ($hasChildren): ?>
<a href="javascript:void(0)" class="sidebar-link<?= $itemActive ? ' active' : '' ?>" onclick="toggleSubmenu(this)">
<span class="sidebar-icon"><?= $icon ?></span>
<span class="sidebar-icon"><i data-lucide="<?= e($iconName) ?>"></i></span>
<span class="sidebar-text"><?= e($item['label_ar']) ?></span>
<span class="sidebar-arrow"></span>
<span class="sidebar-arrow"><i data-lucide="chevron-down"></i></span>
</a>
<ul class="sidebar-submenu" style="display:<?= $isOpen ? 'block' : 'none' ?>;">
<ul class="sidebar-submenu" style="display:<?= $isOpen ? 'block' : 'none' ?>;<?= $isOpen ? '' : 'max-height:0;overflow:hidden;' ?>">
<?php foreach ($visibleChildren as $child): ?>
<?php $childRoute = $child['route'] ?? '#'; ?>
<li>
......@@ -169,11 +173,11 @@ if (empty($menuItems)) {
</ul>
<?php else: ?>
<a href="<?= e($itemRoute) ?>" class="sidebar-link<?= $itemActive ? ' active' : '' ?>">
<span class="sidebar-icon"><?= $icon ?></span>
<span class="sidebar-icon"><i data-lucide="<?= e($iconName) ?>"></i></span>
<span class="sidebar-text"><?= e($item['label_ar']) ?></span>
</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</nav>
\ No newline at end of file
</nav>
<?php
/**
* Stats card component — with Lucide icon support, animated counters, and hover effects.
*
* @var string $title
* @var string $value
* @var string $icon
* @var string $color (primary|success|danger|warning)
* @var string $link (optional)
* @var string $change (optional, e.g. "+5%")
* @var string $icon Lucide icon name (e.g. 'users', 'banknote', 'trending-up')
* @var string $color (primary|success|danger|warning)
* @var string $link (optional)
* @var string $change (optional, e.g. "+5%")
*/
$color = $color ?? 'primary';
$iconName = $icon ?? 'bar-chart-3';
$numericValue = (string)($value ?? '0');
$isNumeric = is_numeric(str_replace([',', ' '], '', $numericValue));
?>
<div class="stats-card stats-card-<?= e($color) ?>">
<div class="stats-card-icon"><?= $icon ?? '📊' ?></div>
<div class="stats-card-icon">
<i data-lucide="<?= e($iconName) ?>"></i>
</div>
<div class="stats-card-content">
<div class="stats-card-title"><?= e($title ?? '') ?></div>
<div class="stats-card-value"><?= e((string)($value ?? '0')) ?></div>
<div class="stats-card-value" <?= $isNumeric ? 'data-count="' . e(str_replace([',', ' '], '', $numericValue)) . '"' : '' ?>><?= e($numericValue) ?></div>
<?php if (!empty($change)): ?>
<div class="stats-card-change"><?= e($change) ?></div>
<div class="stats-card-change <?= str_starts_with($change, '+') ? 'positive' : (str_starts_with($change, '-') ? 'negative' : '') ?>">
<?php if (str_starts_with($change, '+')): ?>
<i data-lucide="trending-up" style="width:14px;height:14px;"></i>
<?php elseif (str_starts_with($change, '-')): ?>
<i data-lucide="trending-down" style="width:14px;height:14px;"></i>
<?php endif; ?>
<?= e($change) ?>
</div>
<?php endif; ?>
</div>
<?php if (!empty($link)): ?>
<a href="<?= e($link) ?>" class="stats-card-link">عرض الكل ←</a>
<a href="<?= e($link) ?>" class="stats-card-link">
عرض الكل
<i data-lucide="arrow-left" style="width:14px;height:14px;display:inline;vertical-align:middle;"></i>
</a>
<?php endif; ?>
</div>
\ No newline at end of file
</div>
......@@ -7,46 +7,271 @@ use App\Core\CSRF;
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="<?= e(CSRF::token()) ?>">
<title><?= $__template->yield('title', 'تسجيل الدخول') ?> — نادي النادي شيراتون</title>
<link rel="stylesheet" href="<?= url('assets/css/main.css') ?>">
<title><?= $__template->yield('title', 'تسجيل الدخول') ?> — THE CLUB</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #1A1A2E 0%, #0D7377 100%);
font-family: 'Cairo', 'Segoe UI', Tahoma, Arial, sans-serif;
background: #0f0f1a;
font-family: 'Cairo', 'Segoe UI', system-ui, -apple-system, sans-serif;
direction: rtl;
overflow: hidden;
position: relative;
}
/* Animated gradient background */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse at 20% 50%, rgba(13, 115, 119, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(99, 102, 241, 0.1) 0%, transparent 50%),
radial-gradient(ellipse at 50% 80%, rgba(20, 184, 166, 0.08) 0%, transparent 50%);
animation: bgShift 15s ease-in-out infinite alternate;
z-index: 0;
}
@keyframes bgShift {
0% { transform: scale(1) translateY(0); }
100% { transform: scale(1.1) translateY(-20px); }
}
/* Floating particles */
.particles {
position: fixed;
inset: 0;
z-index: 0;
overflow: hidden;
pointer-events: none;
}
.particle {
position: absolute;
border-radius: 50%;
opacity: 0;
animation: float linear infinite;
}
@keyframes float {
0% { opacity: 0; transform: translateY(100vh) scale(0); }
10% { opacity: 1; }
90% { opacity: 1; }
100% { opacity: 0; transform: translateY(-10vh) scale(1); }
}
/* Grid lines */
.grid-bg {
position: fixed;
inset: 0;
z-index: 0;
background-image:
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 60px 60px;
}
/* Auth card */
.auth-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
padding: 40px;
position: relative;
z-index: 1;
background: rgba(255, 255, 255, 0.97);
border-radius: 20px;
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.1);
padding: 48px 40px;
width: 100%;
max-width: 420px;
max-width: 440px;
animation: cardEntry 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes cardEntry {
from {
opacity: 0;
transform: translateY(30px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.auth-header {
text-align: center;
margin-bottom: 30px;
margin-bottom: 36px;
}
.auth-logo {
width: 56px;
height: 56px;
margin: 0 auto 16px;
background: linear-gradient(135deg, #0D7377, #14b8a6);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8px 24px rgba(13, 115, 119, 0.3);
}
.auth-logo svg {
width: 28px;
height: 28px;
color: white;
}
.auth-header h1 {
color: #0D7377;
font-size: 24px;
margin: 0 0 5px;
color: #0f172a;
font-size: 22px;
font-weight: 800;
margin: 0 0 4px;
letter-spacing: -0.01em;
}
.auth-header p {
color: #6B7280;
color: #94a3b8;
font-size: 14px;
margin: 0;
font-weight: 500;
letter-spacing: 2px;
text-transform: uppercase;
}
/* Form styles */
.auth-card .form-group {
margin-bottom: 20px;
}
.auth-card .form-label {
display: block;
margin-bottom: 6px;
font-size: 13px;
font-weight: 600;
color: #475569;
}
.auth-card .form-input {
width: 100%;
padding: 12px 16px;
border: 1.5px solid #e2e8f0;
border-radius: 10px;
font-size: 14px;
font-family: inherit;
background: #f8fafc;
color: #0f172a;
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.auth-card .form-input:focus {
outline: none;
border-color: #0D7377;
background: #fff;
box-shadow: 0 0 0 4px rgba(13, 115, 119, 0.1);
}
.auth-card .form-input::placeholder {
color: #94a3b8;
}
.auth-card .btn-primary {
width: 100%;
padding: 13px;
background: linear-gradient(135deg, #0D7377, #0a5c5f);
color: #fff;
border: none;
border-radius: 10px;
font-size: 15px;
font-weight: 700;
font-family: inherit;
cursor: pointer;
transition: all 0.25s ease;
box-shadow: 0 4px 16px rgba(13, 115, 119, 0.3);
position: relative;
overflow: hidden;
}
.auth-card .btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 6px 24px rgba(13, 115, 119, 0.4);
}
.auth-card .btn-primary:active {
transform: scale(0.98);
}
/* Alert */
.auth-alert {
padding: 12px 16px;
border-radius: 10px;
margin-bottom: 20px;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
gap: 10px;
animation: alertSlideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes alertSlideIn {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.auth-alert-error {
background: #fef2f2;
color: #dc2626;
border: 1px solid #fecaca;
}
.auth-alert-success {
background: #ecfdf5;
color: #059669;
border: 1px solid #a7f3d0;
}
.auth-alert svg {
width: 18px;
height: 18px;
flex-shrink: 0;
}
/* Footer text */
.auth-footer {
text-align: center;
margin-top: 24px;
font-size: 12px;
color: #94a3b8;
}
@media (max-width: 480px) {
.auth-card {
margin: 16px;
padding: 36px 28px;
}
}
</style>
</head>
<body>
<!-- Background effects -->
<div class="grid-bg"></div>
<div class="particles" id="particles"></div>
<div class="auth-card">
<div class="auth-header">
<div class="auth-logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<h1>نادي النادي شيراتون</h1>
<p>THE CLUB Sheraton</p>
</div>
......@@ -56,12 +281,15 @@ use App\Core\CSRF;
$alerts = $session->getAlerts();
if (!empty($alerts)):
foreach ($alerts as $alert):
$isError = ($alert['type'] ?? '') === 'error';
?>
<div style="padding:10px 15px;border-radius:6px;margin-bottom:15px;font-size:13px;
background:<?= ($alert['type'] ?? '') === 'error' ? '#FEF2F2' : '#F0FDF4' ?>;
color:<?= ($alert['type'] ?? '') === 'error' ? '#DC2626' : '#059669' ?>;
border:1px solid <?= ($alert['type'] ?? '') === 'error' ? '#FECACA' : '#BBF7D0' ?>;">
<?= e($alert['message'] ?? '') ?>
<div class="auth-alert <?= $isError ? 'auth-alert-error' : 'auth-alert-success' ?>">
<?php if ($isError): ?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
<?php else: ?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
<?php endif; ?>
<span><?= e($alert['message'] ?? '') ?></span>
</div>
<?php
endforeach;
......@@ -69,6 +297,32 @@ use App\Core\CSRF;
?>
<?= $__template->yield('content', '') ?>
<div class="auth-footer">
THE CLUB ERP &copy; <?= date('Y') ?>
</div>
</div>
<script>
// Generate floating particles
(function() {
var container = document.getElementById('particles');
if (!container) return;
var colors = ['rgba(13,115,119,0.3)', 'rgba(20,184,166,0.2)', 'rgba(99,102,241,0.15)', 'rgba(56,189,248,0.2)'];
for (var i = 0; i < 30; i++) {
var p = document.createElement('div');
p.className = 'particle';
var size = Math.random() * 4 + 2;
p.style.width = size + 'px';
p.style.height = size + 'px';
p.style.left = Math.random() * 100 + '%';
p.style.background = colors[Math.floor(Math.random() * colors.length)];
p.style.animationDuration = (Math.random() * 15 + 10) + 's';
p.style.animationDelay = (Math.random() * 10) + 's';
container.appendChild(p);
}
})();
</script>
</body>
</html>
\ No newline at end of file
</html>
......@@ -10,6 +10,7 @@ use App\Core\CSRF;
$app = App::getInstance();
$employee = $app->currentEmployee();
$employeeName = $employee ? ($employee->full_name_ar ?? 'مستخدم') : 'زائر';
$employeeInitial = mb_substr($employeeName, 0, 1);
$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
?>
<!DOCTYPE html>
......@@ -18,12 +19,34 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="<?= e(CSRF::token()) ?>">
<title><?= $__template->yield('title', 'لوحة التحكم') ?> — نادي النادي شيراتون</title>
<title><?= $__template->yield('title', 'لوحة التحكم') ?> — THE CLUB</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<!-- Main Stylesheet -->
<link rel="stylesheet" href="<?= url('assets/css/main.css') ?>">
<?= $__template->yield('styles', '') ?>
</head>
<body>
<!-- Page Loading Overlay -->
<div class="page-loading" id="page-loading">
<div class="loader">
<div class="loader-dot"></div>
<div class="loader-dot"></div>
<div class="loader-dot"></div>
</div>
</div>
<!-- Sidebar Mobile Overlay -->
<div class="sidebar-overlay" id="sidebar-overlay" onclick="closeSidebar()"></div>
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<?php $__template->include('Shared.Components.sidebar'); ?>
......@@ -33,9 +56,11 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
<div class="main-wrapper" id="main-wrapper">
<!-- Top Header -->
<header class="top-header">
<header class="top-header" id="top-header">
<div class="header-right">
<button class="sidebar-toggle-btn" onclick="toggleSidebar()"></button>
<button class="sidebar-toggle-btn" onclick="toggleSidebar()" aria-label="toggle sidebar">
<i data-lucide="menu" style="width:20px;height:20px;"></i>
</button>
<div class="header-title">
<h1><?= $__template->yield('title', 'لوحة التحكم') ?></h1>
</div>
......@@ -44,7 +69,10 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
<?= $__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 class="header-user-avatar"><?= e($employeeInitial) ?></div>
<a href="/logout" class="header-logout" title="تسجيل الخروج">
<i data-lucide="log-out" style="width:18px;height:18px;"></i>
</a>
</div>
</div>
</header>
......@@ -52,6 +80,9 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
<!-- Alerts -->
<?php $__template->include('Shared.Components.alerts'); ?>
<!-- Toast Container -->
<div id="toast-container"></div>
<!-- Page Content -->
<main class="page-content">
<?= $__template->yield('content', '') ?>
......@@ -59,30 +90,77 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
<!-- Footer -->
<footer class="page-footer">
<span>نادي النادي شيراتون &copy; <?= date('Y') ?></span>
<span>الإصدار 1.0.0</span>
<span>THE CLUB Sheraton &copy; <?= date('Y') ?></span>
<span>v1.0.0</span>
<span><?= arabic_date(date('Y-m-d')) ?></span>
</footer>
</div>
<script src="<?= url('assets/js/app.js') ?>"></script>
<script>
// Initialize Lucide icons
lucide.createIcons();
// Sidebar functions
function toggleSidebar() {
var sb = document.getElementById('sidebar');
var mw = document.getElementById('main-wrapper');
sb.classList.toggle('collapsed');
mw.classList.toggle('sidebar-collapsed');
var overlay = document.getElementById('sidebar-overlay');
var isMobile = window.innerWidth <= 1024;
if (isMobile) {
sb.classList.toggle('show');
overlay.classList.toggle('active');
} else {
sb.classList.toggle('collapsed');
mw.classList.toggle('sidebar-collapsed');
}
}
function closeSidebar() {
var sb = document.getElementById('sidebar');
var overlay = document.getElementById('sidebar-overlay');
sb.classList.remove('show');
overlay.classList.remove('active');
}
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';
var isOpen = parent.classList.contains('open');
parent.classList.toggle('open', !isOpen);
if (!isOpen) {
submenu.style.maxHeight = submenu.scrollHeight + 'px';
submenu.style.display = 'block';
} else {
submenu.style.maxHeight = '0';
setTimeout(function() {
if (!parent.classList.contains('open')) {
submenu.style.display = 'none';
}
}, 300);
}
}
}
// Header scroll shadow
var header = document.getElementById('top-header');
if (header) {
window.addEventListener('scroll', function() {
header.classList.toggle('scrolled', window.scrollY > 10);
}, { passive: true });
}
// Hide page loading
window.addEventListener('load', function() {
var loader = document.getElementById('page-loading');
if (loader) {
loader.classList.add('hidden');
setTimeout(function() { loader.remove(); }, 300);
}
});
</script>
<?= $__template->yield('scripts', '') ?>
</body>
</html>
\ No newline at end of file
</html>
......@@ -3,20 +3,53 @@
<head>
<meta charset="UTF-8">
<title><?= $__template->yield('title', 'طباعة') ?></title>
<link rel="stylesheet" href="<?= url('assets/css/main.css') ?>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body { padding: 20px; background: #fff; }
@media print { body { padding: 0; } }
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
padding: 30px;
background: #fff;
font-family: 'Cairo', 'Segoe UI', sans-serif;
font-size: 13px;
color: #1a1a2e;
direction: rtl;
line-height: 1.7;
}
.print-header {
text-align: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid #0D7377;
}
.print-header h2 {
color: #0D7377;
font-size: 20px;
font-weight: 800;
margin: 0 0 4px;
}
.print-header p {
color: #6B7280;
font-size: 12px;
margin: 0;
}
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #e2e8f0; padding: 8px 12px; text-align: right; font-size: 12px; }
th { background: #f8fafc; font-weight: 600; color: #475569; }
@media print {
body { padding: 0; }
}
</style>
</head>
<body>
<div class="print-header" style="text-align:center;margin-bottom:20px;border-bottom:2px solid #0D7377;padding-bottom:10px;">
<h2 style="color:#0D7377;margin:0;">نادي النادي شيراتون</h2>
<p style="color:#6B7280;margin:5px 0;"><?= arabic_date(today()) ?></p>
<div class="print-header">
<h2>نادي النادي شيراتون</h2>
<p><?= arabic_date(today()) ?></p>
</div>
<?= $__template->yield('content', '') ?>
<script>window.onload = function() { window.print(); }</script>
</body>
</html>
\ No newline at end of file
</html>
/* ═══════════════════════════════════════════
ADD THESE TO THE END OF YOUR main.css
IF SIDEBAR STYLES ARE MISSING
═══════════════════════════════════════════ */
/* ═══════════════════════════════════════════════════════════════
THE CLUB ERP — Premium UI System
Designed for Arabic RTL-first, built with CSS custom properties,
animations, glassmorphism, and micro-interactions.
═══════════════════════════════════════════════════════════════ */
/* ── CSS Custom Properties ── */
:root {
/* Brand Colors */
--brand-primary: #0D7377;
--brand-primary-light: #14b8a6;
--brand-primary-dark: #0a5c5f;
--brand-primary-rgb: 13, 115, 119;
--brand-accent: #6366f1;
--brand-accent-rgb: 99, 102, 241;
/* Sidebar */
--sidebar-bg: #0f0f1a;
--sidebar-width: 270px;
--sidebar-item-hover: rgba(255, 255, 255, 0.06);
--sidebar-item-active: rgba(13, 115, 119, 0.15);
--sidebar-border: rgba(255, 255, 255, 0.06);
/* Surfaces */
--surface-bg: #f8fafc;
--surface-card: #ffffff;
--surface-raised: #ffffff;
--surface-overlay: rgba(15, 15, 26, 0.6);
/* Text */
--text-primary: #0f172a;
--text-secondary: #475569;
--text-muted: #94a3b8;
--text-inverse: #f8fafc;
/* Borders */
--border-light: #e2e8f0;
--border-medium: #cbd5e1;
--border-focus: var(--brand-primary);
/* Status Colors */
--success: #059669;
--success-bg: #ecfdf5;
--success-border: #a7f3d0;
--danger: #dc2626;
--danger-bg: #fef2f2;
--danger-border: #fecaca;
--warning: #d97706;
--warning-bg: #fffbeb;
--warning-border: #fde68a;
--info: #0284c7;
--info-bg: #eff6ff;
--info-border: #bfdbfe;
/* Shadows */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.04);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 8px 10px -6px rgba(0, 0, 0, 0.04);
--shadow-glow: 0 0 20px rgba(var(--brand-primary-rgb), 0.15);
--shadow-card-hover: 0 20px 40px -12px rgba(0, 0, 0, 0.12);
/* Radius */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-xl: 20px;
--radius-full: 9999px;
/* Transitions */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--duration-fast: 150ms;
--duration-normal: 250ms;
--duration-slow: 400ms;
/* Z-indexes */
--z-dropdown: 100;
--z-sticky: 200;
--z-overlay: 500;
--z-modal: 600;
--z-toast: 700;
--z-sidebar: 800;
}
/* ── Reset & Base ── */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: 'Cairo', 'Segoe UI', system-ui, -apple-system, sans-serif;
font-size: 14px;
color: var(--text-primary);
background: var(--surface-bg);
direction: rtl;
line-height: 1.7;
overflow-x: hidden;
}
a {
color: var(--brand-primary);
text-decoration: none;
transition: color var(--duration-fast) ease;
}
a:hover {
color: var(--brand-primary-dark);
}
code {
background: var(--surface-bg);
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: 12px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
border: 1px solid var(--border-light);
}
::selection {
background: rgba(var(--brand-primary-rgb), 0.15);
color: var(--brand-primary-dark);
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border-medium);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* ── Sidebar ── */
/* ══════════════════════════════════════════════════
SIDEBAR
══════════════════════════════════════════════════ */
.sidebar {
position: fixed;
top: 0;
right: 0;
width: 260px;
width: var(--sidebar-width);
height: 100vh;
background: #1A1A2E;
color: #E5E7EB;
background: var(--sidebar-bg);
color: #e2e8f0;
overflow-y: auto;
overflow-x: hidden;
z-index: 1000;
transition: width 0.3s ease, transform 0.3s ease;
z-index: var(--z-sidebar);
transition: width var(--duration-slow) var(--ease-out),
transform var(--duration-slow) var(--ease-out);
display: flex;
flex-direction: column;
border-left: 1px solid rgba(255, 255, 255, 0.04);
}
.sidebar::-webkit-scrollbar {
width: 4px;
}
.sidebar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: var(--radius-full);
}
.sidebar.collapsed {
width: 0;
transform: translateX(260px);
transform: translateX(var(--sidebar-width));
}
/* Sidebar Header */
.sidebar-header {
padding: 20px 15px;
border-bottom: 1px solid rgba(255,255,255,0.1);
padding: 24px 20px;
border-bottom: 1px solid var(--sidebar-border);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
position: relative;
}
.sidebar-brand {
font-size: 18px;
font-weight: 700;
color: #14b8a6;
letter-spacing: 1px;
font-size: 20px;
font-weight: 800;
letter-spacing: 2px;
background: linear-gradient(135deg, var(--brand-primary-light), #38bdf8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.sidebar-toggle {
background: none;
border: none;
color: #9CA3AF;
color: var(--text-muted);
font-size: 18px;
cursor: pointer;
padding: 5px;
transition: color var(--duration-fast) ease;
}
.sidebar-toggle:hover {
color: #fff;
}
/* Sidebar Navigation */
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 10px 0;
padding: 12px 0;
}
.sidebar-menu {
......@@ -66,40 +229,79 @@
margin: 0;
}
/* Sidebar Section Labels */
.sidebar-section-label {
padding: 20px 20px 8px;
font-size: 11px;
font-weight: 700;
color: rgba(148, 163, 184, 0.6);
text-transform: uppercase;
letter-spacing: 1.5px;
}
.sidebar-item {
margin: 2px 0;
margin: 1px 8px;
}
.sidebar-link {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 20px;
color: #D1D5DB;
gap: 12px;
padding: 10px 16px;
color: #94a3b8;
text-decoration: none;
font-size: 14px;
font-size: 13.5px;
font-weight: 500;
transition: all 0.2s ease;
border-radius: 0;
transition: all var(--duration-normal) var(--ease-out);
border-radius: var(--radius-md);
position: relative;
}
.sidebar-link:hover {
background: rgba(255,255,255,0.08);
color: #fff;
background: var(--sidebar-item-hover);
color: #e2e8f0;
text-decoration: none;
}
.sidebar-link.active {
background: rgba(13, 115, 119, 0.3);
color: #14b8a6;
border-left: 3px solid #14b8a6;
background: var(--sidebar-item-active);
color: var(--brand-primary-light);
}
.sidebar-link.active::before {
content: '';
position: absolute;
right: -8px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 60%;
background: var(--brand-primary-light);
border-radius: var(--radius-full);
}
/* Sidebar Icon */
.sidebar-icon {
font-size: 16px;
width: 24px;
text-align: center;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
opacity: 0.7;
transition: opacity var(--duration-fast) ease;
}
.sidebar-link:hover .sidebar-icon,
.sidebar-link.active .sidebar-icon {
opacity: 1;
}
.sidebar-icon svg,
.sidebar-icon i {
width: 18px;
height: 18px;
stroke-width: 1.75;
}
.sidebar-text {
......@@ -109,372 +311,1819 @@
text-overflow: ellipsis;
}
/* Sidebar Arrow */
.sidebar-arrow {
font-size: 10px;
transition: transform 0.2s ease;
color: #6B7280;
display: flex;
align-items: center;
transition: transform var(--duration-normal) var(--ease-out);
color: rgba(148, 163, 184, 0.4);
}
.sidebar-arrow svg {
width: 16px;
height: 16px;
}
.sidebar-item.open > .sidebar-link .sidebar-arrow {
transform: rotate(-90deg);
}
/* Sidebar Submenu */
.sidebar-submenu {
list-style: none;
padding: 0;
padding: 2px 0 6px;
margin: 0;
background: rgba(0,0,0,0.15);
overflow: hidden;
}
.sidebar-submenu li {
margin: 0 4px;
}
.sidebar-sublink {
display: block;
padding: 8px 20px 8px 45px;
color: #9CA3AF;
display: flex;
align-items: center;
gap: 8px;
padding: 7px 16px 7px 16px;
margin-right: 28px;
color: rgba(148, 163, 184, 0.7);
text-decoration: none;
font-size: 13px;
transition: all 0.2s ease;
transition: all var(--duration-normal) var(--ease-out);
border-radius: var(--radius-sm);
border-right: 2px solid transparent;
}
.sidebar-sublink:hover {
color: #fff;
background: rgba(255,255,255,0.05);
color: #e2e8f0;
background: rgba(255, 255, 255, 0.03);
text-decoration: none;
}
.sidebar-sublink.active {
color: #14b8a6;
color: var(--brand-primary-light);
border-right-color: var(--brand-primary-light);
font-weight: 600;
}
/* ── Main Wrapper ── */
.sidebar-sublink::before {
content: '';
width: 5px;
height: 5px;
border-radius: 50%;
background: currentColor;
opacity: 0.4;
flex-shrink: 0;
}
.sidebar-sublink:hover::before,
.sidebar-sublink.active::before {
opacity: 1;
}
/* Sidebar Footer */
.sidebar-footer {
padding: 16px 20px;
border-top: 1px solid var(--sidebar-border);
flex-shrink: 0;
}
/* ══════════════════════════════════════════════════
MAIN WRAPPER
══════════════════════════════════════════════════ */
.main-wrapper {
margin-right: 260px;
margin-right: var(--sidebar-width);
min-height: 100vh;
display: flex;
flex-direction: column;
transition: margin-right 0.3s ease;
background: #F3F4F6;
transition: margin-right var(--duration-slow) var(--ease-out);
background: var(--surface-bg);
}
.main-wrapper.sidebar-collapsed {
margin-right: 0;
}
/* ── Top Header ── */
/* ══════════════════════════════════════════════════
TOP HEADER
══════════════════════════════════════════════════ */
.top-header {
background: #fff;
border-bottom: 1px solid #E5E7EB;
padding: 12px 25px;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-light);
padding: 0 28px;
height: 64px;
display: flex;
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);
z-index: var(--z-sticky);
transition: box-shadow var(--duration-normal) ease;
}
.top-header.scrolled {
box-shadow: var(--shadow-md);
}
.header-right {
display: flex;
align-items: center;
gap: 15px;
gap: 16px;
}
.header-left {
display: flex;
align-items: center;
gap: 15px;
gap: 12px;
}
.header-title h1 {
margin: 0;
font-size: 18px;
font-weight: 700;
color: #1A1A2E;
color: var(--text-primary);
letter-spacing: -0.01em;
}
.header-user {
display: flex;
align-items: center;
gap: 10px;
gap: 12px;
font-size: 13px;
color: var(--text-secondary);
}
.header-user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, var(--brand-primary), var(--brand-primary-light));
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: 14px;
color: #4B5563;
}
.header-user-name {
font-weight: 600;
color: var(--text-primary);
}
.header-logout {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: var(--radius-md);
color: var(--text-muted);
transition: all var(--duration-fast) ease;
text-decoration: none;
}
.header-logout:hover {
background: var(--danger-bg);
color: var(--danger);
text-decoration: none;
font-size: 18px;
padding: 4px;
}
.sidebar-toggle-btn {
background: none;
border: 1px solid #E5E7EB;
border-radius: 6px;
padding: 6px 10px;
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
width: 40px;
height: 40px;
cursor: pointer;
font-size: 16px;
color: #4B5563;
display: none;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: all var(--duration-fast) ease;
}
/* ── Page Content ── */
.sidebar-toggle-btn:hover {
background: var(--surface-bg);
border-color: var(--border-medium);
}
/* ══════════════════════════════════════════════════
PAGE CONTENT
══════════════════════════════════════════════════ */
.page-content {
flex: 1;
padding: 25px;
padding: 28px;
animation: fadeInUp var(--duration-slow) var(--ease-out);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ── Footer ── */
/* ══════════════════════════════════════════════════
FOOTER
══════════════════════════════════════════════════ */
.page-footer {
padding: 15px 25px;
background: #fff;
border-top: 1px solid #E5E7EB;
padding: 16px 28px;
background: var(--surface-card);
border-top: 1px solid var(--border-light);
display: flex;
justify-content: space-between;
font-size: 12px;
color: #9CA3AF;
color: var(--text-muted);
}
/* ── Cards ── */
/* ══════════════════════════════════════════════════
CARDS
══════════════════════════════════════════════════ */
.card {
background: #fff;
border-radius: 8px;
border: 1px solid #E5E7EB;
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
background: var(--surface-card);
border-radius: var(--radius-lg);
border: 1px solid var(--border-light);
box-shadow: var(--shadow-sm);
margin-bottom: 0;
overflow: hidden;
transition: box-shadow var(--duration-normal) var(--ease-out),
transform var(--duration-normal) var(--ease-out),
border-color var(--duration-normal) ease;
}
/* ── Tables ── */
.table-responsive {
overflow-x: auto;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
.card:hover {
box-shadow: var(--shadow-md);
}
.data-table thead {
background: #0D7377;
color: #fff;
.card-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border-light);
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(to bottom, rgba(248, 250, 252, 0.5), transparent);
}
.data-table th {
padding: 12px 15px;
text-align: right;
font-weight: 600;
white-space: nowrap;
.card-header h2,
.card-header h3 {
margin: 0;
font-size: 16px;
font-weight: 700;
color: var(--text-primary);
}
.data-table td {
padding: 10px 15px;
border-bottom: 1px solid #F3F4F6;
text-align: right;
.card-body {
padding: 24px;
}
.data-table tbody tr:hover {
background: #F9FAFB;
.card-footer {
padding: 16px 24px;
border-top: 1px solid var(--border-light);
background: rgba(248, 250, 252, 0.5);
}
/* ── Buttons ── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
/* Interactive Card */
.card-interactive {
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s ease;
white-space: nowrap;
font-family: inherit;
}
.btn-primary {
background: #0D7377;
color: #fff;
border-color: #0D7377;
.card-interactive:hover {
border-color: rgba(var(--brand-primary-rgb), 0.3);
box-shadow: var(--shadow-card-hover);
transform: translateY(-2px);
}
.btn-primary:hover {
background: #0a5c5f;
/* ══════════════════════════════════════════════════
STATS CARDS
══════════════════════════════════════════════════ */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 28px;
}
.btn-outline {
background: transparent;
color: #0D7377;
border-color: #D1D5DB;
.stats-card {
background: var(--surface-card);
border-radius: var(--radius-lg);
border: 1px solid var(--border-light);
padding: 24px;
display: flex;
align-items: flex-start;
gap: 16px;
position: relative;
overflow: hidden;
transition: all var(--duration-normal) var(--ease-out);
}
.btn-outline:hover {
background: #F3F4F6;
border-color: #0D7377;
.stats-card::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 3px;
border-radius: var(--radius-full);
transition: height var(--duration-normal) var(--ease-out);
}
.btn-sm {
padding: 4px 10px;
font-size: 12px;
.stats-card:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-lg);
}
/* ── Forms ── */
.form-group {
margin-bottom: 0;
.stats-card:hover::before {
height: 4px;
}
.form-label {
display: block;
margin-bottom: 5px;
font-size: 13px;
font-weight: 600;
color: #374151;
.stats-card-primary::before { background: linear-gradient(90deg, var(--brand-primary), var(--brand-primary-light)); }
.stats-card-success::before { background: linear-gradient(90deg, var(--success), #34d399); }
.stats-card-danger::before { background: linear-gradient(90deg, var(--danger), #f87171); }
.stats-card-warning::before { background: linear-gradient(90deg, var(--warning), #fbbf24); }
.stats-card-icon {
width: 48px;
height: 48px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: transform var(--duration-normal) var(--ease-spring);
}
.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;
.stats-card:hover .stats-card-icon {
transform: scale(1.1);
}
.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);
.stats-card-primary .stats-card-icon { background: rgba(var(--brand-primary-rgb), 0.1); color: var(--brand-primary); }
.stats-card-success .stats-card-icon { background: rgba(5, 150, 105, 0.1); color: var(--success); }
.stats-card-danger .stats-card-icon { background: rgba(220, 38, 38, 0.1); color: var(--danger); }
.stats-card-warning .stats-card-icon { background: rgba(217, 119, 6, 0.1); color: var(--warning); }
.stats-card-icon svg {
width: 24px;
height: 24px;
}
.form-textarea {
resize: vertical;
min-height: 80px;
.stats-card-content {
flex: 1;
min-width: 0;
}
/* ── Alerts ── */
.alert {
padding: 12px 20px;
border-radius: 8px;
margin: 0 25px 15px;
font-size: 14px;
.stats-card-title {
font-size: 13px;
font-weight: 500;
display: flex;
justify-content: space-between;
align-items: center;
color: var(--text-muted);
margin-bottom: 4px;
}
.alert-success {
background: #F0FDF4;
color: #059669;
border: 1px solid #BBF7D0;
.stats-card-value {
font-size: 28px;
font-weight: 800;
color: var(--text-primary);
line-height: 1.2;
letter-spacing: -0.02em;
}
.alert-error {
background: #FEF2F2;
color: #DC2626;
border: 1px solid #FECACA;
.stats-card-change {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 600;
margin-top: 6px;
padding: 2px 8px;
border-radius: var(--radius-full);
}
.alert-warning {
background: #FFF7ED;
color: #D97706;
border: 1px solid #FED7AA;
.stats-card-change.positive {
color: var(--success);
background: var(--success-bg);
}
.alert-info {
background: #EFF6FF;
color: #0284C7;
border: 1px solid #BFDBFE;
.stats-card-change.negative {
color: var(--danger);
background: var(--danger-bg);
}
.alert-close {
background: none;
border: none;
cursor: pointer;
font-size: 18px;
color: inherit;
opacity: 0.6;
padding: 0 5px;
.stats-card-link {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 10px 24px;
font-size: 12px;
font-weight: 600;
color: var(--brand-primary);
text-align: center;
border-top: 1px solid var(--border-light);
transition: all var(--duration-fast) ease;
opacity: 0;
transform: translateY(100%);
}
.alert-close:hover {
.stats-card:hover .stats-card-link {
opacity: 1;
transform: translateY(0);
background: rgba(var(--brand-primary-rgb), 0.04);
}
/* ── Responsive ── */
@media (max-width: 1024px) {
.sidebar {
transform: translateX(260px);
width: 260px;
}
.sidebar.show {
transform: translateX(0);
}
.main-wrapper {
margin-right: 0;
}
.sidebar-toggle-btn {
display: block;
}
/* ══════════════════════════════════════════════════
TABLES
══════════════════════════════════════════════════ */
.table-responsive {
overflow-x: auto;
border-radius: var(--radius-lg);
}
/* ── Print ── */
@media print {
.sidebar, .top-header, .page-footer, .btn, .sidebar-toggle-btn {
display: none !important;
}
.main-wrapper {
margin-right: 0 !important;
}
.page-content {
padding: 0 !important;
}
.data-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 13.5px;
}
/* ── Body Reset ── */
* {
box-sizing: border-box;
.data-table thead {
position: sticky;
top: 0;
z-index: 1;
}
body {
margin: 0;
padding: 0;
font-family: 'Cairo', 'Segoe UI', Tahoma, Arial, sans-serif;
font-size: 14px;
color: #1A1A2E;
background: #F3F4F6;
direction: rtl;
line-height: 1.6;
.data-table thead th {
padding: 14px 18px;
text-align: right;
font-weight: 600;
white-space: nowrap;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
background: var(--surface-bg);
border-bottom: 2px solid var(--border-light);
}
a {
color: #0D7377;
text-decoration: none;
.data-table tbody td {
padding: 14px 18px;
border-bottom: 1px solid var(--border-light);
text-align: right;
color: var(--text-secondary);
transition: background var(--duration-fast) ease;
}
a:hover {
text-decoration: underline;
.data-table tbody tr {
transition: all var(--duration-fast) ease;
}
code {
background: #F3F4F6;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
font-family: 'Courier New', monospace;
}
\ No newline at end of file
.data-table tbody tr:hover {
background: rgba(var(--brand-primary-rgb), 0.03);
}
.data-table tbody tr:hover td {
color: var(--text-primary);
}
.data-table tbody tr:last-child td {
border-bottom: none;
}
/* Table row entrance animation */
.data-table tbody tr {
animation: tableRowIn var(--duration-normal) var(--ease-out) backwards;
}
.data-table tbody tr:nth-child(1) { animation-delay: 0ms; }
.data-table tbody tr:nth-child(2) { animation-delay: 30ms; }
.data-table tbody tr:nth-child(3) { animation-delay: 60ms; }
.data-table tbody tr:nth-child(4) { animation-delay: 90ms; }
.data-table tbody tr:nth-child(5) { animation-delay: 120ms; }
.data-table tbody tr:nth-child(6) { animation-delay: 150ms; }
.data-table tbody tr:nth-child(7) { animation-delay: 180ms; }
.data-table tbody tr:nth-child(8) { animation-delay: 210ms; }
.data-table tbody tr:nth-child(9) { animation-delay: 240ms; }
.data-table tbody tr:nth-child(10) { animation-delay: 270ms; }
@keyframes tableRowIn {
from {
opacity: 0;
transform: translateX(8px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.actions-col {
width: 1%;
white-space: nowrap;
}
.action-buttons {
display: flex;
gap: 6px;
align-items: center;
}
/* ══════════════════════════════════════════════════
BUTTONS
══════════════════════════════════════════════════ */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 9px 20px;
border-radius: var(--radius-md);
font-size: 14px;
font-weight: 600;
text-decoration: none;
cursor: pointer;
border: 1px solid transparent;
transition: all var(--duration-normal) var(--ease-out);
white-space: nowrap;
font-family: inherit;
position: relative;
overflow: hidden;
line-height: 1.5;
}
.btn::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(to bottom, rgba(255,255,255,0.1), transparent);
pointer-events: none;
opacity: 0;
transition: opacity var(--duration-fast) ease;
}
.btn:hover::after {
opacity: 1;
}
.btn:active {
transform: scale(0.97);
}
/* Button ripple effect */
.btn-ripple {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: scale(0);
animation: ripple 0.6s ease-out;
pointer-events: none;
}
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
/* Button Variants */
.btn-primary {
background: linear-gradient(135deg, var(--brand-primary), var(--brand-primary-dark));
color: #fff;
box-shadow: 0 2px 8px rgba(var(--brand-primary-rgb), 0.3);
}
.btn-primary:hover {
box-shadow: 0 4px 16px rgba(var(--brand-primary-rgb), 0.4);
transform: translateY(-1px);
text-decoration: none;
color: #fff;
}
.btn-secondary {
background: var(--surface-bg);
color: var(--text-secondary);
border-color: var(--border-light);
}
.btn-secondary:hover {
background: var(--border-light);
color: var(--text-primary);
text-decoration: none;
}
.btn-outline {
background: transparent;
color: var(--brand-primary);
border-color: var(--border-light);
}
.btn-outline:hover {
background: rgba(var(--brand-primary-rgb), 0.06);
border-color: var(--brand-primary);
text-decoration: none;
}
.btn-danger {
background: linear-gradient(135deg, var(--danger), #b91c1c);
color: #fff;
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.3);
}
.btn-danger:hover {
box-shadow: 0 4px 16px rgba(220, 38, 38, 0.4);
transform: translateY(-1px);
text-decoration: none;
color: #fff;
}
.btn-success {
background: linear-gradient(135deg, var(--success), #047857);
color: #fff;
box-shadow: 0 2px 8px rgba(5, 150, 105, 0.3);
}
.btn-success:hover {
box-shadow: 0 4px 16px rgba(5, 150, 105, 0.4);
transform: translateY(-1px);
text-decoration: none;
color: #fff;
}
.btn-ghost {
background: transparent;
color: var(--text-secondary);
border: none;
padding: 8px 12px;
}
.btn-ghost:hover {
background: var(--surface-bg);
color: var(--text-primary);
text-decoration: none;
}
.btn-sm {
padding: 5px 12px;
font-size: 12px;
border-radius: var(--radius-sm);
}
.btn-lg {
padding: 12px 28px;
font-size: 16px;
border-radius: var(--radius-md);
}
.btn-icon {
padding: 8px;
width: 36px;
height: 36px;
}
.btn-icon.btn-sm {
width: 30px;
height: 30px;
padding: 6px;
}
/* Loading button */
.btn.loading {
color: transparent;
pointer-events: none;
position: relative;
}
.btn.loading::before {
content: '';
position: absolute;
width: 18px;
height: 18px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ══════════════════════════════════════════════════
FORMS
══════════════════════════════════════════════════ */
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 6px;
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
transition: color var(--duration-fast) ease;
}
.form-group:focus-within .form-label {
color: var(--brand-primary);
}
.required-mark {
color: var(--danger);
margin-right: 2px;
}
.form-input,
.form-select,
.form-textarea {
width: 100%;
padding: 10px 14px;
border: 1.5px solid var(--border-light);
border-radius: var(--radius-md);
font-size: 14px;
font-family: inherit;
background: var(--surface-card);
color: var(--text-primary);
transition: all var(--duration-normal) var(--ease-out);
box-sizing: border-box;
line-height: 1.5;
}
.form-input:hover,
.form-select:hover,
.form-textarea:hover {
border-color: var(--border-medium);
}
.form-input:focus,
.form-select:focus,
.form-textarea:focus {
outline: none;
border-color: var(--brand-primary);
box-shadow: 0 0 0 3px rgba(var(--brand-primary-rgb), 0.1),
var(--shadow-sm);
}
.form-input::placeholder,
.form-textarea::placeholder {
color: var(--text-muted);
}
.form-textarea {
resize: vertical;
min-height: 100px;
}
.form-select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: left 12px center;
padding-left: 36px;
}
.form-error {
color: var(--danger);
font-size: 12px;
margin-top: 6px;
display: flex;
align-items: center;
gap: 4px;
animation: shakeX 0.4s ease;
}
@keyframes shakeX {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-4px); }
75% { transform: translateX(4px); }
}
.has-error .form-input,
.has-error .form-select,
.has-error .form-textarea {
border-color: var(--danger);
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.08);
}
.form-help {
display: block;
margin-top: 5px;
font-size: 12px;
color: var(--text-muted);
}
/* Checkbox & Radio */
.checkbox-label,
.radio-label {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
color: var(--text-secondary);
padding: 4px 0;
transition: color var(--duration-fast) ease;
}
.checkbox-label:hover,
.radio-label:hover {
color: var(--text-primary);
}
.checkbox-label input[type="checkbox"],
.radio-label input[type="radio"] {
width: 18px;
height: 18px;
accent-color: var(--brand-primary);
cursor: pointer;
}
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
/* File input */
.form-input[type="file"] {
padding: 8px;
cursor: pointer;
}
.form-input[type="file"]::file-selector-button {
padding: 6px 14px;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
background: var(--surface-bg);
color: var(--text-secondary);
font-family: inherit;
font-size: 13px;
font-weight: 500;
cursor: pointer;
margin-left: 10px;
transition: all var(--duration-fast) ease;
}
.form-input[type="file"]::file-selector-button:hover {
background: var(--border-light);
color: var(--text-primary);
}
/* Form Grid */
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.form-grid-2 { grid-template-columns: repeat(2, 1fr); }
.form-grid-3 { grid-template-columns: repeat(3, 1fr); }
.form-grid-4 { grid-template-columns: repeat(4, 1fr); }
/* Form Section */
.form-section {
margin-bottom: 32px;
}
.form-section-title {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 16px;
padding-bottom: 10px;
border-bottom: 2px solid var(--border-light);
display: flex;
align-items: center;
gap: 8px;
}
/* ══════════════════════════════════════════════════
ALERTS
══════════════════════════════════════════════════ */
.alert {
padding: 14px 20px;
border-radius: var(--radius-md);
margin: 0 28px 16px;
font-size: 14px;
font-weight: 500;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
animation: alertSlideIn var(--duration-slow) var(--ease-out);
border: 1px solid transparent;
position: relative;
overflow: hidden;
}
.alert::before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 4px;
}
@keyframes alertSlideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.alert-content {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.alert-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.alert-icon svg {
width: 20px;
height: 20px;
}
.alert-success {
background: var(--success-bg);
color: var(--success);
border-color: var(--success-border);
}
.alert-success::before { background: var(--success); }
.alert-error {
background: var(--danger-bg);
color: var(--danger);
border-color: var(--danger-border);
}
.alert-error::before { background: var(--danger); }
.alert-warning {
background: var(--warning-bg);
color: var(--warning);
border-color: var(--warning-border);
}
.alert-warning::before { background: var(--warning); }
.alert-info {
background: var(--info-bg);
color: var(--info);
border-color: var(--info-border);
}
.alert-info::before { background: var(--info); }
.alert-close {
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: inherit;
opacity: 0.5;
transition: all var(--duration-fast) ease;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
}
.alert-close:hover {
opacity: 1;
background: rgba(0, 0, 0, 0.06);
}
.alert-close svg {
width: 16px;
height: 16px;
}
/* Alert dismissing animation */
.alert.dismissing {
animation: alertDismiss var(--duration-normal) var(--ease-out) forwards;
}
@keyframes alertDismiss {
to {
opacity: 0;
transform: translateX(20px);
height: 0;
padding: 0;
margin: 0;
border: 0;
}
}
/* ══════════════════════════════════════════════════
TOAST NOTIFICATIONS
══════════════════════════════════════════════════ */
#toast-container {
position: fixed;
top: 80px;
left: 28px;
z-index: var(--z-toast);
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: none;
max-width: 400px;
}
.toast {
padding: 14px 18px;
border-radius: var(--radius-md);
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 12px;
pointer-events: auto;
box-shadow: var(--shadow-xl);
animation: toastSlideIn var(--duration-slow) var(--ease-spring);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid transparent;
position: relative;
overflow: hidden;
}
.toast::before {
content: '';
position: absolute;
bottom: 0;
right: 0;
height: 3px;
background: currentColor;
opacity: 0.3;
animation: toastProgress 5s linear forwards;
}
@keyframes toastProgress {
from { width: 100%; }
to { width: 0%; }
}
@keyframes toastSlideIn {
from {
opacity: 0;
transform: translateX(-40px) scale(0.95);
}
to {
opacity: 1;
transform: translateX(0) scale(1);
}
}
.toast.removing {
animation: toastSlideOut var(--duration-normal) var(--ease-out) forwards;
}
@keyframes toastSlideOut {
to {
opacity: 0;
transform: translateX(-40px) scale(0.95);
}
}
.toast-icon {
width: 20px;
height: 20px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.toast-icon svg {
width: 20px;
height: 20px;
}
.toast-info {
background: rgba(2, 132, 199, 0.95);
color: #fff;
border-color: rgba(2, 132, 199, 0.3);
}
.toast-success {
background: rgba(5, 150, 105, 0.95);
color: #fff;
border-color: rgba(5, 150, 105, 0.3);
}
.toast-error {
background: rgba(220, 38, 38, 0.95);
color: #fff;
border-color: rgba(220, 38, 38, 0.3);
}
.toast-warning {
background: rgba(217, 119, 6, 0.95);
color: #fff;
border-color: rgba(217, 119, 6, 0.3);
}
.toast-close {
background: none;
border: none;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
padding: 4px;
margin-right: auto;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
transition: all var(--duration-fast) ease;
}
.toast-close:hover {
color: #fff;
background: rgba(255, 255, 255, 0.15);
}
.toast-close svg {
width: 14px;
height: 14px;
}
/* ══════════════════════════════════════════════════
MODALS
══════════════════════════════════════════════════ */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(15, 15, 26, 0.5);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
z-index: var(--z-modal);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
animation: overlayFadeIn var(--duration-normal) ease;
}
@keyframes overlayFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal {
background: var(--surface-card);
border-radius: var(--radius-xl);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
width: 100%;
max-height: 90vh;
display: flex;
flex-direction: column;
animation: modalScaleIn var(--duration-slow) var(--ease-spring);
}
@keyframes modalScaleIn {
from {
opacity: 0;
transform: scale(0.92) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modal-small { max-width: 420px; }
.modal-medium { max-width: 600px; }
.modal-large { max-width: 900px; }
.modal-fullscreen { max-width: 95vw; max-height: 95vh; }
.modal-header {
padding: 24px 28px 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-light);
flex-shrink: 0;
}
.modal-title {
margin: 0;
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
}
.modal-close {
background: none;
border: none;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--text-muted);
border-radius: var(--radius-md);
transition: all var(--duration-fast) ease;
}
.modal-close:hover {
background: var(--surface-bg);
color: var(--text-primary);
}
.modal-close svg {
width: 20px;
height: 20px;
}
.modal-body {
padding: 24px 28px;
overflow-y: auto;
flex: 1;
}
.modal-footer {
padding: 20px 28px;
border-top: 1px solid var(--border-light);
display: flex;
justify-content: flex-start;
gap: 10px;
flex-shrink: 0;
}
/* ══════════════════════════════════════════════════
BREADCRUMBS
══════════════════════════════════════════════════ */
.breadcrumbs {
display: flex;
align-items: center;
gap: 8px;
padding: 0 0 20px;
font-size: 13px;
flex-wrap: wrap;
}
.breadcrumb-item {
color: var(--text-muted);
text-decoration: none;
display: flex;
align-items: center;
gap: 6px;
transition: color var(--duration-fast) ease;
}
.breadcrumb-item:hover {
color: var(--brand-primary);
text-decoration: none;
}
.breadcrumb-item.current {
color: var(--text-primary);
font-weight: 600;
}
.breadcrumb-separator {
color: var(--text-muted);
opacity: 0.4;
font-size: 12px;
}
/* ══════════════════════════════════════════════════
PAGINATION
══════════════════════════════════════════════════ */
.pagination-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-top: 1px solid var(--border-light);
flex-wrap: wrap;
gap: 12px;
}
.pagination-info {
font-size: 13px;
color: var(--text-muted);
}
.pagination {
list-style: none;
display: flex;
align-items: center;
gap: 4px;
margin: 0;
padding: 0;
}
.page-link {
display: flex;
align-items: center;
justify-content: center;
min-width: 36px;
height: 36px;
padding: 0 10px;
border-radius: var(--radius-md);
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
text-decoration: none;
transition: all var(--duration-fast) ease;
border: 1px solid transparent;
}
.page-link:hover {
background: var(--surface-bg);
border-color: var(--border-light);
color: var(--text-primary);
text-decoration: none;
}
.page-link.active {
background: var(--brand-primary);
color: #fff;
border-color: var(--brand-primary);
font-weight: 600;
box-shadow: 0 2px 6px rgba(var(--brand-primary-rgb), 0.3);
}
.page-ellipsis {
color: var(--text-muted);
padding: 0 4px;
}
/* ══════════════════════════════════════════════════
EMPTY STATE
══════════════════════════════════════════════════ */
.empty-state {
text-align: center;
padding: 60px 20px;
animation: fadeInUp var(--duration-slow) var(--ease-out);
}
.empty-state-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--surface-bg);
border: 2px dashed var(--border-light);
color: var(--text-muted);
animation: emptyBounce 2s ease-in-out infinite;
}
.empty-state-icon svg {
width: 32px;
height: 32px;
}
@keyframes emptyBounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.empty-state-message {
font-size: 15px;
color: var(--text-muted);
margin-bottom: 20px;
max-width: 400px;
margin-left: auto;
margin-right: auto;
}
/* ══════════════════════════════════════════════════
TABS
══════════════════════════════════════════════════ */
.tabs-container {
margin-bottom: 24px;
}
.tab-nav {
display: flex;
gap: 0;
border-bottom: 2px solid var(--border-light);
overflow-x: auto;
}
.tab-link {
padding: 12px 20px;
font-size: 14px;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
text-decoration: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: all var(--duration-normal) ease;
white-space: nowrap;
display: flex;
align-items: center;
gap: 8px;
}
.tab-link:hover {
color: var(--text-primary);
text-decoration: none;
}
.tab-link.active {
color: var(--brand-primary);
border-bottom-color: var(--brand-primary);
font-weight: 600;
}
.tab-content {
display: none;
animation: fadeInUp var(--duration-normal) var(--ease-out);
}
.tab-content.active {
display: block;
}
/* ══════════════════════════════════════════════════
BADGES & TAGS
══════════════════════════════════════════════════ */
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: var(--radius-full);
font-size: 12px;
font-weight: 600;
line-height: 1.5;
}
.badge-primary { background: rgba(var(--brand-primary-rgb), 0.1); color: var(--brand-primary); }
.badge-success { background: var(--success-bg); color: var(--success); }
.badge-danger { background: var(--danger-bg); color: var(--danger); }
.badge-warning { background: var(--warning-bg); color: var(--warning); }
.badge-info { background: var(--info-bg); color: var(--info); }
.badge-neutral { background: var(--surface-bg); color: var(--text-secondary); }
/* ══════════════════════════════════════════════════
LOADING STATES
══════════════════════════════════════════════════ */
/* Page loading overlay */
.page-loading {
position: fixed;
inset: 0;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(4px);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
opacity: 1;
transition: opacity var(--duration-normal) ease;
}
.page-loading.hidden {
opacity: 0;
pointer-events: none;
}
.loader {
display: flex;
gap: 6px;
}
.loader-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--brand-primary);
animation: loaderBounce 1.4s ease-in-out infinite both;
}
.loader-dot:nth-child(1) { animation-delay: -0.32s; }
.loader-dot:nth-child(2) { animation-delay: -0.16s; }
.loader-dot:nth-child(3) { animation-delay: 0s; }
@keyframes loaderBounce {
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
40% { transform: scale(1); opacity: 1; }
}
/* Spinner */
.spinner {
width: 20px;
height: 20px;
border: 2.5px solid var(--border-light);
border-top-color: var(--brand-primary);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
.spinner-sm { width: 16px; height: 16px; border-width: 2px; }
.spinner-lg { width: 32px; height: 32px; border-width: 3px; }
/* Skeleton loading */
.skeleton {
background: linear-gradient(90deg, var(--border-light) 25%, #e8ecf1 37%, var(--border-light) 63%);
background-size: 200% 100%;
animation: skeleton 1.5s ease-in-out infinite;
border-radius: var(--radius-sm);
}
.skeleton-text { height: 14px; margin-bottom: 8px; }
.skeleton-title { height: 20px; width: 60%; margin-bottom: 12px; }
.skeleton-avatar { width: 40px; height: 40px; border-radius: 50%; }
.skeleton-card { height: 120px; border-radius: var(--radius-lg); }
@keyframes skeleton {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* ══════════════════════════════════════════════════
TOPBAR (header.php component)
══════════════════════════════════════════════════ */
.topbar-right {
display: flex;
align-items: center;
gap: 16px;
}
.topbar-left {
display: flex;
align-items: center;
gap: 16px;
}
.topbar-btn {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 8px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
transition: all var(--duration-fast) ease;
position: relative;
}
.topbar-btn:hover {
background: var(--surface-bg);
color: var(--text-primary);
}
.topbar-search-input {
border: 1.5px solid var(--border-light);
border-radius: var(--radius-full);
padding: 8px 16px;
font-size: 13px;
font-family: inherit;
width: 260px;
background: var(--surface-bg);
color: var(--text-primary);
transition: all var(--duration-normal) var(--ease-out);
}
.topbar-search-input:focus {
outline: none;
border-color: var(--brand-primary);
box-shadow: 0 0 0 3px rgba(var(--brand-primary-rgb), 0.1);
width: 320px;
background: var(--surface-card);
}
.topbar-date {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
.topbar-branch {
font-size: 12px;
color: var(--brand-primary);
font-weight: 600;
padding: 4px 10px;
background: rgba(var(--brand-primary-rgb), 0.08);
border-radius: var(--radius-full);
}
.topbar-notifications {
position: relative;
}
.notif-badge {
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
background: var(--danger);
color: #fff;
font-size: 10px;
font-weight: 700;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
animation: badgePulse 2s ease-in-out infinite;
}
@keyframes badgePulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.topbar-user {
display: flex;
align-items: center;
gap: 10px;
}
.topbar-username {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
}
.topbar-logout {
font-size: 12px;
font-weight: 500;
color: var(--text-muted);
padding: 6px 12px;
border-radius: var(--radius-sm);
transition: all var(--duration-fast) ease;
text-decoration: none;
}
.topbar-logout:hover {
background: var(--danger-bg);
color: var(--danger);
text-decoration: none;
}
/* ══════════════════════════════════════════════════
UTILITY CLASSES
══════════════════════════════════════════════════ */
/* Spacing */
.mt-0 { margin-top: 0; } .mt-1 { margin-top: 4px; } .mt-2 { margin-top: 8px; } .mt-3 { margin-top: 12px; } .mt-4 { margin-top: 16px; } .mt-5 { margin-top: 24px; } .mt-6 { margin-top: 32px; }
.mb-0 { margin-bottom: 0; } .mb-1 { margin-bottom: 4px; } .mb-2 { margin-bottom: 8px; } .mb-3 { margin-bottom: 12px; } .mb-4 { margin-bottom: 16px; } .mb-5 { margin-bottom: 24px; } .mb-6 { margin-bottom: 32px; }
.p-0 { padding: 0; } .p-3 { padding: 12px; } .p-4 { padding: 16px; } .p-5 { padding: 24px; }
/* Flex */
.flex { display: flex; }
.flex-wrap { flex-wrap: wrap; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-1 { gap: 4px; } .gap-2 { gap: 8px; } .gap-3 { gap: 12px; } .gap-4 { gap: 16px; } .gap-5 { gap: 20px; }
/* Text */
.text-center { text-align: center; }
.text-muted { color: var(--text-muted); }
.text-primary { color: var(--brand-primary); }
.text-success { color: var(--success); }
.text-danger { color: var(--danger); }
.text-warning { color: var(--warning); }
.font-bold { font-weight: 700; }
.text-sm { font-size: 12px; }
.text-lg { font-size: 18px; }
.text-xl { font-size: 24px; }
/* Grid */
.grid { display: grid; }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
/* Width */
.w-full { width: 100%; }
/* Truncate */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ══════════════════════════════════════════════════
PRINT STYLES
══════════════════════════════════════════════════ */
@media print {
.sidebar, .top-header, .page-footer, .btn, .sidebar-toggle-btn,
#toast-container, .no-print {
display: none !important;
}
.main-wrapper {
margin-right: 0 !important;
}
.page-content {
padding: 0 !important;
}
.card {
box-shadow: none !important;
border: 1px solid #ddd !important;
}
body {
background: #fff !important;
}
}
/* ══════════════════════════════════════════════════
RESPONSIVE
══════════════════════════════════════════════════ */
@media (max-width: 1024px) {
.sidebar {
transform: translateX(var(--sidebar-width));
width: var(--sidebar-width);
}
.sidebar.show {
transform: translateX(0);
box-shadow: -10px 0 30px rgba(0, 0, 0, 0.2);
}
.main-wrapper {
margin-right: 0;
}
.sidebar-toggle-btn {
display: flex;
}
.page-content {
padding: 20px;
}
.form-grid-2,
.form-grid-3,
.form-grid-4 {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.topbar-search-input {
width: 180px;
}
.topbar-search-input:focus {
width: 220px;
}
}
@media (max-width: 640px) {
.page-content {
padding: 16px;
}
.top-header {
padding: 0 16px;
}
.alert {
margin: 0 16px 12px;
}
.pagination-wrapper {
flex-direction: column;
align-items: stretch;
text-align: center;
}
.pagination {
justify-content: center;
}
.modal {
margin: 12px;
max-height: calc(100vh - 24px);
}
.topbar-search-input {
display: none;
}
}
/* ══════════════════════════════════════════════════
SIDEBAR MOBILE OVERLAY
══════════════════════════════════════════════════ */
.sidebar-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: calc(var(--z-sidebar) - 1);
opacity: 0;
transition: opacity var(--duration-normal) ease;
}
.sidebar-overlay.active {
display: block;
opacity: 1;
}
/* ════════════════════════════════════════════════════════════
THE CLUB ERP — Core JavaScript (Vanilla ES6+)
Premium UI interactions, animations, and utilities.
════════════════════════════════════════════════════════════ */
// ── CSRF Token ──
......@@ -63,30 +64,65 @@ async function ajax(method, url, data = null, options = {}) {
}
}
// ── Toast Notifications ──
// ── Toast Notifications (Upgraded with icons & animations) ──
function toast(message, type = 'info', duration = 5000) {
const container = document.getElementById('toast-container');
if (!container) return;
const iconSvg = {
info: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
success: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
error: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
warning: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
};
const el = document.createElement('div');
el.className = `toast toast-${type}`;
el.innerHTML = `<span>${escapeHtml(message)}</span><button class="toast-close" onclick="this.parentElement.remove()">✕</button>`;
el.innerHTML = `
<span class="toast-icon">${iconSvg[type] || iconSvg.info}</span>
<span>${escapeHtml(message)}</span>
<button class="toast-close" onclick="dismissToast(this.parentElement)">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>`;
container.appendChild(el);
if (duration > 0) {
setTimeout(() => { if (el.parentElement) el.remove(); }, duration);
setTimeout(() => dismissToast(el), duration);
}
}
// ── Modal System ──
function dismissToast(el) {
if (!el || !el.parentElement) return;
el.classList.add('removing');
el.addEventListener('animationend', () => el.remove(), { once: true });
}
// ── Modal System (Upgraded with animations) ──
function openModal(id) {
const modal = document.getElementById(id);
if (modal) modal.style.display = 'flex';
if (modal) {
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
// Re-init icons inside modal
if (window.lucide) lucide.createIcons({ nodes: [modal] });
}
}
function closeModal(id) {
const modal = document.getElementById(id);
if (modal) modal.style.display = 'none';
if (modal) {
modal.style.animation = 'overlayFadeIn 200ms ease reverse forwards';
const inner = modal.querySelector('.modal');
if (inner) {
inner.style.animation = 'modalScaleIn 250ms cubic-bezier(0.16, 1, 0.3, 1) reverse forwards';
}
setTimeout(() => {
modal.style.display = 'none';
modal.style.animation = '';
if (inner) inner.style.animation = '';
document.body.style.overflow = '';
}, 250);
}
}
function confirmModal(title, message, onConfirm) {
......@@ -96,19 +132,26 @@ function confirmModal(title, message, onConfirm) {
<div class="modal modal-small">
<div class="modal-header">
<h3 class="modal-title">${escapeHtml(title)}</h3>
<button class="modal-close" onclick="closeModal('${id}');document.getElementById('${id}').remove();">✕</button>
<button class="modal-close" onclick="closeModal('${id}');setTimeout(function(){var e=document.getElementById('${id}');if(e)e.remove();},300);">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body"><p>${escapeHtml(message)}</p></div>
<div class="modal-body"><p style="color:var(--text-secondary);line-height:1.8;">${escapeHtml(message)}</p></div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('${id}');document.getElementById('${id}').remove();">إلغاء</button>
<button class="btn btn-secondary" onclick="closeModal('${id}');setTimeout(function(){var e=document.getElementById('${id}');if(e)e.remove();},300);">إلغاء</button>
<button class="btn btn-danger" id="${id}-confirm">تأكيد</button>
</div>
</div>
</div>`;
document.body.insertAdjacentHTML('beforeend', html);
document.body.style.overflow = 'hidden';
document.getElementById(`${id}-confirm`).addEventListener('click', function() {
closeModal(id);
document.getElementById(id).remove();
setTimeout(function() {
var e = document.getElementById(id);
if (e) e.remove();
}, 300);
onConfirm();
});
}
......@@ -122,9 +165,23 @@ function toggleSubmenu(el) {
const parent = el.closest('.sidebar-item');
const submenu = parent.querySelector('.sidebar-submenu');
if (submenu) {
const isOpen = submenu.style.display === 'block';
submenu.style.display = isOpen ? 'none' : 'block';
parent.classList.toggle('open');
const isOpen = parent.classList.contains('open');
parent.classList.toggle('open', !isOpen);
if (!isOpen) {
submenu.style.display = 'block';
submenu.style.maxHeight = submenu.scrollHeight + 'px';
submenu.style.overflow = 'hidden';
submenu.style.transition = 'max-height 0.3s cubic-bezier(0.16, 1, 0.3, 1)';
} else {
submenu.style.maxHeight = '0';
submenu.style.overflow = 'hidden';
submenu.style.transition = 'max-height 0.25s ease';
setTimeout(function() {
if (!parent.classList.contains('open')) {
submenu.style.display = 'none';
}
}, 250);
}
}
}
......@@ -156,7 +213,14 @@ async function parseNid(nid, config = {}) {
if (el && data[key] !== undefined) {
el.value = data[key];
el.setAttribute('readonly', 'readonly');
el.style.backgroundColor = '#f3f4f6';
el.style.backgroundColor = 'var(--surface-bg)';
// Brief highlight animation
el.style.borderColor = 'var(--success)';
el.style.boxShadow = '0 0 0 3px rgba(5, 150, 105, 0.1)';
setTimeout(() => {
el.style.borderColor = '';
el.style.boxShadow = '';
}, 2000);
}
}
}
......@@ -174,7 +238,7 @@ function showFormErrors(errors) {
if (group) group.classList.add('has-error');
const errorDiv = document.createElement('div');
errorDiv.className = 'form-error';
errorDiv.textContent = Array.isArray(messages) ? messages[0] : messages;
errorDiv.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg> ` + escapeHtml(Array.isArray(messages) ? messages[0] : messages);
input.parentNode.insertBefore(errorDiv, input.nextSibling);
}
}
......@@ -202,7 +266,7 @@ function initPhoneFormat(inputId) {
function disableFields(selector) {
document.querySelectorAll(selector).forEach(el => {
el.setAttribute('disabled', 'disabled');
el.style.backgroundColor = '#f3f4f6';
el.style.backgroundColor = 'var(--surface-bg)';
});
}
......@@ -274,24 +338,72 @@ function initTabs(containerId) {
});
}
// ── Auto-dismiss alerts ──
// ── Button Ripple Effect ──
function addRipple(e) {
const btn = e.currentTarget;
const rect = btn.getBoundingClientRect();
const ripple = document.createElement('span');
ripple.className = 'btn-ripple';
const size = Math.max(rect.width, rect.height);
ripple.style.width = ripple.style.height = size + 'px';
ripple.style.left = (e.clientX - rect.left - size / 2) + 'px';
ripple.style.top = (e.clientY - rect.top - size / 2) + 'px';
btn.appendChild(ripple);
ripple.addEventListener('animationend', () => ripple.remove());
}
// ── Animated Counter ──
function animateCounter(element, target, duration = 800) {
const start = parseInt(element.textContent) || 0;
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Ease out cubic
const eased = 1 - Math.pow(1 - progress, 3);
const current = Math.round(start + (target - start) * eased);
element.textContent = current.toLocaleString('en-US');
if (progress < 1) requestAnimationFrame(update);
}
requestAnimationFrame(update);
}
// ── DOMContentLoaded ──
document.addEventListener('DOMContentLoaded', function() {
// Auto-dismiss alerts with slide-out
document.querySelectorAll('[data-auto-dismiss]').forEach(el => {
const ms = parseInt(el.dataset.autoDismiss) || 5000;
setTimeout(() => { if (el.parentElement) el.remove(); }, ms);
setTimeout(() => {
el.classList.add('dismissing');
el.addEventListener('animationend', () => el.remove(), { once: true });
}, ms);
});
// Close modals on overlay click
document.addEventListener('click', function(e) {
if (e.target.classList.contains('modal-overlay')) {
e.target.style.display = 'none';
const id = e.target.id;
if (id) closeModal(id);
else {
e.target.style.display = 'none';
document.body.style.overflow = '';
}
}
});
// Escape closes modals
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay').forEach(m => m.style.display = 'none');
document.querySelectorAll('.modal-overlay[style*="display: flex"], .modal-overlay[style*="display:flex"]').forEach(m => {
if (m.id) closeModal(m.id);
else {
m.style.display = 'none';
document.body.style.overflow = '';
}
});
}
});
......@@ -308,6 +420,63 @@ document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('[data-nid-parser]').forEach(input => {
initNationalIdParser(input.id);
});
// Add ripple effect to all buttons
document.querySelectorAll('.btn').forEach(btn => {
btn.addEventListener('click', addRipple);
});
// Animate stat values on page load
document.querySelectorAll('.stats-card-value[data-count]').forEach(el => {
const target = parseInt(el.dataset.count);
if (!isNaN(target)) {
el.textContent = '0';
// Use IntersectionObserver for scroll-triggered animation
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
animateCounter(el, target);
observer.disconnect();
}
});
}, { threshold: 0.5 });
observer.observe(el);
}
});
// Initialize Lucide icons (for dynamically added content)
if (window.lucide) {
lucide.createIcons();
}
// Loading state for forms
document.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', function() {
const submitBtn = form.querySelector('button[type="submit"], input[type="submit"]');
if (submitBtn && !submitBtn.classList.contains('loading')) {
submitBtn.classList.add('loading');
submitBtn.disabled = true;
}
});
});
// Smooth scroll reveal for cards
const revealObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
revealObserver.unobserve(entry.target);
}
});
}, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' });
document.querySelectorAll('.card, .stats-card').forEach(card => {
card.style.opacity = '0';
card.style.transform = 'translateY(16px)';
card.style.transition = 'opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1)';
revealObserver.observe(card);
});
});
// ── Helpers ──
......@@ -319,8 +488,8 @@ function escapeHtml(text) {
// Session timeout warning
(function() {
const timeout = 30 * 60 * 1000; // 30 min
const warningBefore = 5 * 60 * 1000; // 5 min
const timeout = 30 * 60 * 1000;
const warningBefore = 5 * 60 * 1000;
let timer;
function resetTimer() {
......@@ -335,4 +504,4 @@ function escapeHtml(text) {
});
resetTimer();
})();
\ 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