|
|
|
import os |
|
import streamlit as st |
|
import pandas as pd |
|
import plotly.figure_factory as ff |
|
from datetime import datetime, timedelta |
|
import plotly.express as px |
|
import numpy as np |
|
import io |
|
import openpyxl |
|
|
|
class ScheduleApp: |
|
def __init__(self): |
|
if 'saved_pricing' not in st.session_state: |
|
st.session_state.saved_pricing = [] |
|
if 'uploaded_files' not in st.session_state: |
|
st.session_state.uploaded_files = {} |
|
|
|
def run(self): |
|
st.title("الجدول الزمني للمشروع") |
|
|
|
|
|
tabs = st.tabs(["جدول الكميات", "ملفات المشروع"]) |
|
|
|
with tabs[0]: |
|
self._handle_boq_tab() |
|
|
|
with tabs[1]: |
|
self._handle_project_files() |
|
|
|
def _handle_project_files(self): |
|
st.subheader("إدارة ملفات المشروع") |
|
|
|
|
|
uploaded_file = st.file_uploader( |
|
"قم برفع ملفات المشروع", |
|
type=['xls', 'xlsx', 'xml', 'xer', 'pmxml', 'mpp', 'vsdx'], |
|
help="يمكنك رفع ملفات من برامج مثل Primavera P6, Microsoft Project, Power BI, Visio" |
|
) |
|
|
|
if uploaded_file: |
|
try: |
|
|
|
if uploaded_file.name.endswith(('.xls', '.xlsx')): |
|
df = pd.read_excel(uploaded_file) |
|
st.session_state.uploaded_files[uploaded_file.name] = { |
|
'data': df, |
|
'type': 'excel', |
|
'upload_time': datetime.now() |
|
} |
|
|
|
|
|
st.success(f"تم استيراد الملف: {uploaded_file.name}") |
|
st.write("معلومات الملف:") |
|
st.write(f"- عدد الأنشطة: {len(df)}") |
|
st.write(f"- الأعمدة: {', '.join(df.columns)}") |
|
|
|
|
|
st.dataframe(df, use_container_width=True) |
|
|
|
|
|
if 'Start' in df.columns and 'Finish' in df.columns: |
|
self._create_interactive_gantt(df) |
|
|
|
else: |
|
st.info(f"تم استلام الملف {uploaded_file.name}. سيتم إضافة دعم لهذا النوع من الملفات قريباً.") |
|
|
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء معالجة الملف: {str(e)}") |
|
|
|
|
|
if st.session_state.uploaded_files: |
|
st.subheader("الملفات المحفوظة") |
|
for filename, file_info in st.session_state.uploaded_files.items(): |
|
with st.expander(filename): |
|
st.write(f"نوع الملف: {file_info['type']}") |
|
st.write(f"تاريخ الرفع: {file_info['upload_time']}") |
|
if file_info['type'] == 'excel': |
|
st.dataframe(file_info['data'], use_container_width=True) |
|
|
|
def _create_interactive_gantt(self, df): |
|
st.subheader("مخطط جانت التفاعلي") |
|
|
|
|
|
df['Start'] = pd.to_datetime(df['Start']) |
|
df['Finish'] = pd.to_datetime(df['Finish']) |
|
|
|
fig = ff.create_gantt( |
|
df, |
|
colors={ |
|
'Task': '#2196F3', |
|
'Complete': '#4CAF50' |
|
}, |
|
index_col='Resource', |
|
show_colorbar=True, |
|
group_tasks=True, |
|
showgrid_x=True, |
|
showgrid_y=True |
|
) |
|
|
|
fig.update_layout( |
|
title="مخطط جانت للمشروع", |
|
xaxis_title="التاريخ", |
|
yaxis_title="الأنشطة", |
|
height=600 |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
def _handle_boq_tab(self): |
|
|
|
source_type = st.radio("اختر مصدر جدول الكميات:", |
|
["جدول كميات محفوظ", "رفع جدول كميات جديد"], |
|
key="boq_source") |
|
|
|
if source_type == "جدول كميات محفوظ": |
|
self._handle_saved_boq() |
|
else: |
|
self._handle_new_boq() |
|
|
|
def _handle_saved_boq(self): |
|
if not st.session_state.saved_pricing: |
|
st.warning("لا توجد جداول كميات محفوظة.") |
|
return |
|
|
|
projects = [(p['project_name'], i) for i, p in enumerate(st.session_state.saved_pricing)] |
|
selected_project_name = st.selectbox("اختر المشروع", [p[0] for p in projects]) |
|
project_index = next(p[1] for p in projects if p[0] == selected_project_name) |
|
project = st.session_state.saved_pricing[project_index] |
|
|
|
self._display_project_schedule(project) |
|
|
|
def _handle_new_boq(self): |
|
uploaded_file = st.file_uploader("قم برفع ملف Excel لجدول الكميات", type=['xlsx', 'xls']) |
|
if uploaded_file: |
|
try: |
|
df = pd.read_excel(uploaded_file) |
|
project = self._create_project_from_boq(df) |
|
self._display_project_schedule(project) |
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء قراءة الملف: {str(e)}") |
|
|
|
def _create_project_from_boq(self, df): |
|
return { |
|
'project_name': 'مشروع جديد', |
|
'items': [row.to_dict() for _, row in df.iterrows()], |
|
'total_price': df.get('الإجمالي', df.get('total_price', 0)).sum(), |
|
'project_duration': 180 |
|
} |
|
|
|
def _display_project_schedule(self, project): |
|
if not project: |
|
return |
|
|
|
st.subheader("تفاصيل المشروع") |
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
with col1: |
|
st.write(f"اسم المشروع: {project['project_name']}") |
|
st.write(f"رقم المشروع: {project.get('project_number', 'غير محدد')}") |
|
st.write(f"العميل: {project.get('client', 'غير محدد')}") |
|
|
|
with col2: |
|
st.write(f"إجمالي القيمة: {project['total_price']:,.2f} ريال") |
|
st.write(f"الموقع: {project.get('location', 'غير محدد')}") |
|
st.write(f"الحالة: {project.get('status', 'قيد التنفيذ')}") |
|
|
|
with col3: |
|
project['project_duration'] = st.number_input( |
|
"مدة المشروع (بالأيام)", |
|
min_value=30, |
|
max_value=1800, |
|
value=project.get('project_duration', 180) |
|
) |
|
st.write(f"تاريخ البدء: {project.get('start_date', 'غير محدد')}") |
|
st.write(f"تاريخ الانتهاء المتوقع: {project.get('end_date', 'غير محدد')}") |
|
|
|
|
|
if project.get('description'): |
|
st.write("وصف المشروع:") |
|
st.write(project['description']) |
|
|
|
self._generate_and_display_schedule(project) |
|
|
|
def _generate_and_display_schedule(self, project): |
|
if 'schedule_items' not in project: |
|
self._initialize_schedule_items(project) |
|
|
|
st.subheader("تحرير الجدول الزمني") |
|
edited_df = self._edit_schedule(project['schedule_items']) |
|
project['schedule_items'] = edited_df.to_dict('records') |
|
|
|
self._display_gantt_chart(project['schedule_items']) |
|
self._display_progress_report(project['schedule_items']) |
|
|
|
def _initialize_schedule_items(self, project): |
|
project['schedule_items'] = [] |
|
start_date = datetime.now() |
|
total_project_price = project.get('total_price', 0) |
|
|
|
if total_project_price == 0: |
|
total_project_price = sum(item.get('total_price', 0) for item in project['items']) |
|
project['total_price'] = total_project_price |
|
|
|
current_date = start_date |
|
for item in project['items']: |
|
item_price = item.get('total_price', 0) |
|
if total_project_price > 0: |
|
duration = max(1, int((item_price / total_project_price) * project['project_duration'])) |
|
else: |
|
duration = max(1, int(project['project_duration'] / len(project['items']))) |
|
|
|
end_date = current_date + timedelta(days=duration) |
|
|
|
|
|
materials_list = [] |
|
if 'materials' in item: |
|
for material in item['materials']: |
|
materials_list.append(f"{material['name']} - {material['quantity']} {material['unit']}") |
|
|
|
|
|
|
|
boq_item = next((x for x in project.get('items', []) |
|
if x.get('description', '').strip().lower() == item.get('description', '').strip().lower() |
|
or (x.get('code', '') == item.get('code', '') if item.get('code') else False)), None) |
|
|
|
if boq_item: |
|
task_description = f"البند: {boq_item.get('code', '')} - {boq_item.get('description', '')}\n" |
|
task_description += f"الكمية: {boq_item.get('quantity', 0)} {boq_item.get('unit', '')}\n" |
|
task_description += f"سعر الوحدة: {boq_item.get('unit_price', 0):,.2f} ريال\n" |
|
task_description += f"الإجمالي: {boq_item.get('total_price', 0):,.2f} ريال" |
|
else: |
|
task_description = f"{item.get('code', '')} - {item.get('description', '')}" |
|
|
|
schedule_item = { |
|
'Task': task_description, |
|
'Start': current_date.strftime('%Y-%m-%d'), |
|
'Finish': end_date.strftime('%Y-%m-%d'), |
|
'Duration': duration, |
|
'Progress': 0, |
|
'Resource': item.get('category', 'الموارد الأساسية'), |
|
'materials': ', '.join(materials_list) if materials_list else '' |
|
} |
|
project['schedule_items'].append(schedule_item) |
|
current_date = end_date |
|
|
|
def _edit_schedule(self, schedule_items): |
|
df = pd.DataFrame(schedule_items) |
|
|
|
|
|
df['Start'] = pd.to_datetime(df['Start']) |
|
df['Finish'] = pd.to_datetime(df['Finish']) |
|
|
|
|
|
if 'materials' not in df.columns: |
|
df['materials'] = '' |
|
|
|
|
|
df['Duration'] = (pd.to_datetime(df['Finish']) - pd.to_datetime(df['Start'])).dt.days |
|
|
|
edited_df = st.data_editor( |
|
df, |
|
column_config={ |
|
"Task": st.column_config.TextColumn( |
|
"البند", |
|
width="large", |
|
help="وصف البند" |
|
), |
|
"Start": st.column_config.DatetimeColumn( |
|
"تاريخ البداية", |
|
format="YYYY-MM-DD", |
|
step=86400 |
|
), |
|
"Finish": st.column_config.DatetimeColumn( |
|
"تاريخ النهاية", |
|
format="YYYY-MM-DD", |
|
step=86400 |
|
), |
|
"Duration": st.column_config.NumberColumn( |
|
"المدة (أيام)", |
|
min_value=1, |
|
format="%d" |
|
), |
|
"Progress": st.column_config.ProgressColumn( |
|
"نسبة الإنجاز", |
|
min_value=0, |
|
max_value=100, |
|
format="%d%%" |
|
), |
|
"Resource": st.column_config.TextColumn( |
|
"الموارد", |
|
width="medium" |
|
), |
|
"materials": st.column_config.TextColumn( |
|
"المواد المطلوبة", |
|
width="large" |
|
) |
|
}, |
|
num_rows="dynamic", |
|
use_container_width=True, |
|
hide_index=True, |
|
key="schedule_editor" |
|
) |
|
|
|
return edited_df |
|
|
|
def _display_gantt_chart(self, schedule_items): |
|
st.subheader("مخطط جانت") |
|
df = pd.DataFrame(schedule_items) |
|
|
|
|
|
df['Start'] = pd.to_datetime(df['Start']) |
|
df['Finish'] = pd.to_datetime(df['Finish']) |
|
|
|
|
|
df['Resource'] = df['Resource'].astype(str) |
|
|
|
|
|
df['ID'] = range(len(df)) |
|
|
|
|
|
colors = ['rgb(46, 137, 205)', 'rgb(82, 189, 97)', 'rgb(241, 196, 15)', 'rgb(231, 76, 60)'] |
|
|
|
fig = ff.create_gantt( |
|
df, |
|
colors=colors, |
|
index_col='ID', |
|
show_colorbar=False, |
|
group_tasks=True, |
|
showgrid_x=True, |
|
showgrid_y=True |
|
) |
|
|
|
fig.update_layout( |
|
title="مخطط جانت للمشروع", |
|
xaxis_title="التاريخ", |
|
yaxis_title="البنود", |
|
height=600 |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
def _display_progress_report(self, schedule_items): |
|
st.subheader("تقرير تقدم المشروع") |
|
df = pd.DataFrame(schedule_items) |
|
avg_progress = df['Progress'].mean() if 'Progress' in df.columns else 0 |
|
|
|
col1, col2, col3 = st.columns(3) |
|
with col1: |
|
st.metric("متوسط نسبة الإنجاز", f"{avg_progress:.1f}%") |
|
with col2: |
|
completed_tasks = len(df[df['Progress'] == 100]) if 'Progress' in df.columns else 0 |
|
st.metric("البنود المكتملة", f"{completed_tasks} من {len(df)}") |
|
with col3: |
|
not_started = len(df[df['Progress'] == 0]) if 'Progress' in df.columns else len(df) |
|
st.metric("البنود غير المبدوءة", not_started) |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
with col1: |
|
if st.button("💾 حفظ الجدول الزمني", key="save_schedule_btn"): |
|
self._save_schedule_to_db(df) |
|
st.success("تم حفظ الجدول الزمني بنجاح!") |
|
|
|
with col2: |
|
if st.button("📊 تصدير إلى Excel", key="export_schedule_btn"): |
|
self._export_schedule_to_excel(df) |
|
|
|
def _save_schedule_to_db(self, df): |
|
"""حفظ الجدول الزمني في قاعدة البيانات""" |
|
try: |
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
|
schedule_data = { |
|
'timestamp': timestamp, |
|
'project_name': st.session_state.current_project.get('name', 'مشروع جديد'), |
|
'schedule_items': df.to_dict('records') |
|
} |
|
|
|
if 'saved_schedules' not in st.session_state: |
|
st.session_state.saved_schedules = [] |
|
|
|
st.session_state.saved_schedules.append(schedule_data) |
|
|
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء حفظ الجدول الزمني: {str(e)}") |
|
|
|
def _export_schedule_to_excel(self, df): |
|
"""تصدير الجدول الزمني إلى ملف Excel""" |
|
try: |
|
export_path = "data/exports" |
|
os.makedirs(export_path, exist_ok=True) |
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
|
excel_file = f"{export_path}/schedule_{timestamp}.xlsx" |
|
|
|
|
|
with pd.ExcelWriter(excel_file, engine='openpyxl') as writer: |
|
df.to_excel(writer, index=False, sheet_name='الجدول الزمني') |
|
worksheet = writer.sheets['الجدول الزمني'] |
|
|
|
|
|
worksheet['A1'] = f"اسم المشروع: {st.session_state.current_project.get('name', 'مشروع جديد')}" |
|
worksheet['A2'] = f"تاريخ التصدير: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" |
|
|
|
|
|
with open(excel_file, 'rb') as f: |
|
excel_data = f.read() |
|
st.download_button( |
|
label="تحميل ملف Excel", |
|
data=excel_data, |
|
file_name=f"schedule_{timestamp}.xlsx", |
|
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" |
|
) |
|
st.success("تم تصدير الجدول الزمني بنجاح!") |
|
|
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء تصدير الجدول الزمني: {str(e)}") |
|
|