Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
Clubphp
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Clubphp
Commits
de5cfc33
Commit
de5cfc33
authored
Apr 10, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
goooooooooo
parent
048af80b
Changes
16
Show whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
2691 additions
and
437 deletions
+2691
-437
settings.local.json
.claude/settings.local.json
+7
-0
alerts.php
app/Shared/Components/alerts.php
+17
-33
breadcrumbs.php
app/Shared/Components/breadcrumbs.php
+10
-3
data-table.php
app/Shared/Components/data-table.php
+11
-3
empty-state.php
app/Shared/Components/empty-state.php
+17
-4
form-fields.php
app/Shared/Components/form-fields.php
+8
-3
header.php
app/Shared/Components/header.php
+17
-5
modal.php
app/Shared/Components/modal.php
+6
-2
pagination.php
app/Shared/Components/pagination.php
+13
-3
sidebar.php
app/Shared/Components/sidebar.php
+66
-62
stats-card.php
app/Shared/Components/stats-card.php
+26
-9
auth.php
app/Shared/Layout/auth.php
+274
-20
main.php
app/Shared/Layout/main.php
+89
-11
print.php
app/Shared/Layout/print.php
+40
-7
main.css
public/assets/css/main.css
+1898
-249
app.js
public/assets/js/app.js
+192
-23
No files found.
.claude/settings.local.json
0 → 100644
View file @
de5cfc33
{
"permissions"
:
{
"allow"
:
[
"Bash(wc -l /Users/mahmoudaglan/clubphp/app/Core/*.php)"
]
}
}
app/Shared/Components/alerts.php
View file @
de5cfc33
<?php
<?php
/**
/**
* Flash alert messages component.
* Flash alert messages component
— with Lucide icons and animations
.
*/
*/
$session
=
\App\Core\App
::
getInstance
()
->
session
();
$session
=
\App\Core\App
::
getInstance
()
->
session
();
$alerts
=
$session
->
getAlerts
();
$alerts
=
$session
->
getAlerts
();
...
@@ -8,42 +8,26 @@ $alerts = $session->getAlerts();
...
@@ -8,42 +8,26 @@ $alerts = $session->getAlerts();
if
(
empty
(
$alerts
))
{
if
(
empty
(
$alerts
))
{
return
;
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
foreach
(
$alerts
as
$alert
)
:
?>
<?php
<?php
$type
=
$alert
[
'type'
]
??
'info'
;
$type
=
$alert
[
'type'
]
??
'info'
;
$message
=
$alert
[
'message'
]
??
''
;
$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;"
>
<div
class=
"alert alert-
<?=
$type
?>
"
data-auto-dismiss=
"5000"
>
<span>
<?=
$icon
?>
<?=
e
(
$message
)
?>
</span>
<div
class=
"alert-content"
>
<button
onclick=
"this.parentElement.remove()"
style=
"background:none;border:none;cursor:pointer;font-size:18px;color:
<?=
$textColor
?>
;opacity:0.6;padding:0 5px;"
>
×
</button>
<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>
</div>
<?php
endforeach
;
?>
<?php
endforeach
;
?>
app/Shared/Components/breadcrumbs.php
View file @
de5cfc33
<?php
<?php
/**
/**
* Breadcrumbs component — with Lucide home icon and clean separators.
*
* @var array $items [['label'=>..., 'url'=>...]]
* @var array $items [['label'=>..., 'url'=>...]]
*/
*/
$items
=
$items
??
[];
$items
=
$items
??
[];
?>
?>
<?php
if
(
!
empty
(
$items
))
:
?>
<?php
if
(
!
empty
(
$items
))
:
?>
<nav
class=
"breadcrumbs"
>
<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
)
:
?>
<?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'
]))
:
?>
<?php
if
(
$i
<
count
(
$items
)
-
1
&&
!
empty
(
$item
[
'url'
]))
:
?>
<a
href=
"
<?=
e
(
$item
[
'url'
])
?>
"
class=
"breadcrumb-item"
>
<?=
e
(
$item
[
'label'
])
?>
</a>
<a
href=
"
<?=
e
(
$item
[
'url'
])
?>
"
class=
"breadcrumb-item"
>
<?=
e
(
$item
[
'label'
])
?>
</a>
<?php
else
:
?>
<?php
else
:
?>
...
...
app/Shared/Components/data-table.php
View file @
de5cfc33
<?php
<?php
/**
/**
* Data table component — with row animations and action buttons.
*
* @var array $columns [['key'=>..., 'label_ar'=>..., 'sortable'=>bool, 'class'=>'']]
* @var array $columns [['key'=>..., 'label_ar'=>..., 'sortable'=>bool, 'class'=>'']]
* @var array $rows
* @var array $rows
* @var array $pagination (optional)
* @var array $pagination (optional)
...
@@ -11,7 +13,7 @@ $pagination = $pagination ?? null;
...
@@ -11,7 +13,7 @@ $pagination = $pagination ?? null;
$actions
=
$actions
??
[];
$actions
=
$actions
??
[];
?>
?>
<?php
if
(
empty
(
$rows
))
:
?>
<?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
:
?>
<?php
else
:
?>
<div
class=
"table-responsive"
>
<div
class=
"table-responsive"
>
<table
class=
"data-table"
>
<table
class=
"data-table"
>
...
@@ -52,8 +54,14 @@ $actions = $actions ?? [];
...
@@ -52,8 +54,14 @@ $actions = $actions ?? [];
if
(
is_callable
(
$href
))
$href
=
$href
(
$row
);
if
(
is_callable
(
$href
))
$href
=
$href
(
$row
);
$label
=
$action
[
'label'
]
??
''
;
$label
=
$action
[
'label'
]
??
''
;
$class
=
$action
[
'class'
]
??
'btn btn-sm btn-outline'
;
$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
;
?>
<?php
endforeach
;
?>
</div>
</div>
</td>
</td>
...
...
app/Shared/Components/empty-state.php
View file @
de5cfc33
<?php
<?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
??
'لا توجد بيانات لعرضها'
;
$message
=
$message
??
'لا توجد بيانات لعرضها'
;
$icon
=
$icon
??
'📭
'
;
$icon
Name
=
$icon
??
'inbox
'
;
$actionUrl
=
$actionUrl
??
null
;
$actionUrl
=
$actionUrl
??
null
;
$actionLabel
=
$actionLabel
??
null
;
$actionLabel
=
$actionLabel
??
null
;
?>
?>
<div
class=
"empty-state"
>
<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>
<p
class=
"empty-state-message"
>
<?=
e
(
$message
)
?>
</p>
<?php
if
(
$actionUrl
&&
$actionLabel
)
:
?>
<?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
;
?>
<?php
endif
;
?>
</div>
</div>
app/Shared/Components/form-fields.php
View file @
de5cfc33
<?php
<?php
/**
/**
* Renders a form field.
* Renders a form field — with enhanced styling and Lucide icon support.
*
* @var string $type
* @var string $type
* @var string $name
* @var string $name
* @var string $label_ar
* @var string $label_ar
...
@@ -11,6 +12,7 @@
...
@@ -11,6 +12,7 @@
* @var array $options (for select/radio)
* @var array $options (for select/radio)
* @var string $placeholder
* @var string $placeholder
* @var string $help_text
* @var string $help_text
* @var string $icon (optional) Lucide icon name
*/
*/
$type
=
$type
??
'text'
;
$type
=
$type
??
'text'
;
$name
=
$name
??
''
;
$name
=
$name
??
''
;
...
@@ -104,7 +106,10 @@ $id = 'field_' . str_replace(['[', ']', '.'], '_', $name);
...
@@ -104,7 +106,10 @@ $id = 'field_' . str_replace(['[', ']', '.'], '_', $name);
<?php
endif
;
?>
<?php
endif
;
?>
<?php
if
(
$error
)
:
?>
<?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
endif
;
?>
<?php
if
(
$help_text
)
:
?>
<?php
if
(
$help_text
)
:
?>
...
...
app/Shared/Components/header.php
View file @
de5cfc33
<?php
<?php
/**
* Top bar component — with search, notifications, and user profile.
*/
use
App\Core\App
;
use
App\Core\App
;
$employee
=
App
::
getInstance
()
->
currentEmployee
();
$employee
=
App
::
getInstance
()
->
currentEmployee
();
$branch
=
App
::
getInstance
()
->
currentBranch
();
$branch
=
App
::
getInstance
()
->
currentBranch
();
?>
?>
<div
class=
"topbar-right"
>
<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"
>
<div
class=
"topbar-search"
>
<input
type=
"text"
id=
"global-search"
placeholder=
"بحث سريع..."
class=
"topbar-search-input"
autocomplete=
"off"
>
<input
type=
"text"
id=
"global-search"
placeholder=
"بحث سريع..."
class=
"topbar-search-input"
autocomplete=
"off"
>
</div>
</div>
...
@@ -12,17 +17,24 @@ $branch = App::getInstance()->currentBranch();
...
@@ -12,17 +17,24 @@ $branch = App::getInstance()->currentBranch();
<div
class=
"topbar-left"
>
<div
class=
"topbar-left"
>
<span
class=
"topbar-date"
>
<?=
arabic_date
(
today
())
?>
</span>
<span
class=
"topbar-date"
>
<?=
arabic_date
(
today
())
?>
</span>
<?php
if
(
$branch
)
:
?>
<?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
;
?>
<?php
endif
;
?>
<div
class=
"topbar-notifications"
>
<div
class=
"topbar-notifications"
>
<button
class=
"topbar-btn"
id=
"notif-bell"
title=
"الإشعارات"
>
<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>
</button>
</div>
</div>
<?php
if
(
$employee
)
:
?>
<?php
if
(
$employee
)
:
?>
<div
class=
"topbar-user"
>
<div
class=
"topbar-user"
>
<span
class=
"topbar-username"
>
<?=
e
(
$employee
->
full_name_ar
??
(
$employee
[
'full_name_ar'
]
??
'مستخدم'
))
?>
</span>
<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>
</div>
<?php
endif
;
?>
<?php
endif
;
?>
</div>
</div>
app/Shared/Components/modal.php
View file @
de5cfc33
<?php
<?php
/**
/**
* Modal component — with backdrop blur, scale animation, and Lucide close icon.
*
* @var string $id
* @var string $id
* @var string $title
* @var string $title
* @var string $size (small|medium|large|fullscreen)
* @var string $size (small|medium|large|fullscreen)
...
@@ -12,7 +14,9 @@ $size = $size ?? 'medium';
...
@@ -12,7 +14,9 @@ $size = $size ?? 'medium';
<div
class=
"modal modal-
<?=
e
(
$size
)
?>
"
>
<div
class=
"modal modal-
<?=
e
(
$size
)
?>
"
>
<div
class=
"modal-header"
>
<div
class=
"modal-header"
>
<h3
class=
"modal-title"
>
<?=
e
(
$title
)
?>
</h3>
<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>
<div
class=
"modal-body"
>
<div
class=
"modal-body"
>
<?=
$__template
->
yield
(
'modal_body_'
.
$id
,
''
)
?>
<?=
$__template
->
yield
(
'modal_body_'
.
$id
,
''
)
?>
...
...
app/Shared/Components/pagination.php
View file @
de5cfc33
<?php
<?php
/**
/**
* Pagination component — with styled links and Lucide arrow icons.
*
* @var array $pagination
* @var array $pagination
* @var string $baseUrl (optional)
* @var string $baseUrl (optional)
*/
*/
...
@@ -14,7 +16,11 @@ $separator = str_contains($baseUrl, '?') ? '&' : '?';
...
@@ -14,7 +16,11 @@ $separator = str_contains($baseUrl, '?') ? '&' : '?';
</div>
</div>
<ul
class=
"pagination"
>
<ul
class=
"pagination"
>
<?php
if
(
$pagination
[
'has_prev'
])
:
?>
<?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
endif
;
?>
<?php
foreach
(
$pagination
[
'pages'
]
as
$page
)
:
?>
<?php
foreach
(
$pagination
[
'pages'
]
as
$page
)
:
?>
...
@@ -31,7 +37,11 @@ $separator = str_contains($baseUrl, '?') ? '&' : '?';
...
@@ -31,7 +37,11 @@ $separator = str_contains($baseUrl, '?') ? '&' : '?';
<?php
endforeach
;
?>
<?php
endforeach
;
?>
<?php
if
(
$pagination
[
'has_next'
])
:
?>
<?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
;
?>
<?php
endif
;
?>
</ul>
</ul>
</nav>
</nav>
app/Shared/Components/sidebar.php
View file @
de5cfc33
<?php
<?php
/**
/**
* Sidebar component — reads menu items from MenuRegistry.
* Sidebar component — reads menu items from MenuRegistry.
* Uses Lucide icons instead of emojis for a professional look.
*/
*/
use
App\Core\App
;
use
App\Core\App
;
...
@@ -9,47 +10,50 @@ use App\Core\Registries\MenuRegistry;
...
@@ -9,47 +10,50 @@ use App\Core\Registries\MenuRegistry;
$currentPath
=
parse_url
(
$_SERVER
[
'REQUEST_URI'
]
??
'/'
,
PHP_URL_PATH
);
$currentPath
=
parse_url
(
$_SERVER
[
'REQUEST_URI'
]
??
'/'
,
PHP_URL_PATH
);
$employee
=
App
::
getInstance
()
->
currentEmployee
();
$employee
=
App
::
getInstance
()
->
currentEmployee
();
// Icon name →
emoji
mapping
// Icon name →
Lucide icon
mapping
$iconMap
=
[
$iconMap
=
[
'dashboard'
=>
'
📊'
,
'tachometer-alt'
=>
'📊'
,
'home'
=>
'🏠
'
,
'dashboard'
=>
'
layout-dashboard'
,
'tachometer-alt'
=>
'gauge'
,
'home'
=>
'home
'
,
'users'
=>
'
👥'
,
'user'
=>
'👤'
,
'user-plus'
=>
'👤'
,
'user-clock'
=>
'⏰
'
,
'users'
=>
'
users'
,
'user'
=>
'user'
,
'user-plus'
=>
'user-plus'
,
'user-clock'
=>
'user-cog
'
,
'user-tie'
=>
'
👔'
,
'user-shield'
=>
'🛡️'
,
'user-friends'
=>
'👥
'
,
'user-tie'
=>
'
briefcase'
,
'user-shield'
=>
'shield-check'
,
'user-friends'
=>
'users
'
,
'clipboard'
=>
'
📋'
,
'clipboard-list'
=>
'📋'
,
'file-alt'
=>
'📄
'
,
'clipboard'
=>
'
clipboard-list'
,
'clipboard-list'
=>
'clipboard-list'
,
'file-alt'
=>
'file-text
'
,
'calendar'
=>
'
📅'
,
'calendar-alt'
=>
'📅'
,
'calendar-check'
=>
'✅
'
,
'calendar'
=>
'
calendar'
,
'calendar-alt'
=>
'calendar-days'
,
'calendar-check'
=>
'calendar-check
'
,
'money-bill'
=>
'
💰'
,
'wallet'
=>
'💳'
,
'cash-register'
=>
'💰
'
,
'money-bill'
=>
'
banknote'
,
'wallet'
=>
'wallet'
,
'cash-register'
=>
'banknote
'
,
'credit-card'
=>
'
💳'
,
'receipt'
=>
'🧾'
,
'coins'
=>
'🪙
'
,
'credit-card'
=>
'
credit-card'
,
'receipt'
=>
'receipt'
,
'coins'
=>
'coins
'
,
'file-invoice-dollar'
=>
'
💲'
,
'hand-holding-usd'
=>
'💰
'
,
'file-invoice-dollar'
=>
'
file-text'
,
'hand-holding-usd'
=>
'hand-coins
'
,
'exchange-alt'
=>
'
🔀'
,
'random'
=>
'🔀'
,
'transfer'
=>
'🔀
'
,
'exchange-alt'
=>
'
arrow-left-right'
,
'random'
=>
'shuffle'
,
'transfer'
=>
'arrow-left-right
'
,
'gavel'
=>
'
⚖️'
,
'balance-scale'
=>
'⚖️'
,
'exclamation-triangle'
=>
'⚠️
'
,
'gavel'
=>
'
gavel'
,
'balance-scale'
=>
'scale'
,
'exclamation-triangle'
=>
'alert-triangle
'
,
'alert'
=>
'
⚠️'
,
'warning'
=>
'⚠️'
,
'ban'
=>
'🚫
'
,
'alert'
=>
'
alert-triangle'
,
'warning'
=>
'alert-triangle'
,
'ban'
=>
'ban
'
,
'trophy'
=>
'
🏆'
,
'medal'
=>
'🏅'
,
'award'
=>
'🎖️'
,
'star'
=>
'⭐
'
,
'trophy'
=>
'
trophy'
,
'medal'
=>
'medal'
,
'award'
=>
'award'
,
'star'
=>
'star
'
,
'globe'
=>
'
🌍'
,
'globe-americas'
=>
'🌎'
,
'flag'
=>
'🏳️
'
,
'globe'
=>
'
globe'
,
'globe-americas'
=>
'globe-2'
,
'flag'
=>
'flag
'
,
'id-card'
=>
'
🪪'
,
'address-card'
=>
'🪪'
,
'qrcode'
=>
'📱
'
,
'id-card'
=>
'
id-card'
,
'address-card'
=>
'contact'
,
'qrcode'
=>
'qr-code
'
,
'file'
=>
'
📁'
,
'folder'
=>
'📁'
,
'folder-open'
=>
'📂
'
,
'file'
=>
'
folder'
,
'folder'
=>
'folder'
,
'folder-open'
=>
'folder-open
'
,
'sms'
=>
'
📱'
,
'envelope'
=>
'✉️'
,
'bell'
=>
'🔔'
,
'comment'
=>
'💬
'
,
'sms'
=>
'
smartphone'
,
'envelope'
=>
'mail'
,
'bell'
=>
'bell'
,
'comment'
=>
'message-circle
'
,
'chart-bar'
=>
'
📈'
,
'chart-line'
=>
'📈'
,
'chart-pie'
=>
'📊
'
,
'chart-bar'
=>
'
bar-chart-3'
,
'chart-line'
=>
'trending-up'
,
'chart-pie'
=>
'pie-chart
'
,
'cog'
=>
'
⚙️'
,
'cogs'
=>
'⚙️'
,
'wrench'
=>
'🔧'
,
'tools'
=>
'🛠️
'
,
'cog'
=>
'
settings'
,
'cogs'
=>
'settings-2'
,
'wrench'
=>
'wrench'
,
'tools'
=>
'wrench
'
,
'sliders-h'
=>
'
⚙️'
,
'settings'
=>
'⚙️
'
,
'sliders-h'
=>
'
sliders-horizontal'
,
'settings'
=>
'settings
'
,
'shield-alt'
=>
'
🔐'
,
'lock'
=>
'🔒'
,
'key'
=>
'🔑
'
,
'shield-alt'
=>
'
shield'
,
'lock'
=>
'lock'
,
'key'
=>
'key-round
'
,
'building'
=>
'
🏢'
,
'city'
=>
'🏙️'
,
'store'
=>
'🏪
'
,
'building'
=>
'
building-2'
,
'city'
=>
'building'
,
'store'
=>
'store
'
,
'book'
=>
'
📖'
,
'history'
=>
'📜'
,
'archive'
=>
'🗄️
'
,
'book'
=>
'
book-open'
,
'history'
=>
'history'
,
'archive'
=>
'archive
'
,
'sitemap'
=>
'
🔄'
,
'project-diagram'
=>
'🔄'
,
'workflow'
=>
'🔄
'
,
'sitemap'
=>
'
git-branch'
,
'project-diagram'
=>
'workflow'
,
'workflow'
=>
'workflow
'
,
'heart'
=>
'
❤️'
,
'heartbeat'
=>
'💓'
,
'cross'
=>
'✝️
'
,
'heart'
=>
'
heart'
,
'heartbeat'
=>
'heart-pulse'
,
'cross'
=>
'cross
'
,
'ring'
=>
'
💍'
,
'baby'
=>
'👶'
,
'child'
=>
'👦'
,
'children'
=>
'👨👩👧👦
'
,
'ring'
=>
'
gem'
,
'baby'
=>
'baby'
,
'child'
=>
'user'
,
'children'
=>
'users
'
,
'repeat'
=>
'
🔄'
,
'sync'
=>
'🔄'
,
'redo'
=>
'🔄
'
,
'repeat'
=>
'
repeat'
,
'sync'
=>
'refresh-cw'
,
'redo'
=>
'redo
'
,
'print'
=>
'
🖨️'
,
'search'
=>
'🔍'
,
'plus'
=>
'➕'
,
'edit'
=>
'✏️
'
,
'print'
=>
'
printer'
,
'search'
=>
'search'
,
'plus'
=>
'plus'
,
'edit'
=>
'pencil
'
,
'trash'
=>
'
🗑️'
,
'times'
=>
'❌'
,
'check'
=>
'✅
'
,
'trash'
=>
'
trash-2'
,
'times'
=>
'x'
,
'check'
=>
'check
'
,
'dollar-sign'
=>
'
💲'
,
'percentage'
=>
'💹
'
,
'dollar-sign'
=>
'
circle-dollar-sign'
,
'percentage'
=>
'percent
'
,
'swimming-pool'
=>
'
🏊'
,
'running'
=>
'🏃'
,
'futbol'
=>
'⚽
'
,
'swimming-pool'
=>
'
waves'
,
'running'
=>
'activity'
,
'futbol'
=>
'trophy
'
,
'sun'
=>
'
☀️'
,
'umbrella-beach'
=>
'🏖️
'
,
'sun'
=>
'
sun'
,
'umbrella-beach'
=>
'umbrella
'
,
];
];
$getIcon
=
function
(
?
string
$icon
)
use
(
$iconMap
)
:
string
{
$getIcon
=
function
(
?
string
$icon
)
use
(
$iconMap
)
:
string
{
if
(
$icon
===
null
||
$icon
===
''
)
return
'📌'
;
if
(
$icon
===
null
||
$icon
===
''
)
return
'circle'
;
// Already an emoji
// If it's already a lucide icon name (contains hyphen or is a known name)
if
(
mb_strlen
(
$icon
)
<=
2
&&
!
ctype_alpha
(
$icon
))
return
$icon
;
if
(
isset
(
$iconMap
[
$icon
]))
return
$iconMap
[
$icon
];
// Check map
$lower
=
strtolower
(
$icon
);
return
$iconMap
[
$icon
]
??
$iconMap
[
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
// Get all permissions for current employee
...
@@ -102,15 +106,15 @@ usort($menuItems, fn($a, $b) => ($a['order'] ?? 999) <=> ($b['order'] ?? 999));
...
@@ -102,15 +106,15 @@ usort($menuItems, fn($a, $b) => ($a['order'] ?? 999) <=> ($b['order'] ?? 999));
// Fallback if registry empty
// Fallback if registry empty
if
(
empty
(
$menuItems
))
{
if
(
empty
(
$menuItems
))
{
$menuItems
=
[
$menuItems
=
[
[
'key'
=>
'dashboard'
,
'label_ar'
=>
'لوحة التحكم'
,
'icon'
=>
'
📊
'
,
'route'
=>
'/dashboard'
,
'permission'
=>
''
,
'order'
=>
10
,
'children'
=>
[]],
[
'key'
=>
'dashboard'
,
'label_ar'
=>
'لوحة التحكم'
,
'icon'
=>
'
dashboard
'
,
'route'
=>
'/dashboard'
,
'permission'
=>
''
,
'order'
=>
10
,
'children'
=>
[]],
[
'key'
=>
'members'
,
'label_ar'
=>
'إدارة الأعضاء'
,
'icon'
=>
'
👥
'
,
'route'
=>
'/members'
,
'permission'
=>
'member.view'
,
'order'
=>
100
,
'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'
,
'permission'
=>
'member.view'
],
[
'label_ar'
=>
'عضو جديد'
,
'route'
=>
'/members/create'
,
'permission'
=>
'member.create'
],
[
'label_ar'
=>
'عضو جديد'
,
'route'
=>
'/members/create'
,
'permission'
=>
'member.create'
],
]],
]],
[
'key'
=>
'users'
,
'label_ar'
=>
'الموظفون'
,
'icon'
=>
'
👤
'
,
'route'
=>
'/users'
,
'permission'
=>
'user.view'
,
'order'
=>
910
,
'children'
=>
[]],
[
'key'
=>
'users'
,
'label_ar'
=>
'الموظفون'
,
'icon'
=>
'
user
'
,
'route'
=>
'/users'
,
'permission'
=>
'user.view'
,
'order'
=>
910
,
'children'
=>
[]],
[
'key'
=>
'roles'
,
'label_ar'
=>
'الأدوار'
,
'icon'
=>
'
🔐
'
,
'route'
=>
'/roles'
,
'permission'
=>
'role.view'
,
'order'
=>
920
,
'children'
=>
[]],
[
'key'
=>
'roles'
,
'label_ar'
=>
'الأدوار'
,
'icon'
=>
'
shield-alt
'
,
'route'
=>
'/roles'
,
'permission'
=>
'role.view'
,
'order'
=>
920
,
'children'
=>
[]],
[
'key'
=>
'branches'
,
'label_ar'
=>
'الفروع'
,
'icon'
=>
'
🏢
'
,
'route'
=>
'/branches'
,
'permission'
=>
'branch.view'
,
'order'
=>
930
,
'children'
=>
[]],
[
'key'
=>
'branches'
,
'label_ar'
=>
'الفروع'
,
'icon'
=>
'
building
'
,
'route'
=>
'/branches'
,
'permission'
=>
'branch.view'
,
'order'
=>
930
,
'children'
=>
[]],
[
'key'
=>
'settings'
,
'label_ar'
=>
'الإعدادات'
,
'icon'
=>
'
⚙️
'
,
'route'
=>
'/settings'
,
'permission'
=>
'settings.view'
,
'order'
=>
960
,
'children'
=>
[]],
[
'key'
=>
'settings'
,
'label_ar'
=>
'الإعدادات'
,
'icon'
=>
'
settings
'
,
'route'
=>
'/settings'
,
'permission'
=>
'settings.view'
,
'order'
=>
960
,
'children'
=>
[]],
];
];
}
}
?>
?>
...
@@ -126,7 +130,7 @@ if (empty($menuItems)) {
...
@@ -126,7 +130,7 @@ if (empty($menuItems)) {
if
(
!
empty
(
$item
[
'permission'
])
&&
!
$hasPerm
(
$item
[
'permission'
]))
continue
;
if
(
!
empty
(
$item
[
'permission'
])
&&
!
$hasPerm
(
$item
[
'permission'
]))
continue
;
if
(
!
empty
(
$item
[
'is_separator'
]))
{
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
;
continue
;
}
}
...
@@ -148,16 +152,16 @@ if (empty($menuItems)) {
...
@@ -148,16 +152,16 @@ if (empty($menuItems)) {
}
}
$isOpen
=
$itemActive
||
$childActive
;
$isOpen
=
$itemActive
||
$childActive
;
$icon
=
$getIcon
(
$item
[
'icon'
]
??
''
);
$icon
Name
=
$getIcon
(
$item
[
'icon'
]
??
''
);
?>
?>
<li
class=
"sidebar-item
<?=
$isOpen
&&
$hasChildren
?
' open'
:
''
?>
"
>
<li
class=
"sidebar-item
<?=
$isOpen
&&
$hasChildren
?
' open'
:
''
?>
"
>
<?php
if
(
$hasChildren
)
:
?>
<?php
if
(
$hasChildren
)
:
?>
<a
href=
"javascript:void(0)"
class=
"sidebar-link
<?=
$itemActive
?
' active'
:
''
?>
"
onclick=
"toggleSubmenu(this)"
>
<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-text"
>
<?=
e
(
$item
[
'label_ar'
])
?>
</span>
<span
class=
"sidebar-arrow"
>
◀
</span>
<span
class=
"sidebar-arrow"
>
<i
data-lucide=
"chevron-down"
></i>
</span>
</a>
</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
foreach
(
$visibleChildren
as
$child
)
:
?>
<?php
$childRoute
=
$child
[
'route'
]
??
'#'
;
?>
<?php
$childRoute
=
$child
[
'route'
]
??
'#'
;
?>
<li>
<li>
...
@@ -169,7 +173,7 @@ if (empty($menuItems)) {
...
@@ -169,7 +173,7 @@ if (empty($menuItems)) {
</ul>
</ul>
<?php
else
:
?>
<?php
else
:
?>
<a
href=
"
<?=
e
(
$itemRoute
)
?>
"
class=
"sidebar-link
<?=
$itemActive
?
' active'
:
''
?>
"
>
<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>
<span
class=
"sidebar-text"
>
<?=
e
(
$item
[
'label_ar'
])
?>
</span>
</a>
</a>
<?php
endif
;
?>
<?php
endif
;
?>
...
...
app/Shared/Components/stats-card.php
View file @
de5cfc33
<?php
<?php
/**
/**
* Stats card component — with Lucide icon support, animated counters, and hover effects.
*
* @var string $title
* @var string $title
* @var string $value
* @var string $value
* @var string $icon
* @var string $icon
Lucide icon name (e.g. 'users', 'banknote', 'trending-up')
* @var string $color (primary|success|danger|warning)
* @var string $color (primary|success|danger|warning)
* @var string $link (optional)
* @var string $link (optional)
* @var string $change (optional, e.g. "+5%")
* @var string $change (optional, e.g. "+5%")
*/
*/
$color
=
$color
??
'primary'
;
$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 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-content"
>
<div
class=
"stats-card-title"
>
<?=
e
(
$title
??
''
)
?>
</div>
<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
))
:
?>
<?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
;
?>
<?php
endif
;
?>
</div>
</div>
<?php
if
(
!
empty
(
$link
))
:
?>
<?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
;
?>
<?php
endif
;
?>
</div>
</div>
app/Shared/Layout/auth.php
View file @
de5cfc33
...
@@ -7,46 +7,271 @@ use App\Core\CSRF;
...
@@ -7,46 +7,271 @@ use App\Core\CSRF;
<meta
charset=
"UTF-8"
>
<meta
charset=
"UTF-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
>
<meta
name=
"csrf-token"
content=
"
<?=
e
(
CSRF
::
token
())
?>
"
>
<meta
name=
"csrf-token"
content=
"
<?=
e
(
CSRF
::
token
())
?>
"
>
<title>
<?=
$__template
->
yield
(
'title'
,
'تسجيل الدخول'
)
?>
— نادي النادي شيراتون
</title>
<title>
<?=
$__template
->
yield
(
'title'
,
'تسجيل الدخول'
)
?>
— THE CLUB
</title>
<link
rel=
"stylesheet"
href=
"
<?=
url
(
'assets/css/main.css'
)
?>
"
>
<!-- 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>
<style>
*,
*
::before
,
*
::after
{
box-sizing
:
border-box
;
margin
:
0
;
padding
:
0
;
}
body
{
body
{
display
:
flex
;
display
:
flex
;
justify-content
:
center
;
justify-content
:
center
;
align-items
:
center
;
align-items
:
center
;
min-height
:
100vh
;
min-height
:
100vh
;
margin
:
0
;
margin
:
0
;
background
:
linear-gradient
(
135deg
,
#1A1A2E
0%
,
#0D7377
100%
)
;
background
:
#0f0f1a
;
font-family
:
'Cairo'
,
'Segoe UI'
,
Tahoma
,
Arial
,
sans-serif
;
font-family
:
'Cairo'
,
'Segoe UI'
,
system-ui
,
-apple-system
,
sans-serif
;
direction
:
rtl
;
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
{
.auth-card
{
background
:
#fff
;
position
:
relative
;
border-radius
:
12px
;
z-index
:
1
;
box-shadow
:
0
20px
60px
rgba
(
0
,
0
,
0
,
0.3
);
background
:
rgba
(
255
,
255
,
255
,
0.97
);
padding
:
40px
;
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%
;
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
{
.auth-header
{
text-align
:
center
;
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
{
.auth-header
h1
{
color
:
#0D7377
;
color
:
#0f172a
;
font-size
:
24px
;
font-size
:
22px
;
margin
:
0
0
5px
;
font-weight
:
800
;
margin
:
0
0
4px
;
letter-spacing
:
-0.01em
;
}
}
.auth-header
p
{
.auth-header
p
{
color
:
#
6B7280
;
color
:
#
94a3b8
;
font-size
:
14px
;
font-size
:
14px
;
margin
:
0
;
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>
</style>
</head>
</head>
<body>
<body>
<!-- Background effects -->
<div
class=
"grid-bg"
></div>
<div
class=
"particles"
id=
"particles"
></div>
<div
class=
"auth-card"
>
<div
class=
"auth-card"
>
<div
class=
"auth-header"
>
<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>
<h1>
نادي النادي شيراتون
</h1>
<p>
THE CLUB Sheraton
</p>
<p>
THE CLUB Sheraton
</p>
</div>
</div>
...
@@ -56,12 +281,15 @@ use App\Core\CSRF;
...
@@ -56,12 +281,15 @@ use App\Core\CSRF;
$alerts
=
$session
->
getAlerts
();
$alerts
=
$session
->
getAlerts
();
if
(
!
empty
(
$alerts
))
:
if
(
!
empty
(
$alerts
))
:
foreach
(
$alerts
as
$alert
)
:
foreach
(
$alerts
as
$alert
)
:
$isError
=
(
$alert
[
'type'
]
??
''
)
===
'error'
;
?>
?>
<div
style=
"padding:10px 15px;border-radius:6px;margin-bottom:15px;font-size:13px;
<div
class=
"auth-alert
<?=
$isError
?
'auth-alert-error'
:
'auth-alert-success'
?>
"
>
background:
<?=
(
$alert
[
'type'
]
??
''
)
===
'error'
?
'#FEF2F2'
:
'#F0FDF4'
?>
;
<?php
if
(
$isError
)
:
?>
color:
<?=
(
$alert
[
'type'
]
??
''
)
===
'error'
?
'#DC2626'
:
'#059669'
?>
;
<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>
border:1px solid
<?=
(
$alert
[
'type'
]
??
''
)
===
'error'
?
'#FECACA'
:
'#BBF7D0'
?>
;"
>
<?php
else
:
?>
<?=
e
(
$alert
[
'message'
]
??
''
)
?>
<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>
</div>
<?php
<?php
endforeach
;
endforeach
;
...
@@ -69,6 +297,32 @@ use App\Core\CSRF;
...
@@ -69,6 +297,32 @@ use App\Core\CSRF;
?>
?>
<?=
$__template
->
yield
(
'content'
,
''
)
?>
<?=
$__template
->
yield
(
'content'
,
''
)
?>
<div
class=
"auth-footer"
>
THE CLUB ERP
©
<?=
date
(
'Y'
)
?>
</div>
</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>
</body>
</html>
</html>
app/Shared/Layout/main.php
View file @
de5cfc33
...
@@ -10,6 +10,7 @@ use App\Core\CSRF;
...
@@ -10,6 +10,7 @@ use App\Core\CSRF;
$app
=
App
::
getInstance
();
$app
=
App
::
getInstance
();
$employee
=
$app
->
currentEmployee
();
$employee
=
$app
->
currentEmployee
();
$employeeName
=
$employee
?
(
$employee
->
full_name_ar
??
'مستخدم'
)
:
'زائر'
;
$employeeName
=
$employee
?
(
$employee
->
full_name_ar
??
'مستخدم'
)
:
'زائر'
;
$employeeInitial
=
mb_substr
(
$employeeName
,
0
,
1
);
$currentPath
=
parse_url
(
$_SERVER
[
'REQUEST_URI'
]
??
'/'
,
PHP_URL_PATH
);
$currentPath
=
parse_url
(
$_SERVER
[
'REQUEST_URI'
]
??
'/'
,
PHP_URL_PATH
);
?>
?>
<!DOCTYPE html>
<!DOCTYPE html>
...
@@ -18,12 +19,34 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
...
@@ -18,12 +19,34 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
<meta
charset=
"UTF-8"
>
<meta
charset=
"UTF-8"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
>
<meta
name=
"csrf-token"
content=
"
<?=
e
(
CSRF
::
token
())
?>
"
>
<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'
)
?>
"
>
<link
rel=
"stylesheet"
href=
"
<?=
url
(
'assets/css/main.css'
)
?>
"
>
<?=
$__template
->
yield
(
'styles'
,
''
)
?>
<?=
$__template
->
yield
(
'styles'
,
''
)
?>
</head>
</head>
<body>
<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 -->
<!-- Sidebar -->
<aside
class=
"sidebar"
id=
"sidebar"
>
<aside
class=
"sidebar"
id=
"sidebar"
>
<?php
$__template
->
include
(
'Shared.Components.sidebar'
);
?>
<?php
$__template
->
include
(
'Shared.Components.sidebar'
);
?>
...
@@ -33,9 +56,11 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
...
@@ -33,9 +56,11 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
<div
class=
"main-wrapper"
id=
"main-wrapper"
>
<div
class=
"main-wrapper"
id=
"main-wrapper"
>
<!-- Top Header -->
<!-- Top Header -->
<header
class=
"top-header"
>
<header
class=
"top-header"
id=
"top-header"
>
<div
class=
"header-right"
>
<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"
>
<div
class=
"header-title"
>
<h1>
<?=
$__template
->
yield
(
'title'
,
'لوحة التحكم'
)
?>
</h1>
<h1>
<?=
$__template
->
yield
(
'title'
,
'لوحة التحكم'
)
?>
</h1>
</div>
</div>
...
@@ -44,7 +69,10 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
...
@@ -44,7 +69,10 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
<?=
$__template
->
yield
(
'page_actions'
,
''
)
?>
<?=
$__template
->
yield
(
'page_actions'
,
''
)
?>
<div
class=
"header-user"
>
<div
class=
"header-user"
>
<span
class=
"header-user-name"
>
<?=
e
(
$employeeName
)
?>
</span>
<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>
</div>
</div>
</header>
</header>
...
@@ -52,6 +80,9 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
...
@@ -52,6 +80,9 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
<!-- Alerts -->
<!-- Alerts -->
<?php
$__template
->
include
(
'Shared.Components.alerts'
);
?>
<?php
$__template
->
include
(
'Shared.Components.alerts'
);
?>
<!-- Toast Container -->
<div
id=
"toast-container"
></div>
<!-- Page Content -->
<!-- Page Content -->
<main
class=
"page-content"
>
<main
class=
"page-content"
>
<?=
$__template
->
yield
(
'content'
,
''
)
?>
<?=
$__template
->
yield
(
'content'
,
''
)
?>
...
@@ -59,29 +90,76 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
...
@@ -59,29 +90,76 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
<!-- Footer -->
<!-- Footer -->
<footer
class=
"page-footer"
>
<footer
class=
"page-footer"
>
<span>
نادي النادي شيراتون
©
<?=
date
(
'Y'
)
?>
</span>
<span>
THE CLUB Sheraton
©
<?=
date
(
'Y'
)
?>
</span>
<span>
الإصدار
1.0.0
</span>
<span>
v
1.0.0
</span>
<span>
<?=
arabic_date
(
date
(
'Y-m-d'
))
?>
</span>
<span>
<?=
arabic_date
(
date
(
'Y-m-d'
))
?>
</span>
</footer>
</footer>
</div>
</div>
<script
src=
"
<?=
url
(
'assets/js/app.js'
)
?>
"
></script>
<script
src=
"
<?=
url
(
'assets/js/app.js'
)
?>
"
></script>
<script>
<script>
// Initialize Lucide icons
lucide
.
createIcons
();
// Sidebar functions
function
toggleSidebar
()
{
function
toggleSidebar
()
{
var
sb
=
document
.
getElementById
(
'sidebar'
);
var
sb
=
document
.
getElementById
(
'sidebar'
);
var
mw
=
document
.
getElementById
(
'main-wrapper'
);
var
mw
=
document
.
getElementById
(
'main-wrapper'
);
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'
);
sb
.
classList
.
toggle
(
'collapsed'
);
mw
.
classList
.
toggle
(
'sidebar-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
)
{
function
toggleSubmenu
(
el
)
{
var
parent
=
el
.
parentElement
;
var
parent
=
el
.
parentElement
;
var
submenu
=
parent
.
querySelector
(
'.sidebar-submenu'
);
var
submenu
=
parent
.
querySelector
(
'.sidebar-submenu'
);
if
(
submenu
)
{
if
(
submenu
)
{
var
isOpen
=
submenu
.
style
.
display
===
'block'
;
var
isOpen
=
parent
.
classList
.
contains
(
'open'
);
submenu
.
style
.
display
=
isOpen
?
'none'
:
'block'
;
parent
.
classList
.
toggle
(
'open'
,
!
isOpen
);
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>
</script>
<?=
$__template
->
yield
(
'scripts'
,
''
)
?>
<?=
$__template
->
yield
(
'scripts'
,
''
)
?>
</body>
</body>
...
...
app/Shared/Layout/print.php
View file @
de5cfc33
...
@@ -3,16 +3,49 @@
...
@@ -3,16 +3,49 @@
<head>
<head>
<meta
charset=
"UTF-8"
>
<meta
charset=
"UTF-8"
>
<title>
<?=
$__template
->
yield
(
'title'
,
'طباعة'
)
?>
</title>
<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>
<style>
body
{
padding
:
20px
;
background
:
#fff
;
}
*
{
box-sizing
:
border-box
;
margin
:
0
;
padding
:
0
;
}
@media
print
{
body
{
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>
</style>
</head>
</head>
<body>
<body>
<div
class=
"print-header"
style=
"text-align:center;margin-bottom:20px;border-bottom:2px solid #0D7377;padding-bottom:10px;"
>
<div
class=
"print-header"
>
<h2
style=
"color:#0D7377;margin:0;"
>
نادي النادي شيراتون
</h2>
<h2>
نادي النادي شيراتون
</h2>
<p
style=
"color:#6B7280;margin:5px 0;"
>
<?=
arabic_date
(
today
())
?>
</p>
<p>
<?=
arabic_date
(
today
())
?>
</p>
</div>
</div>
<?=
$__template
->
yield
(
'content'
,
''
)
?>
<?=
$__template
->
yield
(
'content'
,
''
)
?>
...
...
public/assets/css/main.css
View file @
de5cfc33
/* ═══════════════════════════════════════════
/* ═══════════════════════════════════════════════════════════════
ADD THESE TO THE END OF YOUR main.css
THE CLUB ERP — Premium UI System
IF SIDEBAR STYLES ARE MISSING
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
;
}
/* ── Sidebar ── */
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
;
position
:
fixed
;
top
:
0
;
top
:
0
;
right
:
0
;
right
:
0
;
width
:
260px
;
width
:
var
(
--sidebar-width
)
;
height
:
100vh
;
height
:
100vh
;
background
:
#1A1A2E
;
background
:
var
(
--sidebar-bg
)
;
color
:
#
E5E7EB
;
color
:
#
e2e8f0
;
overflow-y
:
auto
;
overflow-y
:
auto
;
overflow-x
:
hidden
;
overflow-x
:
hidden
;
z-index
:
1000
;
z-index
:
var
(
--z-sidebar
);
transition
:
width
0.3s
ease
,
transform
0.3s
ease
;
transition
:
width
var
(
--duration-slow
)
var
(
--ease-out
),
transform
var
(
--duration-slow
)
var
(
--ease-out
);
display
:
flex
;
display
:
flex
;
flex-direction
:
column
;
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
{
.sidebar.collapsed
{
width
:
0
;
width
:
0
;
transform
:
translateX
(
260px
);
transform
:
translateX
(
var
(
--sidebar-width
)
);
}
}
/* Sidebar Header */
.sidebar-header
{
.sidebar-header
{
padding
:
2
0px
15
px
;
padding
:
2
4px
20
px
;
border-bottom
:
1px
solid
rgba
(
255
,
255
,
255
,
0.1
);
border-bottom
:
1px
solid
var
(
--sidebar-border
);
display
:
flex
;
display
:
flex
;
justify-content
:
space-between
;
justify-content
:
space-between
;
align-items
:
center
;
align-items
:
center
;
flex-shrink
:
0
;
flex-shrink
:
0
;
position
:
relative
;
}
}
.sidebar-brand
{
.sidebar-brand
{
font-size
:
18px
;
font-size
:
20px
;
font-weight
:
700
;
font-weight
:
800
;
color
:
#14b8a6
;
letter-spacing
:
2px
;
letter-spacing
:
1px
;
background
:
linear-gradient
(
135deg
,
var
(
--brand-primary-light
),
#38bdf8
);
-webkit-background-clip
:
text
;
-webkit-text-fill-color
:
transparent
;
background-clip
:
text
;
}
}
.sidebar-toggle
{
.sidebar-toggle
{
background
:
none
;
background
:
none
;
border
:
none
;
border
:
none
;
color
:
#9CA3AF
;
color
:
var
(
--text-muted
)
;
font-size
:
18px
;
font-size
:
18px
;
cursor
:
pointer
;
cursor
:
pointer
;
padding
:
5px
;
padding
:
5px
;
transition
:
color
var
(
--duration-fast
)
ease
;
}
}
.sidebar-toggle
:hover
{
.sidebar-toggle
:hover
{
color
:
#fff
;
color
:
#fff
;
}
}
/* Sidebar Navigation */
.sidebar-nav
{
.sidebar-nav
{
flex
:
1
;
flex
:
1
;
overflow-y
:
auto
;
overflow-y
:
auto
;
padding
:
1
0
px
0
;
padding
:
1
2
px
0
;
}
}
.sidebar-menu
{
.sidebar-menu
{
...
@@ -66,40 +229,79 @@
...
@@ -66,40 +229,79 @@
margin
:
0
;
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
{
.sidebar-item
{
margin
:
2px
0
;
margin
:
1px
8px
;
}
}
.sidebar-link
{
.sidebar-link
{
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
gap
:
1
0
px
;
gap
:
1
2
px
;
padding
:
10px
20
px
;
padding
:
10px
16
px
;
color
:
#
D1D5DB
;
color
:
#
94a3b8
;
text-decoration
:
none
;
text-decoration
:
none
;
font-size
:
1
4
px
;
font-size
:
1
3.5
px
;
font-weight
:
500
;
font-weight
:
500
;
transition
:
all
0.2s
ease
;
transition
:
all
var
(
--duration-normal
)
var
(
--ease-out
)
;
border-radius
:
0
;
border-radius
:
var
(
--radius-md
)
;
position
:
relative
;
position
:
relative
;
}
}
.sidebar-link
:hover
{
.sidebar-link
:hover
{
background
:
rgba
(
255
,
255
,
255
,
0.08
);
background
:
var
(
--sidebar-item-hover
);
color
:
#fff
;
color
:
#e2e8f0
;
text-decoration
:
none
;
}
}
.sidebar-link.active
{
.sidebar-link.active
{
background
:
rgba
(
13
,
115
,
119
,
0.3
);
background
:
var
(
--sidebar-item-active
);
color
:
#14b8a6
;
color
:
var
(
--brand-primary-light
);
border-left
:
3px
solid
#14b8a6
;
}
.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
{
.sidebar-icon
{
font-size
:
16px
;
width
:
20px
;
width
:
24px
;
height
:
20px
;
text-align
:
center
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
flex-shrink
:
0
;
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
{
.sidebar-text
{
...
@@ -109,372 +311,1819 @@
...
@@ -109,372 +311,1819 @@
text-overflow
:
ellipsis
;
text-overflow
:
ellipsis
;
}
}
/* Sidebar Arrow */
.sidebar-arrow
{
.sidebar-arrow
{
font-size
:
10px
;
display
:
flex
;
transition
:
transform
0.2s
ease
;
align-items
:
center
;
color
:
#6B7280
;
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
{
.sidebar-item.open
>
.sidebar-link
.sidebar-arrow
{
transform
:
rotate
(
-90deg
);
transform
:
rotate
(
-90deg
);
}
}
/* Sidebar Submenu */
.sidebar-submenu
{
.sidebar-submenu
{
list-style
:
none
;
list-style
:
none
;
padding
:
0
;
padding
:
2px
0
6px
;
margin
:
0
;
margin
:
0
;
background
:
rgba
(
0
,
0
,
0
,
0.15
);
overflow
:
hidden
;
}
.sidebar-submenu
li
{
margin
:
0
4px
;
}
}
.sidebar-sublink
{
.sidebar-sublink
{
display
:
block
;
display
:
flex
;
padding
:
8px
20px
8px
45px
;
align-items
:
center
;
color
:
#9CA3AF
;
gap
:
8px
;
padding
:
7px
16px
7px
16px
;
margin-right
:
28px
;
color
:
rgba
(
148
,
163
,
184
,
0.7
);
text-decoration
:
none
;
text-decoration
:
none
;
font-size
:
13px
;
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
{
.sidebar-sublink
:hover
{
color
:
#fff
;
color
:
#e2e8f0
;
background
:
rgba
(
255
,
255
,
255
,
0.05
);
background
:
rgba
(
255
,
255
,
255
,
0.03
);
text-decoration
:
none
;
}
}
.sidebar-sublink.active
{
.sidebar-sublink.active
{
color
:
#14b8a6
;
color
:
var
(
--brand-primary-light
);
border-right-color
:
var
(
--brand-primary-light
);
font-weight
:
600
;
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
{
.main-wrapper
{
margin-right
:
260px
;
margin-right
:
var
(
--sidebar-width
)
;
min-height
:
100vh
;
min-height
:
100vh
;
display
:
flex
;
display
:
flex
;
flex-direction
:
column
;
flex-direction
:
column
;
transition
:
margin-right
0.3s
ease
;
transition
:
margin-right
var
(
--duration-slow
)
var
(
--ease-out
)
;
background
:
#F3F4F6
;
background
:
var
(
--surface-bg
)
;
}
}
.main-wrapper.sidebar-collapsed
{
.main-wrapper.sidebar-collapsed
{
margin-right
:
0
;
margin-right
:
0
;
}
}
/* ── Top Header ── */
/* ══════════════════════════════════════════════════
TOP HEADER
══════════════════════════════════════════════════ */
.top-header
{
.top-header
{
background
:
#fff
;
background
:
rgba
(
255
,
255
,
255
,
0.85
);
border-bottom
:
1px
solid
#E5E7EB
;
backdrop-filter
:
blur
(
12px
);
padding
:
12px
25px
;
-webkit-backdrop-filter
:
blur
(
12px
);
border-bottom
:
1px
solid
var
(
--border-light
);
padding
:
0
28px
;
height
:
64px
;
display
:
flex
;
display
:
flex
;
justify-content
:
space-between
;
justify-content
:
space-between
;
align-items
:
center
;
align-items
:
center
;
position
:
sticky
;
position
:
sticky
;
top
:
0
;
top
:
0
;
z-index
:
100
;
z-index
:
var
(
--z-sticky
);
box-shadow
:
0
1px
3px
rgba
(
0
,
0
,
0
,
0.05
);
transition
:
box-shadow
var
(
--duration-normal
)
ease
;
}
.top-header.scrolled
{
box-shadow
:
var
(
--shadow-md
);
}
}
.header-right
{
.header-right
{
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
gap
:
1
5
px
;
gap
:
1
6
px
;
}
}
.header-left
{
.header-left
{
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
gap
:
1
5
px
;
gap
:
1
2
px
;
}
}
.header-title
h1
{
.header-title
h1
{
margin
:
0
;
margin
:
0
;
font-size
:
18px
;
font-size
:
18px
;
font-weight
:
700
;
font-weight
:
700
;
color
:
#1A1A2E
;
color
:
var
(
--text-primary
);
letter-spacing
:
-0.01em
;
}
}
.header-user
{
.header-user
{
display
:
flex
;
display
:
flex
;
align-items
:
center
;
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
;
font-size
:
14px
;
color
:
#4B5563
;
}
}
.header-user-name
{
.header-user-name
{
font-weight
:
600
;
font-weight
:
600
;
color
:
var
(
--text-primary
);
}
}
.header-logout
{
.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
;
text-decoration
:
none
;
font-size
:
18px
;
padding
:
4px
;
}
}
.sidebar-toggle-btn
{
.sidebar-toggle-btn
{
background
:
none
;
background
:
none
;
border
:
1px
solid
#E5E7EB
;
border
:
1px
solid
var
(
--border-light
);
border-radius
:
6px
;
border-radius
:
var
(
--radius-md
);
padding
:
6px
10px
;
width
:
40px
;
height
:
40px
;
cursor
:
pointer
;
cursor
:
pointer
;
font-size
:
16px
;
color
:
#4B5563
;
display
:
none
;
display
:
none
;
align-items
:
center
;
justify-content
:
center
;
color
:
var
(
--text-secondary
);
transition
:
all
var
(
--duration-fast
)
ease
;
}
.sidebar-toggle-btn
:hover
{
background
:
var
(
--surface-bg
);
border-color
:
var
(
--border-medium
);
}
}
/* ── Page Content ── */
/* ══════════════════════════════════════════════════
PAGE CONTENT
══════════════════════════════════════════════════ */
.page-content
{
.page-content
{
flex
:
1
;
flex
:
1
;
padding
:
25px
;
padding
:
28px
;
animation
:
fadeInUp
var
(
--duration-slow
)
var
(
--ease-out
);
}
}
/* ── Footer ── */
@keyframes
fadeInUp
{
from
{
opacity
:
0
;
transform
:
translateY
(
12px
);
}
to
{
opacity
:
1
;
transform
:
translateY
(
0
);
}
}
/* ══════════════════════════════════════════════════
FOOTER
══════════════════════════════════════════════════ */
.page-footer
{
.page-footer
{
padding
:
1
5px
25
px
;
padding
:
1
6px
28
px
;
background
:
#fff
;
background
:
var
(
--surface-card
)
;
border-top
:
1px
solid
#E5E7EB
;
border-top
:
1px
solid
var
(
--border-light
)
;
display
:
flex
;
display
:
flex
;
justify-content
:
space-between
;
justify-content
:
space-between
;
font-size
:
12px
;
font-size
:
12px
;
color
:
#9CA3AF
;
color
:
var
(
--text-muted
)
;
}
}
/* ── Cards ── */
/* ══════════════════════════════════════════════════
CARDS
══════════════════════════════════════════════════ */
.card
{
.card
{
background
:
#fff
;
background
:
var
(
--surface-card
)
;
border-radius
:
8px
;
border-radius
:
var
(
--radius-lg
)
;
border
:
1px
solid
#E5E7EB
;
border
:
1px
solid
var
(
--border-light
)
;
box-shadow
:
0
1px
3px
rgba
(
0
,
0
,
0
,
0.04
);
box-shadow
:
var
(
--shadow-sm
);
margin-bottom
:
0
;
margin-bottom
:
0
;
overflow
:
hidden
;
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 ── */
.card
:hover
{
.table-responsive
{
box-shadow
:
var
(
--shadow-md
);
overflow-x
:
auto
;
}
.data-table
{
width
:
100%
;
border-collapse
:
collapse
;
font-size
:
14px
;
}
}
.data-table
thead
{
.card-header
{
background
:
#0D7377
;
padding
:
20px
24px
;
color
:
#fff
;
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
{
.card-header
h2
,
padding
:
12px
15px
;
.card-header
h3
{
text-align
:
right
;
margin
:
0
;
font-weight
:
600
;
font-size
:
16px
;
white-space
:
nowrap
;
font-weight
:
700
;
color
:
var
(
--text-primary
);
}
}
.data-table
td
{
.card-body
{
padding
:
10px
15px
;
padding
:
24px
;
border-bottom
:
1px
solid
#F3F4F6
;
text-align
:
right
;
}
}
.data-table
tbody
tr
:hover
{
.card-footer
{
background
:
#F9FAFB
;
padding
:
16px
24px
;
border-top
:
1px
solid
var
(
--border-light
);
background
:
rgba
(
248
,
250
,
252
,
0.5
);
}
}
/* ── Buttons ── */
/* Interactive Card */
.btn
{
.card-interactive
{
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
;
cursor
:
pointer
;
cursor
:
pointer
;
border
:
1px
solid
transparent
;
transition
:
all
0.2s
ease
;
white-space
:
nowrap
;
font-family
:
inherit
;
}
}
.
btn-primary
{
.
card-interactive
:hover
{
b
ackground
:
#0D7377
;
b
order-color
:
rgba
(
var
(
--brand-primary-rgb
),
0.3
)
;
color
:
#fff
;
box-shadow
:
var
(
--shadow-card-hover
)
;
border-color
:
#0D7377
;
transform
:
translateY
(
-2px
)
;
}
}
.btn-primary
:hover
{
/* ══════════════════════════════════════════════════
background
:
#0a5c5f
;
STATS CARDS
══════════════════════════════════════════════════ */
.stats-grid
{
display
:
grid
;
grid-template-columns
:
repeat
(
auto-fit
,
minmax
(
240px
,
1
fr
));
gap
:
20px
;
margin-bottom
:
28px
;
}
}
.btn-outline
{
.stats-card
{
background
:
transparent
;
background
:
var
(
--surface-card
);
color
:
#0D7377
;
border-radius
:
var
(
--radius-lg
);
border-color
:
#D1D5DB
;
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
{
.stats-card
::before
{
background
:
#F3F4F6
;
content
:
''
;
border-color
:
#0D7377
;
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
{
.
stats-card
:hover
{
padding
:
4px
10px
;
transform
:
translateY
(
-3px
)
;
font-size
:
12px
;
box-shadow
:
var
(
--shadow-lg
)
;
}
}
/* ── Forms ── */
.stats-card
:hover::before
{
.form-group
{
height
:
4px
;
margin-bottom
:
0
;
}
}
.form-label
{
.stats-card-primary
::before
{
background
:
linear-gradient
(
90deg
,
var
(
--brand-primary
),
var
(
--brand-primary-light
));
}
display
:
block
;
.stats-card-success
::before
{
background
:
linear-gradient
(
90deg
,
var
(
--success
),
#34d399
);
}
margin-bottom
:
5px
;
.stats-card-danger
::before
{
background
:
linear-gradient
(
90deg
,
var
(
--danger
),
#f87171
);
}
font-size
:
13px
;
.stats-card-warning
::before
{
background
:
linear-gradient
(
90deg
,
var
(
--warning
),
#fbbf24
);
}
font-weight
:
600
;
color
:
#374151
;
.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
,
.stats-card
:hover
.stats-card-icon
{
.form-select
,
transform
:
scale
(
1.1
);
.form-textarea
{
width
:
100%
;
padding
:
8px
12px
;
border
:
1px
solid
#D1D5DB
;
border-radius
:
6px
;
font-size
:
14px
;
font-family
:
inherit
;
background
:
#fff
;
color
:
#1A1A2E
;
transition
:
border-color
0.2s
;
box-sizing
:
border-box
;
}
}
.form-input
:focus
,
.stats-card-primary
.stats-card-icon
{
background
:
rgba
(
var
(
--brand-primary-rgb
),
0.1
);
color
:
var
(
--brand-primary
);
}
.form-select
:focus
,
.stats-card-success
.stats-card-icon
{
background
:
rgba
(
5
,
150
,
105
,
0.1
);
color
:
var
(
--success
);
}
.form-textarea
:focus
{
.stats-card-danger
.stats-card-icon
{
background
:
rgba
(
220
,
38
,
38
,
0.1
);
color
:
var
(
--danger
);
}
outline
:
none
;
.stats-card-warning
.stats-card-icon
{
background
:
rgba
(
217
,
119
,
6
,
0.1
);
color
:
var
(
--warning
);
}
border-color
:
#0D7377
;
box-shadow
:
0
0
0
3px
rgba
(
13
,
115
,
119
,
0.1
);
.stats-card-icon
svg
{
width
:
24px
;
height
:
24px
;
}
}
.
form-textarea
{
.
stats-card-content
{
resize
:
vertical
;
flex
:
1
;
min-
height
:
80px
;
min-
width
:
0
;
}
}
/* ── Alerts ── */
.stats-card-title
{
.alert
{
font-size
:
13px
;
padding
:
12px
20px
;
border-radius
:
8px
;
margin
:
0
25px
15px
;
font-size
:
14px
;
font-weight
:
500
;
font-weight
:
500
;
display
:
flex
;
color
:
var
(
--text-muted
);
justify-content
:
space-between
;
margin-bottom
:
4px
;
align-items
:
center
;
}
}
.alert-success
{
.stats-card-value
{
background
:
#F0FDF4
;
font-size
:
28px
;
color
:
#059669
;
font-weight
:
800
;
border
:
1px
solid
#BBF7D0
;
color
:
var
(
--text-primary
);
line-height
:
1.2
;
letter-spacing
:
-0.02em
;
}
}
.alert-error
{
.stats-card-change
{
background
:
#FEF2F2
;
display
:
inline-flex
;
color
:
#DC2626
;
align-items
:
center
;
border
:
1px
solid
#FECACA
;
gap
:
4px
;
font-size
:
12px
;
font-weight
:
600
;
margin-top
:
6px
;
padding
:
2px
8px
;
border-radius
:
var
(
--radius-full
);
}
}
.alert-warning
{
.stats-card-change.positive
{
background
:
#FFF7ED
;
color
:
var
(
--success
);
color
:
#D97706
;
background
:
var
(
--success-bg
);
border
:
1px
solid
#FED7AA
;
}
}
.alert-info
{
.stats-card-change.negative
{
background
:
#EFF6FF
;
color
:
var
(
--danger
);
color
:
#0284C7
;
background
:
var
(
--danger-bg
);
border
:
1px
solid
#BFDBFE
;
}
}
.alert-close
{
.stats-card-link
{
background
:
none
;
position
:
absolute
;
border
:
none
;
bottom
:
0
;
cursor
:
pointer
;
left
:
0
;
font-size
:
18px
;
right
:
0
;
color
:
inherit
;
padding
:
10px
24px
;
opacity
:
0.6
;
font-size
:
12px
;
padding
:
0
5px
;
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
;
opacity
:
1
;
transform
:
translateY
(
0
);
background
:
rgba
(
var
(
--brand-primary-rgb
),
0.04
);
}
}
/* ── Responsive ── */
/* ══════════════════════════════════════════════════
@media
(
max-width
:
1024px
)
{
TABLES
.sidebar
{
══════════════════════════════════════════════════ */
transform
:
translateX
(
260px
);
.table-responsive
{
width
:
260px
;
overflow-x
:
auto
;
}
border-radius
:
var
(
--radius-lg
);
.sidebar.show
{
transform
:
translateX
(
0
);
}
.main-wrapper
{
margin-right
:
0
;
}
.sidebar-toggle-btn
{
display
:
block
;
}
}
}
/* ── Print ── */
.data-table
{
@media
print
{
width
:
100%
;
.sidebar
,
.top-header
,
.page-footer
,
.btn
,
.sidebar-toggle-btn
{
border-collapse
:
separate
;
display
:
none
!important
;
border-spacing
:
0
;
}
font-size
:
13.5px
;
.main-wrapper
{
margin-right
:
0
!important
;
}
.page-content
{
padding
:
0
!important
;
}
}
}
/* ── Body Reset ── */
.data-table
thead
{
*
{
position
:
sticky
;
box-sizing
:
border-box
;
top
:
0
;
z-index
:
1
;
}
}
body
{
.data-table
thead
th
{
margin
:
0
;
padding
:
14px
18px
;
padding
:
0
;
text-align
:
right
;
font-family
:
'Cairo'
,
'Segoe UI'
,
Tahoma
,
Arial
,
sans-serif
;
font-weight
:
600
;
font-size
:
14px
;
white-space
:
nowrap
;
color
:
#1A1A2E
;
font-size
:
12px
;
background
:
#F3F4F6
;
text-transform
:
uppercase
;
direction
:
rtl
;
letter-spacing
:
0.5px
;
line-height
:
1.6
;
color
:
var
(
--text-muted
);
background
:
var
(
--surface-bg
);
border-bottom
:
2px
solid
var
(
--border-light
);
}
}
a
{
.data-table
tbody
td
{
color
:
#0D7377
;
padding
:
14px
18px
;
text-decoration
:
none
;
border-bottom
:
1px
solid
var
(
--border-light
);
text-align
:
right
;
color
:
var
(
--text-secondary
);
transition
:
background
var
(
--duration-fast
)
ease
;
}
}
a
:hove
r
{
.data-table
tbody
t
r
{
t
ext-decoration
:
underlin
e
;
t
ransition
:
all
var
(
--duration-fast
)
eas
e
;
}
}
code
{
.data-table
tbody
tr
:hover
{
background
:
#F3F4F6
;
background
:
rgba
(
var
(
--brand-primary-rgb
),
0.03
);
padding
:
2px
6px
;
}
border-radius
:
4px
;
font-size
:
12px
;
.data-table
tbody
tr
:hover
td
{
font-family
:
'Courier New'
,
monospace
;
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
,
1
fr
));
gap
:
20px
;
}
.form-grid-2
{
grid-template-columns
:
repeat
(
2
,
1
fr
);
}
.form-grid-3
{
grid-template-columns
:
repeat
(
3
,
1
fr
);
}
.form-grid-4
{
grid-template-columns
:
repeat
(
4
,
1
fr
);
}
/* 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
,
1
fr
);
}
.grid-cols-3
{
grid-template-columns
:
repeat
(
3
,
1
fr
);
}
.grid-cols-4
{
grid-template-columns
:
repeat
(
4
,
1
fr
);
}
/* 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
:
1
fr
;
}
.stats-grid
{
grid-template-columns
:
repeat
(
auto-fit
,
minmax
(
200px
,
1
fr
));
}
.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
;
}
}
public/assets/js/app.js
View file @
de5cfc33
/* ════════════════════════════════════════════════════════════
/* ════════════════════════════════════════════════════════════
THE CLUB ERP — Core JavaScript (Vanilla ES6+)
THE CLUB ERP — Core JavaScript (Vanilla ES6+)
Premium UI interactions, animations, and utilities.
════════════════════════════════════════════════════════════ */
════════════════════════════════════════════════════════════ */
// ── CSRF Token ──
// ── CSRF Token ──
...
@@ -63,30 +64,65 @@ async function ajax(method, url, data = null, options = {}) {
...
@@ -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
)
{
function
toast
(
message
,
type
=
'info'
,
duration
=
5000
)
{
const
container
=
document
.
getElementById
(
'toast-container'
);
const
container
=
document
.
getElementById
(
'toast-container'
);
if
(
!
container
)
return
;
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'
);
const
el
=
document
.
createElement
(
'div'
);
el
.
className
=
`toast toast-
${
type
}
`
;
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
);
container
.
appendChild
(
el
);
if
(
duration
>
0
)
{
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
)
{
function
openModal
(
id
)
{
const
modal
=
document
.
getElementById
(
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
)
{
function
closeModal
(
id
)
{
const
modal
=
document
.
getElementById
(
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
)
{
function
confirmModal
(
title
,
message
,
onConfirm
)
{
...
@@ -96,19 +132,26 @@ function confirmModal(title, message, onConfirm) {
...
@@ -96,19 +132,26 @@ function confirmModal(title, message, onConfirm) {
<div class="modal modal-small">
<div class="modal modal-small">
<div class="modal-header">
<div class="modal-header">
<h3 class="modal-title">
${
escapeHtml
(
title
)}
</h3>
<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>
<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">
<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>
<button class="btn btn-danger" id="
${
id
}
-confirm">تأكيد</button>
</div>
</div>
</div>
</div>
</div>`
;
</div>`
;
document
.
body
.
insertAdjacentHTML
(
'beforeend'
,
html
);
document
.
body
.
insertAdjacentHTML
(
'beforeend'
,
html
);
document
.
body
.
style
.
overflow
=
'hidden'
;
document
.
getElementById
(
`
${
id
}
-confirm`
).
addEventListener
(
'click'
,
function
()
{
document
.
getElementById
(
`
${
id
}
-confirm`
).
addEventListener
(
'click'
,
function
()
{
closeModal
(
id
);
closeModal
(
id
);
document
.
getElementById
(
id
).
remove
();
setTimeout
(
function
()
{
var
e
=
document
.
getElementById
(
id
);
if
(
e
)
e
.
remove
();
},
300
);
onConfirm
();
onConfirm
();
});
});
}
}
...
@@ -122,9 +165,23 @@ function toggleSubmenu(el) {
...
@@ -122,9 +165,23 @@ function toggleSubmenu(el) {
const
parent
=
el
.
closest
(
'.sidebar-item'
);
const
parent
=
el
.
closest
(
'.sidebar-item'
);
const
submenu
=
parent
.
querySelector
(
'.sidebar-submenu'
);
const
submenu
=
parent
.
querySelector
(
'.sidebar-submenu'
);
if
(
submenu
)
{
if
(
submenu
)
{
const
isOpen
=
submenu
.
style
.
display
===
'block'
;
const
isOpen
=
parent
.
classList
.
contains
(
'open'
);
submenu
.
style
.
display
=
isOpen
?
'none'
:
'block'
;
parent
.
classList
.
toggle
(
'open'
,
!
isOpen
);
parent
.
classList
.
toggle
(
'open'
);
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 = {}) {
...
@@ -156,7 +213,14 @@ async function parseNid(nid, config = {}) {
if
(
el
&&
data
[
key
]
!==
undefined
)
{
if
(
el
&&
data
[
key
]
!==
undefined
)
{
el
.
value
=
data
[
key
];
el
.
value
=
data
[
key
];
el
.
setAttribute
(
'readonly'
,
'readonly'
);
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) {
...
@@ -174,7 +238,7 @@ function showFormErrors(errors) {
if
(
group
)
group
.
classList
.
add
(
'has-error'
);
if
(
group
)
group
.
classList
.
add
(
'has-error'
);
const
errorDiv
=
document
.
createElement
(
'div'
);
const
errorDiv
=
document
.
createElement
(
'div'
);
errorDiv
.
className
=
'form-error'
;
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
);
input
.
parentNode
.
insertBefore
(
errorDiv
,
input
.
nextSibling
);
}
}
}
}
...
@@ -202,7 +266,7 @@ function initPhoneFormat(inputId) {
...
@@ -202,7 +266,7 @@ function initPhoneFormat(inputId) {
function
disableFields
(
selector
)
{
function
disableFields
(
selector
)
{
document
.
querySelectorAll
(
selector
).
forEach
(
el
=>
{
document
.
querySelectorAll
(
selector
).
forEach
(
el
=>
{
el
.
setAttribute
(
'disabled'
,
'disabled'
);
el
.
setAttribute
(
'disabled'
,
'disabled'
);
el
.
style
.
backgroundColor
=
'
#f3f4f6
'
;
el
.
style
.
backgroundColor
=
'
var(--surface-bg)
'
;
});
});
}
}
...
@@ -274,24 +338,72 @@ function initTabs(containerId) {
...
@@ -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
()
{
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
// Auto-dismiss alerts with slide-out
document
.
querySelectorAll
(
'[data-auto-dismiss]'
).
forEach
(
el
=>
{
document
.
querySelectorAll
(
'[data-auto-dismiss]'
).
forEach
(
el
=>
{
const
ms
=
parseInt
(
el
.
dataset
.
autoDismiss
)
||
5000
;
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
// Close modals on overlay click
document
.
addEventListener
(
'click'
,
function
(
e
)
{
document
.
addEventListener
(
'click'
,
function
(
e
)
{
if
(
e
.
target
.
classList
.
contains
(
'modal-overlay'
))
{
if
(
e
.
target
.
classList
.
contains
(
'modal-overlay'
))
{
const
id
=
e
.
target
.
id
;
if
(
id
)
closeModal
(
id
);
else
{
e
.
target
.
style
.
display
=
'none'
;
e
.
target
.
style
.
display
=
'none'
;
document
.
body
.
style
.
overflow
=
''
;
}
}
}
});
});
// Escape closes modals
// Escape closes modals
document
.
addEventListener
(
'keydown'
,
function
(
e
)
{
document
.
addEventListener
(
'keydown'
,
function
(
e
)
{
if
(
e
.
key
===
'Escape'
)
{
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() {
...
@@ -308,6 +420,63 @@ document.addEventListener('DOMContentLoaded', function() {
document
.
querySelectorAll
(
'[data-nid-parser]'
).
forEach
(
input
=>
{
document
.
querySelectorAll
(
'[data-nid-parser]'
).
forEach
(
input
=>
{
initNationalIdParser
(
input
.
id
);
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 ──
// ── Helpers ──
...
@@ -319,8 +488,8 @@ function escapeHtml(text) {
...
@@ -319,8 +488,8 @@ function escapeHtml(text) {
// Session timeout warning
// Session timeout warning
(
function
()
{
(
function
()
{
const
timeout
=
30
*
60
*
1000
;
// 30 min
const
timeout
=
30
*
60
*
1000
;
const
warningBefore
=
5
*
60
*
1000
;
// 5 min
const
warningBefore
=
5
*
60
*
1000
;
let
timer
;
let
timer
;
function
resetTimer
()
{
function
resetTimer
()
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment