|
""" |
|
محسن واجهة المستخدم - نظام تحليل المناقصات |
|
""" |
|
|
|
import streamlit as st |
|
import pandas as pd |
|
import numpy as np |
|
import base64 |
|
from pathlib import Path |
|
import os |
|
|
|
class UIEnhancer: |
|
"""فئة لتحسين واجهة المستخدم وتوحيد التصميم عبر النظام""" |
|
|
|
|
|
COLORS = { |
|
'primary': '#1E88E5', |
|
'secondary': '#5E35B1', |
|
'success': '#43A047', |
|
'warning': '#FB8C00', |
|
'danger': '#E53935', |
|
'info': '#00ACC1', |
|
'light': '#F5F5F5', |
|
'dark': '#212121', |
|
'accent': '#FF4081', |
|
'background': '#FFFFFF', |
|
'text': '#212121', |
|
'border': '#E0E0E0' |
|
} |
|
|
|
|
|
FONT_SIZES = { |
|
'xs': '0.75rem', |
|
'sm': '0.875rem', |
|
'md': '1rem', |
|
'lg': '1.125rem', |
|
'xl': '1.25rem', |
|
'2xl': '1.5rem', |
|
'3xl': '1.875rem', |
|
'4xl': '2.25rem', |
|
'5xl': '3rem' |
|
} |
|
|
|
def __init__(self, page_title="نظام تحليل المناقصات", page_icon="📊"): |
|
"""تهيئة محسن واجهة المستخدم""" |
|
self.page_title = page_title |
|
self.page_icon = page_icon |
|
self.theme_mode = "light" |
|
|
|
|
|
if 'theme' not in st.session_state: |
|
st.session_state.theme = 'light' |
|
|
|
def apply_global_styles(self): |
|
"""تطبيق التنسيقات العامة على الصفحة""" |
|
|
|
css = f""" |
|
@import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700&display=swap'); |
|
|
|
* {{ |
|
font-family: 'Tajawal', sans-serif; |
|
direction: rtl; |
|
}} |
|
|
|
h1, h2, h3, h4, h5, h6 {{ |
|
font-family: 'Tajawal', sans-serif; |
|
font-weight: 700; |
|
color: {self.COLORS['dark']}; |
|
}} |
|
|
|
.module-title {{ |
|
color: {self.COLORS['primary']}; |
|
font-size: {self.FONT_SIZES['3xl']}; |
|
margin-bottom: 1rem; |
|
border-bottom: 2px solid {self.COLORS['primary']}; |
|
padding-bottom: 0.5rem; |
|
}} |
|
|
|
.stTabs [data-baseweb="tab-list"] {{ |
|
gap: 2px; |
|
}} |
|
|
|
.stTabs [data-baseweb="tab"] {{ |
|
height: 50px; |
|
white-space: pre-wrap; |
|
background-color: {self.COLORS['light']}; |
|
border-radius: 4px 4px 0 0; |
|
gap: 1px; |
|
padding-top: 10px; |
|
padding-bottom: 10px; |
|
}} |
|
|
|
.stTabs [aria-selected="true"] {{ |
|
background-color: {self.COLORS['primary']}; |
|
color: white; |
|
}} |
|
|
|
div[data-testid="stSidebarNav"] li div a span {{ |
|
direction: rtl; |
|
text-align: right; |
|
font-family: 'Tajawal', sans-serif; |
|
}} |
|
|
|
div[data-testid="stSidebarNav"] {{ |
|
background-color: {self.COLORS['light']}; |
|
}} |
|
|
|
div[data-testid="stSidebarNav"] li div {{ |
|
margin-right: 0; |
|
margin-left: auto; |
|
}} |
|
|
|
div[data-testid="stSidebarNav"] li div a {{ |
|
padding-right: 10px; |
|
padding-left: 0; |
|
}} |
|
|
|
div[data-testid="stSidebarNav"] li div a:hover {{ |
|
background-color: {self.COLORS['primary'] + '20'}; |
|
}} |
|
|
|
div[data-testid="stSidebarNav"] li div[aria-selected="true"] {{ |
|
background-color: {self.COLORS['primary'] + '40'}; |
|
}} |
|
|
|
div[data-testid="stSidebarNav"] li div[aria-selected="true"] a span {{ |
|
color: {self.COLORS['primary']}; |
|
font-weight: 500; |
|
}} |
|
|
|
.metric-card {{ |
|
background-color: white; |
|
border-radius: 10px; |
|
padding: 20px; |
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); |
|
text-align: center; |
|
transition: all 0.3s ease; |
|
border: 2px solid transparent; |
|
cursor: pointer; |
|
}} |
|
|
|
.metric-card:hover {{ |
|
transform: translateY(-5px); |
|
border-color: {self.COLORS['primary']}; |
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); |
|
}} |
|
|
|
.metric-value {{ |
|
font-size: 2.5rem; |
|
font-weight: 700; |
|
margin: 10px 0; |
|
}} |
|
|
|
.metric-label {{ |
|
font-size: 1rem; |
|
color: #666; |
|
}} |
|
|
|
.metric-change {{ |
|
font-size: 0.9rem; |
|
margin-top: 5px; |
|
}} |
|
|
|
.metric-change-positive {{ |
|
color: {self.COLORS['success']}; |
|
}} |
|
|
|
.metric-change-negative {{ |
|
color: {self.COLORS['danger']}; |
|
}} |
|
|
|
.custom-button {{ |
|
background-color: {self.COLORS['primary']}; |
|
color: white; |
|
border: none; |
|
border-radius: 5px; |
|
padding: 10px 20px; |
|
font-size: 1rem; |
|
cursor: pointer; |
|
transition: background-color 0.3s ease; |
|
}} |
|
|
|
.custom-button:hover {{ |
|
background-color: {self.COLORS['secondary']}; |
|
}} |
|
|
|
.custom-card {{ |
|
background-color: white; |
|
border-radius: 10px; |
|
padding: 20px; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
margin-bottom: 20px; |
|
}} |
|
|
|
.header-container {{ |
|
background: linear-gradient(135deg, {self.COLORS['primary']}, {self.COLORS['secondary']}); |
|
padding: 2rem; |
|
border-radius: 15px; |
|
margin-bottom: 2rem; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
}} |
|
|
|
.header-title {{ |
|
color: #ffffff; |
|
font-size: 2rem; |
|
margin: 0; |
|
font-weight: 700; |
|
}} |
|
|
|
.header-subtitle {{ |
|
color: rgba(255, 255, 255, 0.9); |
|
font-size: 1.1rem; |
|
margin: 0.5rem 0 0 0; |
|
}} |
|
|
|
/* تنسيق الجداول */ |
|
div[data-testid="stTable"] table {{ |
|
width: 100%; |
|
border-collapse: collapse; |
|
}} |
|
|
|
div[data-testid="stTable"] thead tr th {{ |
|
background-color: {self.COLORS['primary']}; |
|
color: white; |
|
text-align: right; |
|
padding: 12px; |
|
}} |
|
|
|
div[data-testid="stTable"] tbody tr:nth-child(even) {{ |
|
background-color: {self.COLORS['light']}; |
|
}} |
|
|
|
div[data-testid="stTable"] tbody tr:hover {{ |
|
background-color: {self.COLORS['primary'] + '10'}; |
|
}} |
|
|
|
div[data-testid="stTable"] tbody tr td {{ |
|
padding: 10px; |
|
text-align: right; |
|
}} |
|
|
|
/* تنسيق النماذج */ |
|
div[data-testid="stForm"] {{ |
|
background-color: white; |
|
border-radius: 10px; |
|
padding: 20px; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
}} |
|
|
|
button[kind="primaryFormSubmit"] {{ |
|
background-color: {self.COLORS['primary']}; |
|
color: white; |
|
}} |
|
|
|
button[kind="secondaryFormSubmit"] {{ |
|
background-color: {self.COLORS['light']}; |
|
color: {self.COLORS['dark']}; |
|
border: 1px solid {self.COLORS['border']}; |
|
}} |
|
|
|
/* تنسيق الرسوم البيانية */ |
|
div[data-testid="stVegaLiteChart"] {{ |
|
background-color: white; |
|
border-radius: 10px; |
|
padding: 20px; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
}} |
|
""" |
|
|
|
|
|
st.markdown(f'<style>{css}</style>', unsafe_allow_html=True) |
|
|
|
def apply_theme_colors(self): |
|
"""تطبيق ألوان السمة الحالية""" |
|
|
|
if self.theme_mode == "dark": |
|
self.COLORS['background'] = '#121212' |
|
self.COLORS['text'] = '#FFFFFF' |
|
self.COLORS['border'] = '#333333' |
|
else: |
|
self.COLORS['background'] = '#FFFFFF' |
|
self.COLORS['text'] = '#212121' |
|
self.COLORS['border'] = '#E0E0E0' |
|
|
|
|
|
theme_css = f""" |
|
body {{ |
|
background-color: {self.COLORS['background']}; |
|
color: {self.COLORS['text']}; |
|
}} |
|
""" |
|
|
|
st.markdown(f'<style>{theme_css}</style>', unsafe_allow_html=True) |
|
|
|
def toggle_theme(self): |
|
"""تبديل وضع السمة بين الفاتح والداكن""" |
|
if self.theme_mode == "light": |
|
self.theme_mode = "dark" |
|
else: |
|
self.theme_mode = "light" |
|
|
|
self.apply_theme_colors() |
|
|
|
def create_sidebar(self, menu_items): |
|
"""إنشاء الشريط الجانبي مع قائمة العناصر""" |
|
with st.sidebar: |
|
|
|
st.markdown( |
|
f""" |
|
<div style="text-align: center; margin-bottom: 20px;"> |
|
<h2 style="color: {self.COLORS['primary']};">{self.page_icon} {self.page_title}</h2> |
|
</div> |
|
""", |
|
unsafe_allow_html=True |
|
) |
|
|
|
|
|
st.markdown( |
|
f""" |
|
<div style="text-align: center; margin-bottom: 20px;"> |
|
<div style="width: 60px; height: 60px; border-radius: 50%; background-color: {self.COLORS['primary']}; color: white; display: flex; align-items: center; justify-content: center; margin: 0 auto; font-size: 24px; font-weight: bold;"> |
|
م |
|
</div> |
|
<p style="margin-top: 10px; font-weight: bold;">مهندس تامر الجوهري</p> |
|
<p style="margin-top: -15px; font-size: 0.8rem; color: #666;">مدير تقنية معلومات المنطقة الشمالية</p> |
|
</div> |
|
""", |
|
unsafe_allow_html=True |
|
) |
|
|
|
st.divider() |
|
|
|
|
|
selected = st.radio( |
|
"القائمة الرئيسية", |
|
[item["name"] for item in menu_items], |
|
format_func=lambda x: x, |
|
label_visibility="collapsed" |
|
) |
|
|
|
st.divider() |
|
|
|
|
|
st.markdown( |
|
""" |
|
<div style="position: absolute; bottom: 20px; left: 20px; right: 20px; text-align: center;"> |
|
<p style="font-size: 0.8rem; color: #666;">نظام تحليل المناقصات | الإصدار 2.0.0</p> |
|
<p style="font-size: 0.7rem; color: #888;">© 2025 جميع الحقوق محفوظة</p> |
|
</div> |
|
""", |
|
unsafe_allow_html=True |
|
) |
|
|
|
return selected |
|
|
|
def create_header(self, title, subtitle=None, show_actions=True, icon=None): |
|
"""إنشاء ترويسة الصفحة مع أيقونة وتصميم محسن""" |
|
|
|
st.markdown( |
|
f""" |
|
<div class="header-container"> |
|
<h1 class="header-title">{title}</h1> |
|
{f'<p class="header-subtitle">{subtitle}</p>' if subtitle else ''} |
|
</div> |
|
""", |
|
unsafe_allow_html=True |
|
) |
|
|
|
if show_actions: |
|
col1, col2 = st.columns([3, 1]) |
|
with col1: |
|
st.button("إضافة جديد", key=f"add_button_{title}", use_container_width=True) |
|
with col2: |
|
st.button("تحديث", key=f"update_button_{title}", use_container_width=True) |
|
|
|
st.divider() |
|
|
|
def create_metric_card(self, label, value, change=None, color=None): |
|
"""إنشاء بطاقة مقياس""" |
|
if color is None: |
|
color = self.COLORS['primary'] |
|
|
|
change_html = "" |
|
if change is not None: |
|
if change.startswith("+"): |
|
change_class = "metric-change-positive" |
|
change_icon = "↑" |
|
elif change.startswith("-"): |
|
change_class = "metric-change-negative" |
|
change_icon = "↓" |
|
else: |
|
change_class = "" |
|
change_icon = "" |
|
|
|
change_html = f'<div class="metric-change {change_class}">{change_icon} {change}</div>' |
|
|
|
st.markdown( |
|
f""" |
|
<div class="metric-card" style="border-top: 4px solid {color};"> |
|
<div class="metric-label">{label}</div> |
|
<div class="metric-value" style="color: {color};">{value}</div> |
|
{change_html} |
|
</div> |
|
""", |
|
unsafe_allow_html=True |
|
) |
|
|
|
def create_card(self, title, content, color=None): |
|
"""إنشاء بطاقة عامة""" |
|
if color is None: |
|
color = self.COLORS['primary'] |
|
|
|
st.markdown( |
|
f""" |
|
<div class="custom-card" style="border-top: 4px solid {color};"> |
|
<h3 style="color: {color}; margin-top: 0;">{title}</h3> |
|
<div>{content}</div> |
|
</div> |
|
""", |
|
unsafe_allow_html=True |
|
) |
|
|
|
def create_button(self, label, color=None, icon=None, key=None): |
|
"""إنشاء زر مخصص""" |
|
if color is None: |
|
color = self.COLORS['primary'] |
|
|
|
|
|
if key is None: |
|
key = f"button_{label}_{hash(label)}" |
|
|
|
icon_html = f"{icon} " if icon else "" |
|
|
|
return st.button( |
|
f"{icon_html}{label}", |
|
key=key |
|
) |
|
|
|
def create_tabs(self, tab_names): |
|
"""إنشاء تبويبات""" |
|
return st.tabs(tab_names) |
|
|
|
def create_expander(self, title, expanded=False, key=None): |
|
"""إنشاء عنصر قابل للتوسيع""" |
|
|
|
if key is None: |
|
key = f"expander_{title}_{hash(title)}" |
|
|
|
return st.expander(title, expanded=expanded, key=key) |
|
|
|
def create_data_table(self, data, use_container_width=True, hide_index=True): |
|
"""إنشاء جدول بيانات""" |
|
return st.dataframe(data, use_container_width=use_container_width, hide_index=hide_index) |
|
|
|
def create_chart(self, chart_type, data, **kwargs): |
|
"""إنشاء رسم بياني""" |
|
if chart_type == "bar": |
|
return st.bar_chart(data, **kwargs) |
|
elif chart_type == "line": |
|
return st.line_chart(data, **kwargs) |
|
elif chart_type == "area": |
|
return st.area_chart(data, **kwargs) |
|
else: |
|
return st.bar_chart(data, **kwargs) |
|
|
|
def create_form(self, title, key=None): |
|
"""إنشاء نموذج""" |
|
|
|
if key is None: |
|
key = f"form_{title}_{hash(title)}" |
|
|
|
return st.form(key=key) |
|
|
|
def create_file_uploader(self, label, types=None, key=None): |
|
"""إنشاء أداة رفع الملفات""" |
|
|
|
if key is None: |
|
key = f"file_uploader_{label}_{hash(label)}" |
|
|
|
return st.file_uploader(label, type=types, key=key) |
|
|
|
def create_date_input(self, label, value=None, key=None): |
|
"""إنشاء حقل إدخال تاريخ""" |
|
|
|
if key is None: |
|
key = f"date_input_{label}_{hash(label)}" |
|
|
|
return st.date_input(label, value=value, key=key) |
|
|
|
def create_select_box(self, label, options, index=0, key=None): |
|
"""إنشاء قائمة منسدلة""" |
|
|
|
if key is None: |
|
key = f"select_box_{label}_{hash(label)}" |
|
|
|
return st.selectbox(label, options, index=index, key=key) |
|
|
|
def create_multi_select(self, label, options, default=None, key=None): |
|
"""إنشاء قائمة اختيار متعدد""" |
|
|
|
if key is None: |
|
key = f"multi_select_{label}_{hash(label)}" |
|
|
|
return st.multiselect(label, options, default=default, key=key) |
|
|
|
def create_slider(self, label, min_value, max_value, value=None, step=1, key=None): |
|
"""إنشاء شريط تمرير""" |
|
|
|
if key is None: |
|
key = f"slider_{label}_{hash(label)}" |
|
|
|
return st.slider(label, min_value=min_value, max_value=max_value, value=value, step=step, key=key) |
|
|
|
def create_text_input(self, label, value="", key=None): |
|
"""إنشاء حقل إدخال نصي""" |
|
|
|
if key is None: |
|
key = f"text_input_{label}_{hash(label)}" |
|
|
|
return st.text_input(label, value=value, key=key) |
|
|
|
def create_text_area(self, label, value="", height=None, key=None): |
|
"""إنشاء منطقة نص""" |
|
|
|
if key is None: |
|
key = f"text_area_{label}_{hash(label)}" |
|
|
|
return st.text_area(label, value=value, height=height, key=key) |
|
|
|
def create_number_input(self, label, min_value=None, max_value=None, value=0, step=1, key=None): |
|
"""إنشاء حقل إدخال رقمي""" |
|
|
|
if key is None: |
|
key = f"number_input_{label}_{hash(label)}" |
|
|
|
return st.number_input(label, min_value=min_value, max_value=max_value, value=value, step=step, key=key) |
|
|
|
def create_checkbox(self, label, value=False, key=None): |
|
"""إنشاء خانة اختيار""" |
|
|
|
if key is None: |
|
key = f"checkbox_{label}_{hash(label)}" |
|
|
|
return st.checkbox(label, value=value, key=key) |
|
|
|
def create_radio(self, label, options, index=0, key=None): |
|
"""إنشاء أزرار راديو""" |
|
|
|
if key is None: |
|
key = f"radio_{label}_{hash(label)}" |
|
|
|
return st.radio(label, options, index=index, key=key) |
|
|
|
def create_progress_bar(self, value, key=None): |
|
"""إنشاء شريط تقدم""" |
|
|
|
if key is None: |
|
key = f"progress_bar_{value}_{hash(str(value))}" |
|
|
|
return st.progress(value, key=key) |
|
|
|
def create_spinner(self, text="جاري التحميل..."): |
|
"""إنشاء مؤشر تحميل""" |
|
return st.spinner(text) |
|
|
|
def create_success_message(self, message): |
|
"""إنشاء رسالة نجاح""" |
|
return st.success(message) |
|
|
|
def create_error_message(self, message): |
|
"""إنشاء رسالة خطأ""" |
|
return st.error(message) |
|
|
|
def create_warning_message(self, message): |
|
"""إنشاء رسالة تحذير""" |
|
return st.warning(message) |
|
|
|
def create_info_message(self, message): |
|
"""إنشاء رسالة معلومات""" |
|
return st.info(message) |