|
""" |
|
تحسينات التصميم المرئي وواجهة المستخدم لنظام تحليل المناقصات |
|
""" |
|
|
|
import streamlit as st |
|
import streamlit_option_menu as option_menu |
|
from streamlit_extras.colored_header import colored_header |
|
from streamlit_extras.switch_page_button import switch_page |
|
import os |
|
import sys |
|
from pathlib import Path |
|
|
|
|
|
sys.path.append(str(Path(__file__).parent.parent)) |
|
|
|
class UIEnhancer: |
|
"""فئة لتحسين واجهة المستخدم وتوحيد التصميم المرئي عبر النظام""" |
|
|
|
|
|
COLORS = { |
|
'primary': '#1E5F74', |
|
'secondary': '#4BA3C3', |
|
'accent': '#F39237', |
|
'success': '#4CAF50', |
|
'warning': '#FFC107', |
|
'danger': '#E63946', |
|
'light': '#F5F5F5', |
|
'dark': '#1A1A1A', |
|
'text_light': '#FFFFFF', |
|
'text_dark': '#333333', |
|
'border': '#DDDDDD', |
|
'hover': '#2A7F9E', |
|
} |
|
|
|
|
|
CUSTOM_CSS = """ |
|
<style> |
|
/* تخصيص الشعار والعنوان */ |
|
.logo-title { |
|
display: flex; |
|
align-items: center; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.logo-img { |
|
height: 60px; |
|
margin-right: 10px; |
|
} |
|
|
|
/* تخصيص البطاقات */ |
|
.card { |
|
border-radius: 10px; |
|
padding: 1.5rem; |
|
margin-bottom: 1rem; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
transition: transform 0.3s ease, box-shadow 0.3s ease; |
|
} |
|
|
|
.card:hover { |
|
transform: translateY(-5px); |
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); |
|
} |
|
|
|
/* تخصيص الأزرار */ |
|
.custom-button { |
|
border-radius: 8px; |
|
padding: 0.5rem 1rem; |
|
font-weight: 500; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.custom-button:hover { |
|
opacity: 0.9; |
|
transform: translateY(-2px); |
|
} |
|
|
|
/* تخصيص القوائم */ |
|
.nav-link { |
|
border-radius: 8px; |
|
margin: 0.2rem 0; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.nav-link:hover { |
|
transform: translateX(5px); |
|
} |
|
|
|
/* تخصيص الجداول */ |
|
.styled-table { |
|
width: 100%; |
|
border-collapse: separate; |
|
border-spacing: 0; |
|
border-radius: 10px; |
|
overflow: hidden; |
|
} |
|
|
|
.styled-table th { |
|
background-color: var(--primary-color); |
|
color: white; |
|
padding: 12px 15px; |
|
text-align: right; |
|
} |
|
|
|
.styled-table td { |
|
padding: 12px 15px; |
|
border-bottom: 1px solid var(--border-color); |
|
} |
|
|
|
.styled-table tr:last-child td { |
|
border-bottom: none; |
|
} |
|
|
|
.styled-table tr:nth-child(even) { |
|
background-color: rgba(0, 0, 0, 0.05); |
|
} |
|
|
|
/* تخصيص لوحات المعلومات */ |
|
.dashboard-metric { |
|
text-align: center; |
|
padding: 1rem; |
|
border-radius: 10px; |
|
background-color: var(--secondary-color); |
|
color: white; |
|
} |
|
|
|
.dashboard-metric h3 { |
|
font-size: 2rem; |
|
margin: 0; |
|
} |
|
|
|
.dashboard-metric p { |
|
margin: 0; |
|
opacity: 0.8; |
|
} |
|
|
|
/* تخصيص الرسوم البيانية */ |
|
.chart-container { |
|
border-radius: 10px; |
|
padding: 1rem; |
|
background-color: white; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
/* تخصيص الإشعارات */ |
|
.notification { |
|
padding: 1rem; |
|
border-radius: 8px; |
|
margin-bottom: 0.5rem; |
|
display: flex; |
|
align-items: center; |
|
} |
|
|
|
.notification-icon { |
|
margin-left: 1rem; |
|
font-size: 1.5rem; |
|
} |
|
|
|
/* تخصيص الشريط الجانبي */ |
|
.sidebar .sidebar-content { |
|
background-color: var(--primary-color); |
|
color: white; |
|
} |
|
|
|
/* تحسين اتجاه النص للغة العربية */ |
|
body { |
|
direction: rtl; |
|
text-align: right; |
|
} |
|
|
|
/* تخصيص الخط */ |
|
@font-face { |
|
font-family: 'Tajawal'; |
|
src: url('https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700&display=swap'); |
|
} |
|
|
|
* { |
|
font-family: 'Tajawal', sans-serif; |
|
} |
|
</style> |
|
""" |
|
|
|
def __init__(self, page_title="نظام تحليل المناقصات", page_icon="📊", layout="wide"): |
|
"""تهيئة محسن واجهة المستخدم""" |
|
self.page_title = page_title |
|
self.page_icon = page_icon |
|
self.layout = layout |
|
self.setup_page() |
|
|
|
def setup_page(self): |
|
"""إعداد الصفحة وتطبيق الإعدادات العامة""" |
|
st.set_page_config( |
|
page_title=self.page_title, |
|
page_icon=self.page_icon, |
|
layout=self.layout, |
|
initial_sidebar_state="expanded" |
|
) |
|
|
|
|
|
st.markdown(self.CUSTOM_CSS, unsafe_allow_html=True) |
|
|
|
|
|
if 'theme' not in st.session_state: |
|
st.session_state.theme = 'light' |
|
if 'language' not in st.session_state: |
|
st.session_state.language = 'ar' |
|
|
|
def create_sidebar(self, menu_items): |
|
"""إنشاء شريط جانبي محسن مع قائمة""" |
|
with st.sidebar: |
|
|
|
st.markdown( |
|
f""" |
|
<div class="logo-title"> |
|
<img src="./assets/images/logo.png" class="logo-img"> |
|
<h2>{self.page_title}</h2> |
|
</div> |
|
""", |
|
unsafe_allow_html=True |
|
) |
|
|
|
|
|
selected = option_menu.option_menu( |
|
menu_title=None, |
|
options=[item["name"] for item in menu_items], |
|
icons=[item["icon"] for item in menu_items], |
|
menu_icon="cast", |
|
default_index=0, |
|
styles={ |
|
"container": {"padding": "0!important", "background-color": f"{self.COLORS['primary']}"}, |
|
"icon": {"color": "white", "font-size": "18px"}, |
|
"nav-link": {"color": "white", "font-size": "16px", "text-align": "right", "margin":"0px"}, |
|
"nav-link-selected": {"background-color": f"{self.COLORS['accent']}"}, |
|
} |
|
) |
|
|
|
|
|
st.markdown("<hr>", unsafe_allow_html=True) |
|
col1, col2 = st.columns([1, 3]) |
|
with col1: |
|
st.write("السمة:") |
|
with col2: |
|
if st.toggle("الوضع الداكن", st.session_state.theme == 'dark'): |
|
st.session_state.theme = 'dark' |
|
else: |
|
st.session_state.theme = 'light' |
|
|
|
|
|
st.markdown("<hr>", unsafe_allow_html=True) |
|
st.markdown( |
|
f""" |
|
<div style="text-align: center;"> |
|
<p>مرحباً، المستخدم</p> |
|
<button class="custom-button" style="background-color: {self.COLORS['danger']}; color: white; border: none;">تسجيل الخروج</button> |
|
</div> |
|
""", |
|
unsafe_allow_html=True |
|
) |
|
|
|
return selected |
|
|
|
def create_header(self, title, description=None): |
|
"""إنشاء ترويسة محسنة للصفحة""" |
|
colored_header( |
|
label=title, |
|
description=description, |
|
color_name=self.COLORS['primary'] |
|
) |
|
|
|
def create_card(self, title, content, color=None): |
|
"""إنشاء بطاقة محسنة""" |
|
bg_color = color if color else self.COLORS['light'] if st.session_state.theme == 'light' else self.COLORS['dark'] |
|
text_color = self.COLORS['text_dark'] if st.session_state.theme == 'light' else self.COLORS['text_light'] |
|
|
|
st.markdown( |
|
f""" |
|
<div class="card" style="background-color: {bg_color}; color: {text_color};"> |
|
<h3>{title}</h3> |
|
<p>{content}</p> |
|
</div> |
|
""", |
|
unsafe_allow_html=True |
|
) |
|
|
|
def create_metric_card(self, title, value, delta=None, color=None): |
|
"""إنشاء بطاقة مقاييس محسنة""" |
|
bg_color = color if color else self.COLORS['primary'] |
|
|
|
delta_html = f"<span style='color: {'green' if delta > 0 else 'red'};'>{delta}%</span>" if delta is not None else "" |
|
|
|
st.markdown( |
|
f""" |
|
<div class="dashboard-metric" style="background-color: {bg_color};"> |
|
<p>{title}</p> |
|
<h3>{value}</h3> |
|
{delta_html} |
|
</div> |
|
""", |
|
unsafe_allow_html=True |
|
) |
|
|
|
def create_notification(self, message, type_="info"): |
|
"""إنشاء إشعار محسن""" |
|
colors = { |
|
"info": self.COLORS['secondary'], |
|
"success": self.COLORS['success'], |
|
"warning": self.COLORS['warning'], |
|
"error": self.COLORS['danger'] |
|
} |
|
|
|
icons = { |
|
"info": "ℹ️", |
|
"success": "✅", |
|
"warning": "⚠️", |
|
"error": "❌" |
|
} |
|
|
|
bg_color = colors.get(type_, colors["info"]) |
|
icon = icons.get(type_, icons["info"]) |
|
|
|
st.markdown( |
|
f""" |
|
<div class="notification" style="background-color: {bg_color};"> |
|
<div class="notification-icon">{icon}</div> |
|
<div>{message}</div> |
|
</div> |
|
""", |
|
unsafe_allow_html=True |
|
) |
|
|
|
def create_button(self, label, type_="primary", on_click=None): |
|
"""إنشاء زر محسن""" |
|
colors = { |
|
"primary": self.COLORS['primary'], |
|
"secondary": self.COLORS['secondary'], |
|
"accent": self.COLORS['accent'], |
|
"success": self.COLORS['success'], |
|
"warning": self.COLORS['warning'], |
|
"danger": self.COLORS['danger'] |
|
} |
|
|
|
bg_color = colors.get(type_, colors["primary"]) |
|
text_color = self.COLORS['text_light'] |
|
|
|
return st.button( |
|
label, |
|
key=f"btn_{label}_{type_}", |
|
on_click=on_click, |
|
use_container_width=True |
|
) |
|
|
|
def create_table(self, data, columns): |
|
"""إنشاء جدول محسن""" |
|
|
|
table_html = f""" |
|
<table class="styled-table"> |
|
<thead> |
|
<tr> |
|
{"".join([f"<th>{col}</th>" for col in columns])} |
|
</tr> |
|
</thead> |
|
<tbody> |
|
""" |
|
|
|
for row in data: |
|
table_html += "<tr>" |
|
for col in columns: |
|
table_html += f"<td>{row.get(col, '')}</td>" |
|
table_html += "</tr>" |
|
|
|
table_html += """ |
|
</tbody> |
|
</table> |
|
""" |
|
|
|
st.markdown(table_html, unsafe_allow_html=True) |
|
|
|
def apply_theme_colors(self): |
|
"""تطبيق ألوان السمة الحالية""" |
|
theme_colors = { |
|
'light': { |
|
'background': self.COLORS['light'], |
|
'text': self.COLORS['text_dark'], |
|
'border': self.COLORS['border'] |
|
}, |
|
'dark': { |
|
'background': self.COLORS['dark'], |
|
'text': self.COLORS['text_light'], |
|
'border': '#444444' |
|
} |
|
} |
|
|
|
current_theme = theme_colors[st.session_state.theme] |
|
|
|
st.markdown( |
|
f""" |
|
<style> |
|
:root {{ |
|
--background-color: {current_theme['background']}; |
|
--text-color: {current_theme['text']}; |
|
--border-color: {current_theme['border']}; |
|
--primary-color: {self.COLORS['primary']}; |
|
--secondary-color: {self.COLORS['secondary']}; |
|
--accent-color: {self.COLORS['accent']}; |
|
}} |
|
|
|
.stApp {{ |
|
background-color: var(--background-color); |
|
color: var(--text-color); |
|
}} |
|
|
|
.card {{ |
|
background-color: var(--background-color); |
|
color: var(--text-color); |
|
border: 1px solid var(--border-color); |
|
}} |
|
</style> |
|
""", |
|
unsafe_allow_html=True |
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
ui = UIEnhancer() |
|
ui.apply_theme_colors() |
|
|
|
|
|
menu_items = [ |
|
{"name": "لوحة المعلومات", "icon": "house"}, |
|
{"name": "المناقصات والعقود", "icon": "file-text"}, |
|
{"name": "تحليل المستندات", "icon": "file-earmark-text"}, |
|
{"name": "نظام التسعير", "icon": "calculator"}, |
|
{"name": "حاسبة تكاليف البناء", "icon": "building"}, |
|
{"name": "الموارد والتكاليف", "icon": "people"}, |
|
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"}, |
|
{"name": "إدارة المشاريع", "icon": "kanban"}, |
|
{"name": "الخرائط والمواقع", "icon": "geo-alt"}, |
|
{"name": "الجدول الزمني", "icon": "calendar3"}, |
|
{"name": "الإشعارات", "icon": "bell"}, |
|
{"name": "مقارنة المستندات", "icon": "files"}, |
|
{"name": "المساعد الذكي", "icon": "robot"}, |
|
{"name": "التقارير", "icon": "bar-chart"}, |
|
{"name": "الإعدادات", "icon": "gear"} |
|
] |
|
|
|
|
|
selected = ui.create_sidebar(menu_items) |
|
|
|
|
|
ui.create_header("لوحة المعلومات", "نظرة عامة على المناقصات والمشاريع") |
|
|
|
|
|
col1, col2, col3, col4 = st.columns(4) |
|
|
|
with col1: |
|
ui.create_metric_card("المناقصات النشطة", "12", delta=5) |
|
|
|
with col2: |
|
ui.create_metric_card("المشاريع الجارية", "8", delta=-2) |
|
|
|
with col3: |
|
ui.create_metric_card("المناقصات المربوحة", "65%", delta=10) |
|
|
|
with col4: |
|
ui.create_metric_card("قيمة المشاريع", "25M ريال", delta=15) |
|
|
|
|
|
col1, col2 = st.columns([2, 1]) |
|
|
|
with col1: |
|
ui.create_header("المناقصات الحالية", "آخر تحديث: اليوم") |
|
|
|
|
|
tenders_data = [ |
|
{"رقم المناقصة": "T-2025-001", "العنوان": "إنشاء مبنى إداري", "الحالة": "قيد الدراسة", "تاريخ التقديم": "2025-04-15"}, |
|
{"رقم المناقصة": "T-2025-002", "العنوان": "صيانة طرق", "الحالة": "تم التقديم", "تاريخ التقديم": "2025-03-20"}, |
|
{"رقم المناقصة": "T-2025-003", "العنوان": "توريد معدات", "الحالة": "فائز", "تاريخ التقديم": "2025-02-10"} |
|
] |
|
|
|
ui.create_table(tenders_data, ["رقم المناقصة", "العنوان", "الحالة", "تاريخ التقديم"]) |
|
|
|
with col2: |
|
ui.create_header("الإشعارات", "آخر التنبيهات") |
|
|
|
ui.create_notification("موعد تسليم مناقصة T-2025-001 بعد 5 أيام", "warning") |
|
ui.create_notification("تم ترسية مناقصة T-2025-003", "success") |
|
ui.create_notification("تم تحديث مستندات مناقصة T-2025-002", "info") |
|
|
|
st.markdown("<br>", unsafe_allow_html=True) |
|
ui.create_button("عرض جميع الإشعارات", "secondary") |
|
|
|
|
|
st.markdown("<br>", unsafe_allow_html=True) |
|
ui.create_header("الإجراءات السريعة", "اختر إجراءً للبدء") |
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
with col1: |
|
ui.create_card( |
|
"إضافة مناقصة جديدة", |
|
"إنشاء مناقصة جديدة وإدخال البيانات الأساسية" |
|
) |
|
|
|
with col2: |
|
ui.create_card( |
|
"تحليل مستند", |
|
"تحميل مستند مناقصة جديد للتحليل التلقائي" |
|
) |
|
|
|
with col3: |
|
ui.create_card( |
|
"إنشاء تقرير", |
|
"إنشاء تقارير مخصصة للمناقصات والمشاريع" |
|
) |
|
|