v3 / utils /components /sidebar.py
EGYADMIN's picture
Upload 113 files
3d20a1c verified
"""
مكون الشريط الجانبي لنظام واهبي لتحليل العقود والمناقصات
Sidebar component for WAHBI Tender Analysis System
"""
import streamlit as st
import os
from pathlib import Path
import streamlit_option_menu as option_menu
import json
import datetime
# ألوان النظام الموحدة
COLORS = {
"primary": "#0B6E4F",
"secondary": "#08603a",
"accent": "#FFB100",
"tertiary": "#5754FF",
"quaternary": "#f43f5e",
"success": "#10b981",
"warning": "#f59e0b",
"danger": "#ef4444",
"light_bg": "#f8fafc",
"text": "#334155",
"heading": "#1e293b",
"muted": "#64748b",
"border": "#e2e8f0",
}
# تهيئة الرموز مع ألوان مخصصة للأقسام المختلفة
SECTION_ICONS = {
"الرئيسية": {"icon": "house-fill", "color": COLORS["primary"]},
"تحليل العقود": {"icon": "file-earmark-text-fill", "color": COLORS["primary"]},
"حاسبة التكاليف": {"icon": "calculator-fill", "color": COLORS["tertiary"]},
"إدارة المشاريع": {"icon": "clipboard2-data-fill", "color": COLORS["primary"]},
"الخريطة التفاعلية": {"icon": "geo-alt-fill", "color": COLORS["quaternary"]},
"المساعد الذكي": {"icon": "robot", "color": COLORS["accent"]},
"التقارير": {"icon": "bar-chart-fill", "color": COLORS["tertiary"]},
"المحتوى المحلي": {"icon": "flag-fill", "color": COLORS["success"]},
"تحليل المخاطر": {"icon": "shield-fill-exclamation", "color": COLORS["warning"]},
"الإعدادات": {"icon": "gear-fill", "color": COLORS["muted"]},
}
def get_user_info():
"""
استرجاع معلومات المستخدم الحالي (يُستخدم كمثال بسيط)
"""
# في بيئة الإنتاج، هذه المعلومات يجب أن تأتي من نظام المصادقة
return {
"name": "محمد أحمد",
"role": "محلل عقود",
"organization": "شركة المشاريع المتطورة",
"last_login": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
"image": None
}
def render_wahbi_logo():
"""
عرض شعار نظام واهبي بتصميم SVG متقدم
"""
logo_html = """
<div style="text-align: center; margin-bottom: 1.5rem; margin-top: 0.5rem;">
<svg width="120" height="60" viewBox="0 0 240 120" xmlns="http://www.w3.org/2000/svg">
<style>
.logo-text { font-family: 'Almarai', sans-serif; font-weight: 800; }
.logo-accent { fill: #FFB100; }
.logo-primary { fill: #0B6E4F; }
.logo-secondary { fill: #08603a; }
@keyframes pulse {
0% { opacity: 0.8; }
50% { opacity: 1; }
100% { opacity: 0.8; }
}
.pulse { animation: pulse 2s infinite ease-in-out; }
</style>
<g>
<!-- الشكل الخارجي -->
<path class="logo-primary" d="M40,20 L200,20 C220,20 230,30 230,50 L230,70 C230,90 220,100 200,100 L40,100 C20,100 10,90 10,70 L10,50 C10,30 20,20 40,20 Z" />
<!-- الشكل الداخلي -->
<path class="logo-secondary" d="M45,30 L195,30 C210,30 220,40 220,55 L220,65 C220,80 210,90 195,90 L45,90 C30,90 20,80 20,65 L20,55 C20,40 30,30 45,30 Z" />
<!-- الشكل الثالث (الهالة) -->
<circle class="pulse logo-accent" cx="120" cy="60" r="35" opacity="0.2" />
<!-- نص "واهبي" -->
<text class="logo-text" x="120" y="70" font-size="40" text-anchor="middle" fill="white">واهبي</text>
<!-- نص "AI" -->
<text class="logo-text logo-accent" x="175" y="70" font-size="30" text-anchor="middle">AI</text>
</g>
</svg>
</div>
"""
return logo_html
def render_sidebar():
"""
عرض الشريط الجانبي الرئيسي للتطبيق
"""
with st.sidebar:
# عرض شعار النظام
st.markdown(render_wahbi_logo(), unsafe_allow_html=True)
# عرض معلومات المستخدم
user = get_user_info()
# مربع معلومات المستخدم بتصميم متقدم
st.markdown(f"""
<div class="user-profile-card">
<div class="user-avatar">
{user["name"][0] if user["name"] else "م"}
</div>
<h3 class="user-name">{user["name"]}</h3>
<p class="user-role">{user["role"]}</p>
<p class="user-organization">{user["organization"]}</p>
<div class="user-last-login">
<i class="fas fa-clock"></i>
آخر دخول: {user["last_login"]}
</div>
</div>
<style>
.user-profile-card {{
padding: var(--spacing-lg);
background: linear-gradient(135deg, rgba(11, 110, 79, 0.08), rgba(11, 110, 79, 0.03));
border-radius: var(--border-radius);
margin-bottom: var(--spacing-lg);
text-align: center;
border: 1px solid rgba(11, 110, 79, 0.12);
box-shadow: var(--shadow-sm);
}}
.user-avatar {{
width: 85px;
height: 85px;
background: var(--primary-gradient);
border-radius: var(--border-radius-circle);
margin: 0 auto var(--spacing-md) auto;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 2.2rem;
font-weight: var(--font-weight-bold);
box-shadow: 0 4px 10px rgba(11, 110, 79, 0.3);
}}
.user-name {{
margin: 0;
font-size: var(--font-size-xl);
color: var(--gray-800);
font-weight: var(--font-weight-bold);
}}
.user-role {{
margin: var(--spacing-xs) 0 var(--spacing-xs) 0;
color: var(--primary-color);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
}}
.user-organization {{
margin: 0;
color: var(--gray-600);
font-size: var(--font-size-xs);
}}
.user-last-login {{
margin-top: var(--spacing-md);
padding-top: var(--spacing-md);
border-top: 1px dashed rgba(11, 110, 79, 0.15);
text-align: center;
font-size: var(--font-size-xs);
color: var(--gray-500);
display: flex;
align-items: center;
justify-content: center;
}}
.user-last-login i {{
margin-left: 5px;
}}
</style>
""", unsafe_allow_html=True)
# القائمة الرئيسية باستخدام كومبوننت الشريط الجانبي
menu_options = [
"الرئيسية",
"تحليل العقود",
"حاسبة التكاليف",
"إدارة المشاريع",
"الخريطة التفاعلية",
"المساعد الذكي",
"التقارير",
"المحتوى المحلي",
"تحليل المخاطر",
"الإعدادات"
]
icons = [SECTION_ICONS[option]["icon"] for option in menu_options]
# توليد قائمة الألوان المخصصة للأيقونات
icon_colors = [SECTION_ICONS[option]["color"] for option in menu_options]
# تطبيق نمط CSS المخصص للقائمة
menu_style = """
<style>
/* تحسين تنسيق القائمة الرئيسية */
.nav-link {
position: relative !important;
overflow: hidden !important;
white-space: nowrap !important;
}
/* تأثير التوهج للعنصر المحدد */
.nav-link.active::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 4px;
height: 100%;
background: linear-gradient(to bottom, rgba(255, 177, 0, 0.8), rgba(255, 177, 0, 0.3));
z-index: 5;
animation: pulse-border 1.5s infinite alternate;
}
/* تأثير الخلفية للعنصر المحدد */
.nav-link.active::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(11, 110, 79, 0.95), rgba(8, 96, 58, 0.95));
z-index: -1;
animation: gradient-shift 8s infinite alternate;
}
/* تنسيق الأيقونات بالألوان المخصصة */
.section-icon-0 { color: #0B6E4F !important; }
.section-icon-1 { color: #0B6E4F !important; }
.section-icon-2 { color: #5754FF !important; }
.section-icon-3 { color: #0B6E4F !important; }
.section-icon-4 { color: #f43f5e !important; }
.section-icon-5 { color: #FFB100 !important; }
.section-icon-6 { color: #5754FF !important; }
.section-icon-7 { color: #10b981 !important; }
.section-icon-8 { color: #f59e0b !important; }
.section-icon-9 { color: #64748b !important; }
/* إظهار الأيقونات بالأبيض للعنصر المحدد */
.nav-link.active i {
color: white !important;
animation: icon-pop 0.3s ease-out;
}
/* تأثيرات الحركة */
@keyframes pulse-border {
0% { opacity: 0.7; }
100% { opacity: 1; }
}
@keyframes gradient-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes icon-pop {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
/* تأثير التحويم */
.nav-link:not(.active):hover {
background-color: rgba(11, 110, 79, 0.08) !important;
transform: translateX(-3px) !important;
}
/* تنعيم عملية الانتقال */
.nav-link {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
</style>
"""
st.markdown(menu_style, unsafe_allow_html=True)
# إضافة عنوان CSS للأيقونات
for i, option in enumerate(menu_options):
st.markdown(f"""
<style>
div[data-testid="stVerticalBlock"] div.stButton:nth-child({i+1}) button i {{
color: {SECTION_ICONS[option]["color"]} !important;
}}
</style>
""", unsafe_allow_html=True)
selected = option_menu.option_menu(
menu_title="نظام WAHBi AI",
options=menu_options,
icons=icons,
menu_icon="grid-fill",
default_index=0,
styles={
"container": {
"padding": "0.5rem !important",
"background-color": "transparent",
"direction": "rtl",
"border-radius": "var(--border-radius)",
"margin-bottom": "var(--spacing-md)",
"border": "1px solid rgba(11, 110, 79, 0.1)",
"backdrop-filter": "blur(10px)",
"background": "rgba(255, 255, 255, 0.6)",
},
"icon": {
"font-size": "1.2rem",
"float": "right",
"margin-left": "15px",
"vertical-align": "middle",
},
"nav-link": {
"font-size": "0.95rem",
"text-align": "right",
"direction": "rtl",
"padding": "0.8rem 1rem",
"margin-bottom": "0.5rem",
"border-radius": "8px",
"font-weight": "500",
"display": "flex",
"align-items": "center",
"justify-content": "flex-start",
"position": "relative",
"overflow": "hidden",
"z-index": "1",
"transition": "all 0.3s ease",
"background": "rgba(255, 255, 255, 0.7)",
"border": "1px solid rgba(226, 232, 240, 0.7)",
},
"nav-link-selected": {
"background": "var(--primary-gradient)",
"color": "white",
"text-align": "right",
"font-weight": "600",
"box-shadow": "0 4px 12px rgba(11, 110, 79, 0.2)",
"border": "none",
},
}
)
# تخزين القيمة المحددة في session_state
st.session_state["sidebar_selected"] = selected
# إظهار حالة الاتصال بقاعدة البيانات وحالة النظام
st.markdown(f"""
<div class="system-status-card">
<h4 class="status-title">حالة النظام</h4>
<div class="status-item">
<div class="status-indicator active"></div>
<span class="status-text">قاعدة البيانات متصلة</span>
</div>
<div class="status-item">
<div class="status-indicator active"></div>
<span class="status-text">واجهة برمجة التطبيقات</span>
</div>
<div class="status-item">
<div class="status-indicator active"></div>
<span class="status-text">الذكاء الاصطناعي</span>
</div>
<div class="status-footer">
آخر تحديث: {datetime.datetime.now().strftime("%H:%M:%S")}
</div>
</div>
<style>
.system-status-card {{
padding: var(--spacing-lg);
background-color: white;
border-radius: var(--border-radius);
margin-top: var(--spacing-lg);
border: 1px solid var(--gray-200);
box-shadow: var(--shadow-card);
}}
.status-title {{
margin: 0 0 var(--spacing-md) 0;
font-size: var(--font-size-md);
color: var(--gray-800);
font-weight: var(--font-weight-semibold);
border-right: 3px solid var(--primary-color);
padding-right: var(--spacing-sm);
}}
.status-item {{
display: flex;
align-items: center;
margin-bottom: var(--spacing-sm);
justify-content: flex-start;
}}
.status-indicator {{
min-width: 10px;
height: 10px;
border-radius: 50%;
margin-left: var(--spacing-sm);
}}
.status-indicator.active {{
background-color: var(--success-color);
box-shadow: 0 0 5px var(--success-color);
}}
.status-indicator.warning {{
background-color: var(--warning-color);
box-shadow: 0 0 5px var(--warning-color);
}}
.status-indicator.error {{
background-color: var(--danger-color);
box-shadow: 0 0 5px var(--danger-color);
}}
.status-text {{
font-size: var(--font-size-sm);
color: var(--gray-700);
}}
.status-footer {{
margin-top: var(--spacing-md);
padding-top: var(--spacing-md);
border-top: 1px solid var(--gray-200);
font-size: var(--font-size-xs);
color: var(--gray-500);
text-align: center;
}}
</style>
""", unsafe_allow_html=True)
# معلومات النظام وعنوان المشروع
with st.expander("حول النظام", expanded=False):
st.markdown(f"""
<div class="about-system">
<div class="about-item">
<i class="fas fa-info-circle about-icon"></i>
<span class="about-text about-title">نظام واهبي للذكاء الاصطناعي - إصدار 2.0</span>
</div>
<div class="about-item">
<i class="fas fa-file-contract about-icon"></i>
<span class="about-text">تحليل العقود والمناقصات</span>
</div>
<div class="about-item">
<i class="fas fa-brain about-icon"></i>
<span class="about-text">بواسطة نماذج الذكاء الاصطناعي المتقدمة</span>
</div>
<div class="about-item">
<i class="fas fa-copyright about-icon"></i>
<span class="about-text">© 2025 جميع الحقوق محفوظة</span>
</div>
</div>
<style>
.about-system {{
font-size: var(--font-size-sm);
color: var(--gray-700);
}}
.about-item {{
display: flex;
align-items: center;
margin-bottom: var(--spacing-sm);
}}
.about-icon {{
color: var(--primary-color);
font-size: 1rem;
margin-left: var(--spacing-sm);
}}
.about-text {{
flex: 1;
}}
.about-title {{
font-weight: var(--font-weight-semibold);
}}
</style>
""", unsafe_allow_html=True)
def get_sidebar_selection():
"""
الحصول على العنصر المحدد في القائمة الجانبية
"""
return st.session_state.get("sidebar_selected", "الرئيسية")
def render_module_sidebar(module_name, options=[]):
"""
عرض شريط جانبي مخصص للوحدة
المعلمات:
module_name (str): اسم الوحدة
options (list): قائمة بالخيارات المتاحة في الوحدة
"""
with st.sidebar:
# عنوان الوحدة
st.markdown(f"""
<div class="module-title">
{module_name}
</div>
<style>
.module-title {{
margin-bottom: var(--spacing-md);
color: var(--primary-color);
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--gray-200);
}}
</style>
""", unsafe_allow_html=True)
# إذا تم توفير خيارات للوحدة
if options:
# تطبيق نمط CSS المخصص للقائمة الفرعية
module_menu_style = """
<style>
/* تحسين تنسيق قائمة الوحدة الفرعية */
.module-menu .nav-link {
position: relative !important;
overflow: hidden !important;
white-space: nowrap !important;
background: rgba(250, 250, 250, 0.5) !important;
border: 1px solid rgba(226, 232, 240, 0.5) !important;
border-radius: 6px !important;
margin-bottom: 0.4rem !important;
padding: 0.6rem 1rem !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
/* تأثير التحويم */
.module-menu .nav-link:not(.active):hover {
background-color: rgba(11, 110, 79, 0.05) !important;
transform: translateX(-2px) !important;
border-color: rgba(11, 110, 79, 0.15) !important;
}
/* تنسيق العنصر النشط */
.module-menu .nav-link.active {
background: linear-gradient(135deg, rgba(11, 110, 79, 0.9), rgba(8, 96, 58, 0.9)) !important;
color: white !important;
box-shadow: 0 4px 10px rgba(11, 110, 79, 0.15) !important;
font-weight: 600 !important;
border: none !important;
}
/* ترتيب العناصر */
.module-menu .nav-link::before {
content: '';
position: absolute;
width: 4px;
height: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
background-color: var(--primary-color);
transition: height 0.3s ease;
}
.module-menu .nav-link:hover::before {
height: 70%;
}
.module-menu .nav-link.active::before {
height: 100%;
background: linear-gradient(to bottom, var(--accent-color), rgba(255, 177, 0, 0.5));
animation: pulse-height 1.5s infinite alternate;
}
@keyframes pulse-height {
0% { opacity: 0.7; }
100% { opacity: 1; }
}
</style>
"""
st.markdown(module_menu_style, unsafe_allow_html=True)
# كتلة تحتوي على القائمة مع فئة CSS مخصصة
menu_container = st.container()
with menu_container:
st.markdown('<div class="module-menu">', unsafe_allow_html=True)
selected = option_menu.option_menu(
menu_title=None,
options=options,
menu_icon=None,
default_index=0,
styles={
"container": {
"padding": "0!important",
"background-color": "transparent",
"direction": "rtl"
},
"icon": {
"color": "var(--primary-color)",
"font-size": "1rem",
"float": "right",
"margin-left": "12px"
},
"nav-link": {
"font-size": "0.9rem",
"text-align": "right",
"direction": "rtl",
"margin-bottom": "0.35rem",
"padding": "0.7rem 1rem",
"border-radius": "6px"
},
"nav-link-selected": {
"background-color": "var(--primary-color)",
"color": "white",
"text-align": "right",
"font-weight": "600"
},
}
)
# إغلاق كتلة القائمة
st.markdown('</div>', unsafe_allow_html=True)
# تخزين الخيار المحدد
st.session_state[f"{module_name}_selected"] = selected
return selected
# زر للعودة إلى القائمة الرئيسية مع تصميم محسن
back_button_container = st.container()
with back_button_container:
st.markdown(f"""
<style>
/* تنسيق زر العودة للقائمة الرئيسية */
div[data-testid="stButton"][data-testid*="back_btn_{module_name}"] button {{
background: linear-gradient(135deg, rgba(11, 110, 79, 0.05), rgba(11, 110, 79, 0.1));
color: var(--primary-color);
border: 1px solid rgba(11, 110, 79, 0.15);
padding: 0.7rem;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 500;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
width: 100%;
text-align: center;
margin-top: 1.5rem;
position: relative;
overflow: hidden;
}}
div[data-testid="stButton"][data-testid*="back_btn_{module_name}"] button:hover {{
background: linear-gradient(135deg, rgba(11, 110, 79, 0.1), rgba(11, 110, 79, 0.15));
border-color: rgba(11, 110, 79, 0.25);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(11, 110, 79, 0.1);
}}
div[data-testid="stButton"][data-testid*="back_btn_{module_name}"] button:active {{
transform: translateY(0);
box-shadow: none;
}}
/* إضافة أيقونة للزر */
div[data-testid="stButton"][data-testid*="back_btn_{module_name}"] button::before {{
content: "⟸";
margin-left: 8px;
font-size: 1.1rem;
display: inline-block;
vertical-align: middle;
}}
/* إضافة تأثير متموج عند النقر */
@keyframes ripple {{
0% {{
transform: scale(0);
opacity: 0.5;
}}
100% {{
transform: scale(4);
opacity: 0;
}}
}}
div[data-testid="stButton"][data-testid*="back_btn_{module_name}"] button::after {{
content: "";
position: absolute;
width: 30px;
height: 30px;
background: rgba(255, 255, 255, 0.4);
border-radius: 50%;
left: 50%;
top: 50%;
transform: scale(0);
transform-origin: center;
pointer-events: none;
}}
div[data-testid="stButton"][data-testid*="back_btn_{module_name}"] button:active::after {{
animation: ripple 0.6s ease-out;
}}
</style>
""", unsafe_allow_html=True)
# إضافة كتلة على شكل بطاقة تحتوي على الزر
st.markdown("""
<div class="back-button-card">
<style>
.back-button-card {
margin-top: 1.5rem;
margin-bottom: 1rem;
}
</style>
</div>
""", unsafe_allow_html=True)
if st.button("العودة للقائمة الرئيسية", key=f"back_btn_{module_name}"):
st.session_state["sidebar_selected"] = "الرئيسية"
st.rerun()
def get_module_selection(module_name):
"""
الحصول على العنصر المحدد في قائمة الوحدة
المعلمات:
module_name (str): اسم الوحدة
"""
return st.session_state.get(f"{module_name}_selected", None)