Spaces:
Sleeping
Sleeping
""" | |
تطبيق وحدة إدارة المشاريع | |
""" | |
import streamlit as st | |
import pandas as pd | |
import numpy as np | |
from datetime import datetime, timedelta | |
import os | |
import time | |
import io | |
from utils.helpers import format_number, format_currency | |
from utils.excel_handler import export_to_excel | |
class ProjectsApp: | |
"""وحدة إدارة المشاريع""" | |
def __init__(self): | |
"""تهيئة وحدة إدارة المشاريع""" | |
# تهيئة البيانات المبدئية | |
if 'projects' not in st.session_state: | |
st.session_state.projects = self._generate_sample_projects() | |
def render(self): | |
"""عرض واجهة وحدة إدارة المشاريع""" | |
st.markdown("<h1 class='module-title'>وحدة إدارة المشاريع</h1>", unsafe_allow_html=True) | |
tabs = st.tabs([ | |
"قائمة المشاريع", | |
"إضافة مشروع جديد", | |
"تفاصيل المشروع", | |
"متابعة المشاريع" | |
]) | |
with tabs[0]: | |
self._render_projects_list_tab() | |
with tabs[1]: | |
self._render_add_project_tab() | |
with tabs[2]: | |
self._render_project_details_tab() | |
with tabs[3]: | |
self._render_projects_tracking_tab() | |
def _render_projects_list_tab(self): | |
"""عرض تبويب قائمة المشاريع""" | |
st.markdown("### قائمة المشاريع") | |
# فلترة المشاريع | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
search_term = st.text_input("البحث في المشاريع", key="project_search") | |
with col2: | |
status_filter = st.multiselect( | |
"حالة المشروع", | |
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"], | |
default=["جديد", "قيد التسعير", "تم التقديم"], | |
key="project_status_filter" | |
) | |
with col3: | |
client_filter = st.multiselect( | |
"الجهة المالكة", | |
list(set([p['client'] for p in st.session_state.projects])), | |
key="project_client_filter" | |
) | |
# تطبيق الفلترة | |
filtered_projects = st.session_state.projects | |
if search_term: | |
filtered_projects = [p for p in filtered_projects if search_term.lower() in p['name'].lower() or search_term in p['number']] | |
if status_filter: | |
filtered_projects = [p for p in filtered_projects if p['status'] in status_filter] | |
if client_filter: | |
filtered_projects = [p for p in filtered_projects if p['client'] in client_filter] | |
# تحويل المشاريع المفلترة إلى DataFrame للعرض | |
if filtered_projects: | |
projects_df = pd.DataFrame(filtered_projects) | |
# اختيار وترتيب الأعمدة | |
display_columns = [ | |
'name', 'number', 'client', 'location', 'status', | |
'submission_date', 'tender_type', 'created_at' | |
] | |
# تغيير أسماء الأعمدة للعرض | |
column_names = { | |
'name': 'اسم المشروع', | |
'number': 'رقم المناقصة', | |
'client': 'الجهة المالكة', | |
'location': 'الموقع', | |
'status': 'الحالة', | |
'submission_date': 'تاريخ التقديم', | |
'tender_type': 'نوع المناقصة', | |
'created_at': 'تاريخ الإنشاء' | |
} | |
display_df = projects_df[display_columns].rename(columns=column_names) | |
# تنسيق التواريخ | |
date_columns = ['تاريخ التقديم', 'تاريخ الإنشاء'] | |
for col in date_columns: | |
if col in display_df.columns: | |
display_df[col] = pd.to_datetime(display_df[col]).dt.strftime('%Y-%m-%d') | |
# عرض الجدول | |
st.dataframe(display_df, use_container_width=True, hide_index=True) | |
# زر تصدير المشاريع | |
if st.button("تصدير المشاريع إلى Excel"): | |
# محاكاة التصدير | |
st.success("تم تصدير المشاريع بنجاح!") | |
else: | |
st.info("لا توجد مشاريع تطابق معايير البحث.") | |
def _render_add_project_tab(self): | |
"""عرض تبويب إضافة مشروع جديد""" | |
st.markdown("### إضافة مشروع جديد") | |
# نموذج إدخال بيانات المشروع | |
with st.form("new_project_form"): | |
col1, col2 = st.columns(2) | |
with col1: | |
project_name = st.text_input("اسم المشروع", key="new_project_name") | |
client = st.text_input("الجهة المالكة", key="new_project_client") | |
location = st.text_input("الموقع", key="new_project_location") | |
tender_type = st.selectbox( | |
"نوع المناقصة", | |
["عامة", "خاصة", "أمر مباشر"], | |
key="new_project_tender_type" | |
) | |
with col2: | |
tender_number = st.text_input("رقم المناقصة", key="new_project_number") | |
submission_date = st.date_input("تاريخ التقديم", key="new_project_submission_date") | |
pricing_method = st.selectbox( | |
"طريقة التسعير", | |
["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"], | |
key="new_project_pricing_method" | |
) | |
status = st.selectbox( | |
"حالة المشروع", | |
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"], | |
index=0, | |
key="new_project_status" | |
) | |
description = st.text_area("وصف المشروع", key="new_project_description") | |
submitted = st.form_submit_button("إضافة المشروع") | |
if submitted: | |
# التحقق من تعبئة الحقول الإلزامية | |
if not project_name or not tender_number or not client: | |
st.error("يرجى تعبئة جميع الحقول الإلزامية (اسم المشروع، رقم المناقصة، الجهة المالكة).") | |
else: | |
# إنشاء مشروع جديد | |
new_project = { | |
'id': len(st.session_state.projects) + 1, | |
'name': project_name, | |
'number': tender_number, | |
'client': client, | |
'location': location, | |
'description': description, | |
'status': status, | |
'tender_type': tender_type, | |
'pricing_method': pricing_method, | |
'submission_date': submission_date, | |
'created_at': datetime.now(), | |
'created_by_id': 1 # معرف المستخدم الحالي | |
} | |
# إضافة المشروع إلى قائمة المشاريع | |
st.session_state.projects.append(new_project) | |
# رسالة نجاح | |
st.success(f"تم إضافة المشروع [{project_name}] بنجاح!") | |
# تعيين المشروع الحالي | |
st.session_state.current_project = new_project | |
def _render_project_details_tab(self): | |
"""عرض تبويب تفاصيل المشروع""" | |
st.markdown("### تفاصيل المشروع") | |
# التحقق من وجود مشروع حالي | |
if 'current_project' not in st.session_state or st.session_state.current_project is None: | |
# إذا لم يكن هناك مشروع محدد، اعرض قائمة باختيار المشروع | |
project_names = [p['name'] for p in st.session_state.projects] | |
selected_project_name = st.selectbox("اختر المشروع", project_names) | |
if selected_project_name: | |
selected_project = next((p for p in st.session_state.projects if p['name'] == selected_project_name), None) | |
if selected_project: | |
st.session_state.current_project = selected_project | |
else: | |
st.warning("لم يتم العثور على المشروع المحدد.") | |
return | |
else: | |
st.info("يرجى اختيار مشروع لعرض تفاصيله.") | |
return | |
# عرض تفاصيل المشروع | |
project = st.session_state.current_project | |
# عرض معلومات المشروع الأساسية | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
st.markdown(f"**اسم المشروع**: {project['name']}") | |
st.markdown(f"**رقم المناقصة**: {project['number']}") | |
st.markdown(f"**الجهة المالكة**: {project['client']}") | |
with col2: | |
st.markdown(f"**الموقع**: {project['location']}") | |
st.markdown(f"**نوع المناقصة**: {project['tender_type']}") | |
st.markdown(f"**حالة المشروع**: {project['status']}") | |
with col3: | |
st.markdown(f"**طريقة التسعير**: {project['pricing_method']}") | |
st.markdown(f"**تاريخ التقديم**: {project['submission_date'].strftime('%Y-%m-%d') if isinstance(project['submission_date'], datetime) else project['submission_date']}") | |
st.markdown(f"**تاريخ الإنشاء**: {project['created_at'].strftime('%Y-%m-%d') if isinstance(project['created_at'], datetime) else project['created_at']}") | |
# عرض وصف المشروع | |
st.markdown("#### وصف المشروع") | |
st.text_area("", value=project.get('description', ''), disabled=True, height=100) | |
# عرض المستندات المرتبطة بالمشروع | |
st.markdown("#### مستندات المشروع") | |
if 'documents' in project and project['documents']: | |
docs_df = pd.DataFrame(project['documents']) | |
st.dataframe(docs_df, use_container_width=True, hide_index=True) | |
else: | |
st.info("لا توجد مستندات مرتبطة بهذا المشروع حاليًا.") | |
# زر إضافة مستندات | |
if st.button("إضافة مستندات"): | |
st.session_state.upload_documents = True | |
# واجهة تحميل المستندات | |
if 'upload_documents' in st.session_state and st.session_state.upload_documents: | |
st.markdown("#### تحميل مستندات جديدة") | |
uploaded_file = st.file_uploader("اختر ملفًا", type=['pdf', 'docx', 'xlsx', 'png', 'jpg', 'dwg']) | |
doc_type = st.selectbox("نوع المستند", ["كراسة شروط", "عقد", "مخططات", "جدول كميات", "مواصفات فنية", "تعديلات وملاحق"]) | |
if uploaded_file and st.button("تحميل المستند"): | |
# محاكاة تحميل المستند | |
with st.spinner("جاري تحميل المستند..."): | |
time.sleep(2) | |
# إنشاء مستند جديد | |
new_document = { | |
'filename': uploaded_file.name, | |
'type': doc_type, | |
'upload_date': datetime.now().strftime('%Y-%m-%d'), | |
'size': f"{uploaded_file.size / 1024:.1f} KB" | |
} | |
# إضافة المستند إلى المشروع | |
if 'documents' not in project: | |
project['documents'] = [] | |
project['documents'].append(new_document) | |
st.success(f"تم تحميل المستند [{uploaded_file.name}] بنجاح!") | |
st.session_state.upload_documents = False | |
st.experimental_rerun() | |
# عرض البنود والكميات | |
st.markdown("#### بنود وكميات المشروع") | |
if 'items' in project and project['items']: | |
items_df = pd.DataFrame(project['items']) | |
st.dataframe(items_df, use_container_width=True, hide_index=True) | |
# زر لتحويل البنود إلى وحدة التسعير | |
if st.button("تحويل البنود إلى وحدة التسعير"): | |
if 'manual_items' not in st.session_state: | |
st.session_state.manual_items = pd.DataFrame() | |
st.session_state.manual_items = items_df.copy() | |
st.success("تم تحويل البنود إلى وحدة التسعير بنجاح!") | |
else: | |
st.info("لا توجد بنود وكميات لهذا المشروع حاليًا.") | |
# زر استيراد البنود من وحدة تحليل المستندات | |
if st.button("استيراد البنود من تحليل المستندات"): | |
st.warning("ميزة استيراد البنود من تحليل المستندات قيد التطوير.") | |
# أزرار الإجراءات | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
if st.button("تعديل المشروع"): | |
st.session_state.edit_project = True | |
st.experimental_rerun() | |
with col2: | |
if st.button("تصدير بيانات المشروع"): | |
st.success("تم تصدير بيانات المشروع بنجاح!") | |
with col3: | |
if st.button("إرسال للاعتماد"): | |
st.success("تم إرسال المشروع للاعتماد بنجاح!") | |
# نموذج تعديل المشروع | |
if 'edit_project' in st.session_state and st.session_state.edit_project: | |
st.markdown("#### تعديل المشروع") | |
with st.form("edit_project_form"): | |
col1, col2 = st.columns(2) | |
with col1: | |
project_name = st.text_input("اسم المشروع", value=project['name']) | |
client = st.text_input("الجهة المالكة", value=project['client']) | |
location = st.text_input("الموقع", value=project['location']) | |
tender_type = st.selectbox( | |
"نوع المناقصة", | |
["عامة", "خاصة", "أمر مباشر"], | |
index=["عامة", "خاصة", "أمر مباشر"].index(project['tender_type']) | |
) | |
with col2: | |
tender_number = st.text_input("رقم المناقصة", value=project['number']) | |
submission_date = st.date_input( | |
"تاريخ التقديم", | |
value=datetime.strptime(project['submission_date'], "%Y-%m-%d") if isinstance(project['submission_date'], str) else project['submission_date'] | |
) | |
pricing_method = st.selectbox( | |
"طريقة التسعير", | |
["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"], | |
index=["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"].index(project['pricing_method']) | |
) | |
status = st.selectbox( | |
"حالة المشروع", | |
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"], | |
index=["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"].index(project['status']) | |
) | |
description = st.text_area("وصف المشروع", value=project.get('description', '')) | |
col1, col2 = st.columns(2) | |
with col1: | |
submit = st.form_submit_button("حفظ التعديلات") | |
with col2: | |
cancel = st.form_submit_button("إلغاء") | |
if submit: | |
# تحديث بيانات المشروع | |
project['name'] = project_name | |
project['number'] = tender_number | |
project['client'] = client | |
project['location'] = location | |
project['description'] = description | |
project['status'] = status | |
project['tender_type'] = tender_type | |
project['pricing_method'] = pricing_method | |
project['submission_date'] = submission_date | |
st.success("تم تحديث بيانات المشروع بنجاح!") | |
st.session_state.edit_project = False | |
st.experimental_rerun() | |
elif cancel: | |
st.session_state.edit_project = False | |
st.experimental_rerun() | |
def _render_projects_tracking_tab(self): | |
"""عرض تبويب متابعة المشاريع""" | |
st.markdown("### متابعة المشاريع") | |
# عرض إحصائيات المشاريع | |
col1, col2, col3, col4 = st.columns(4) | |
projects = st.session_state.projects | |
with col1: | |
total_projects = len(projects) | |
st.metric("إجمالي المشاريع", total_projects) | |
with col2: | |
active_projects = len([p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]]) | |
st.metric("المشاريع النشطة", active_projects) | |
with col3: | |
pending_submission = len([p for p in projects if p['status'] in ["جديد", "قيد التسعير"]]) | |
st.metric("مشاريع قيد التسعير", pending_submission) | |
with col4: | |
completed_projects = len([p for p in projects if p['status'] in ["منتهي"]]) | |
st.metric("المشاريع المنتهية", completed_projects) | |
# عرض رسم بياني لحالة المشاريع | |
st.markdown("#### توزيع المشاريع حسب الحالة") | |
status_counts = {} | |
for p in projects: | |
status = p['status'] | |
status_counts[status] = status_counts.get(status, 0) + 1 | |
status_df = pd.DataFrame({ | |
'الحالة': list(status_counts.keys()), | |
'عدد المشاريع': list(status_counts.values()) | |
}) | |
st.bar_chart(status_df.set_index('الحالة')) | |
# عرض المشاريع قيد المتابعة | |
st.markdown("#### المشاريع قيد المتابعة") | |
# عرض المشاريع النشطة المرتبة حسب تاريخ التقديم | |
active_projects_list = [p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]] | |
if active_projects_list: | |
# تحويل التواريخ إلى كائنات تاريخ إذا كانت نصوصًا | |
for p in active_projects_list: | |
if isinstance(p['submission_date'], str): | |
p['submission_date'] = datetime.strptime(p['submission_date'], "%Y-%m-%d") | |
# ترتيب المشاريع حسب تاريخ التقديم | |
active_projects_list.sort(key=lambda x: x['submission_date']) | |
# تحويل إلى DataFrame | |
active_df = pd.DataFrame(active_projects_list) | |
# اختيار وترتيب الأعمدة | |
display_columns = [ | |
'name', 'number', 'client', 'status', | |
'submission_date', 'tender_type' | |
] | |
# تغيير أسماء الأعمدة | |
column_names = { | |
'name': 'اسم المشروع', | |
'number': 'رقم المناقصة', | |
'client': 'الجهة المالكة', | |
'status': 'الحالة', | |
'submission_date': 'تاريخ التقديم', | |
'tender_type': 'نوع المناقصة' | |
} | |
# تنسيق البيانات | |
display_df = active_df[display_columns].rename(columns=column_names) | |
display_df['تاريخ التقديم'] = pd.to_datetime(display_df['تاريخ التقديم']).dt.strftime('%Y-%m-%d') | |
# عرض الجدول | |
st.dataframe(display_df, use_container_width=True, hide_index=True) | |
else: | |
st.info("لا توجد مشاريع نشطة حاليًا.") | |
# عرض المشاريع المقبلة | |
st.markdown("#### المواعيد المقبلة") | |
upcoming_events = [] | |
today = datetime.now().date() | |
for p in projects: | |
submission_date = p['submission_date'] | |
if isinstance(submission_date, str): | |
submission_date = datetime.strptime(submission_date, "%Y-%m-%d").date() | |
elif isinstance(submission_date, datetime): | |
submission_date = submission_date.date() | |
# المشاريع التي موعد تقديمها خلال الأسبوعين القادمين | |
if today <= submission_date <= today + timedelta(days=14) and p['status'] in ["قيد التسعير"]: | |
days_left = (submission_date - today).days | |
upcoming_events.append({ | |
'المشروع': p['name'], | |
'الحدث': 'موعد تقديم المناقصة', | |
'التاريخ': submission_date.strftime('%Y-%m-%d'), | |
'الأيام المتبقية': days_left | |
}) | |
if upcoming_events: | |
events_df = pd.DataFrame(upcoming_events) | |
st.dataframe(events_df, use_container_width=True, hide_index=True) | |
else: | |
st.info("لا توجد مواعيد قريبة.") | |
def _generate_sample_projects(self): | |
"""توليد بيانات افتراضية للمشاريع""" | |
projects = [ | |
{ | |
'id': 1, | |
'name': "إنشاء مبنى مستشفى الولادة والأطفال بمنطقة الشرقية", | |
'number': "SHPD-2025-001", | |
'client': "وزارة الصحة", | |
'location': "الدمام، المنطقة الشرقية", | |
'description': "يشمل المشروع إنشاء وتجهيز مبنى مستشفى الولادة والأطفال بسعة 300 سرير، ويتكون المبنى من 4 طوابق بمساحة إجمالية 15,000 متر مربع.", | |
'status': "قيد التسعير", | |
'tender_type': "عامة", | |
'pricing_method': "قياسي", | |
'submission_date': (datetime.now() + timedelta(days=5)), | |
'created_at': datetime.now() - timedelta(days=10), | |
'created_by_id': 1, | |
'documents': [ | |
{ | |
'filename': "كراسة الشروط والمواصفات.pdf", | |
'type': "كراسة شروط", | |
'upload_date': (datetime.now() - timedelta(days=9)).strftime('%Y-%m-%d'), | |
'size': "5.2 MB" | |
}, | |
{ | |
'filename': "المخططات الهندسية.dwg", | |
'type': "مخططات", | |
'upload_date': (datetime.now() - timedelta(days=8)).strftime('%Y-%m-%d'), | |
'size': "25.7 MB" | |
}, | |
{ | |
'filename': "جدول الكميات.xlsx", | |
'type': "جدول كميات", | |
'upload_date': (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'), | |
'size': "1.8 MB" | |
} | |
], | |
'items': [ | |
{ | |
'رقم البند': "A1", | |
'وصف البند': "أعمال الحفر والردم", | |
'الوحدة': "م3", | |
'الكمية': 12500 | |
}, | |
{ | |
'رقم البند': "A2", | |
'وصف البند': "أعمال الخرسانة المسلحة للأساسات", | |
'الوحدة': "م3", | |
'الكمية': 3500 | |
}, | |
{ | |
'رقم البند': "A3", | |
'وصف البند': "أعمال حديد التسليح", | |
'الوحدة': "طن", | |
'الكمية': 450 | |
} | |
] | |
}, | |
{ | |
'id': 2, | |
'name': "صيانة وتطوير طريق الملك عبدالله", | |
'number': "MOT-2025-042", | |
'client': "وزارة النقل", | |
'location': "الرياض، المنطقة الوسطى", | |
'description': "صيانة وتطوير طريق الملك عبدالله بطول 25 كم، ويشمل المشروع إعادة الرصف وتحسين الإنارة وتركيب اللوحات الإرشادية.", | |
'status': "تم التقديم", | |
'tender_type': "عامة", | |
'pricing_method': "غير متزن", | |
'submission_date': (datetime.now() - timedelta(days=15)), | |
'created_at': datetime.now() - timedelta(days=45), | |
'created_by_id': 1 | |
}, | |
{ | |
'id': 3, | |
'name': "إنشاء محطة معالجة مياه الصرف الصحي", | |
'number': "SWPC-2025-007", | |
'client': "شركة المياه الوطنية", | |
'location': "جدة، المنطقة الغربية", | |
'description': "إنشاء محطة معالجة مياه الصرف الصحي بطاقة استيعابية 50,000 م3/يوم، مع جميع الأعمال المدنية والكهروميكانيكية.", | |
'status': "تمت الترسية", | |
'tender_type': "عامة", | |
'pricing_method': "قياسي", | |
'submission_date': (datetime.now() - timedelta(days=90)), | |
'created_at': datetime.now() - timedelta(days=120), | |
'created_by_id': 1 | |
}, | |
{ | |
'id': 4, | |
'name': "إنشاء منتزه الملك سلمان", | |
'number': "RAM-2025-015", | |
'client': "أمانة منطقة الرياض", | |
'location': "الرياض، المنطقة الوسطى", | |
'description': "إنشاء منتزه الملك سلمان على مساحة 500,000 متر مربع، ويشمل المشروع أعمال التشجير والتنسيق والمسطحات المائية والمباني الخدمية.", | |
'status': "قيد التنفيذ", | |
'tender_type': "عامة", | |
'pricing_method': "قياسي", | |
'submission_date': (datetime.now() - timedelta(days=180)), | |
'created_at': datetime.now() - timedelta(days=210), | |
'created_by_id': 1 | |
}, | |
{ | |
'id': 5, | |
'name': "إنشاء مبنى مختبرات كلية العلوم", | |
'number': "KSU-2025-032", | |
'client': "جامعة الملك سعود", | |
'location': "الرياض، المنطقة الوسطى", | |
'description': "إنشاء مبنى المختبرات الجديد لكلية العلوم بمساحة 8,000 متر مربع، ويتكون من 3 طوابق ويشمل تجهيز المعامل والمختبرات العلمية.", | |
'status': "جديد", | |
'tender_type': "خاصة", | |
'pricing_method': "تنافسي", | |
'submission_date': (datetime.now() + timedelta(days=10)), | |
'created_at': datetime.now() - timedelta(days=5), | |
'created_by_id': 1 | |
}, | |
{ | |
'id': 6, | |
'name': "توريد وتركيب أنظمة الطاقة الشمسية", | |
'number': "SEC-2025-098", | |
'client': "الشركة السعودية للكهرباء", | |
'location': "تبوك، المنطقة الشمالية", | |
'description': "توريد وتركيب أنظمة الطاقة الشمسية بقدرة 5 ميجاوات، مع جميع الأعمال المدنية والكهربائية.", | |
'status': "جديد", | |
'tender_type': "عامة", | |
'pricing_method': "قياسي", | |
'submission_date': (datetime.now() + timedelta(days=20)), | |
'created_at': datetime.now() - timedelta(days=2), | |
'created_by_id': 1 | |
} | |
] | |
return projects |