|
|
|
|
|
|
|
""" |
|
وحدة متتبع حالة المشروع المتحرك مع تصور التقدم |
|
""" |
|
|
|
import os |
|
import sys |
|
import json |
|
import time |
|
import datetime |
|
import streamlit as st |
|
import pandas as pd |
|
import numpy as np |
|
import plotly.express as px |
|
import plotly.graph_objects as go |
|
from datetime import datetime, timedelta |
|
import random |
|
import math |
|
|
|
|
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) |
|
|
|
|
|
from utils.helpers import format_time, get_user_info, create_directory_if_not_exists |
|
|
|
|
|
class ProjectStatusTracker: |
|
"""فئة متتبع حالة المشروع المتحرك""" |
|
|
|
def __init__(self, project_id=None, user_id=None): |
|
"""تهيئة متتبع حالة المشروع""" |
|
self.project_id = project_id or 1 |
|
self.user_id = user_id or 1 |
|
self.tracker_path = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'project_tracker') |
|
create_directory_if_not_exists(self.tracker_path) |
|
self.project_data_file = os.path.join(self.tracker_path, f'project_{self.project_id}_status.json') |
|
|
|
|
|
self.default_project_phases = [ |
|
{ |
|
"id": "planning", |
|
"name": "التخطيط", |
|
"description": "مرحلة التخطيط وإعداد الجدول الزمني", |
|
"order": 1, |
|
"progress": 100, |
|
"status": "completed", |
|
"start_date": (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d'), |
|
"end_date": (datetime.now() - timedelta(days=20)).strftime('%Y-%m-%d'), |
|
"actual_end_date": (datetime.now() - timedelta(days=18)).strftime('%Y-%m-%d'), |
|
"deliverables": ["خطة المشروع", "الجدول الزمني", "خطة الموارد"], |
|
"responsible": "فريق التخطيط", |
|
"notes": "تم الانتهاء من مرحلة التخطيط بنجاح قبل الموعد المحدد", |
|
"critical": True |
|
}, |
|
{ |
|
"id": "pricing", |
|
"name": "التسعير", |
|
"description": "تسعير المشروع وتحليل التكاليف", |
|
"order": 2, |
|
"progress": 100, |
|
"status": "completed", |
|
"start_date": (datetime.now() - timedelta(days=20)).strftime('%Y-%m-%d'), |
|
"end_date": (datetime.now() - timedelta(days=10)).strftime('%Y-%m-%d'), |
|
"actual_end_date": (datetime.now() - timedelta(days=8)).strftime('%Y-%m-%d'), |
|
"deliverables": ["جدول الكميات المسعر", "تحليل التكاليف", "خطة التدفق النقدي"], |
|
"responsible": "قسم التسعير", |
|
"notes": "تم تحقيق وفر في تكاليف المشروع بنسبة 5%", |
|
"critical": True |
|
}, |
|
{ |
|
"id": "bidding", |
|
"name": "تقديم العطاء", |
|
"description": "إعداد وتقديم وثائق العطاء", |
|
"order": 3, |
|
"progress": 100, |
|
"status": "completed", |
|
"start_date": (datetime.now() - timedelta(days=10)).strftime('%Y-%m-%d'), |
|
"end_date": (datetime.now() - timedelta(days=5)).strftime('%Y-%m-%d'), |
|
"actual_end_date": (datetime.now() - timedelta(days=5)).strftime('%Y-%m-%d'), |
|
"deliverables": ["وثائق العطاء", "خطاب التقديم", "الضمان البنكي الابتدائي"], |
|
"responsible": "مدير المشروع", |
|
"notes": "تم تقديم العطاء في الموعد المحدد", |
|
"critical": True |
|
}, |
|
{ |
|
"id": "evaluation", |
|
"name": "تقييم العطاء", |
|
"description": "مرحلة تقييم العطاء من قبل العميل", |
|
"order": 4, |
|
"progress": 75, |
|
"status": "in_progress", |
|
"start_date": (datetime.now() - timedelta(days=5)).strftime('%Y-%m-%d'), |
|
"end_date": (datetime.now() + timedelta(days=5)).strftime('%Y-%m-%d'), |
|
"actual_end_date": None, |
|
"deliverables": ["الرد على استفسارات العميل", "العرض التقديمي", "تقديم المستندات الإضافية"], |
|
"responsible": "العميل / مدير المشروع", |
|
"notes": "مرحلة التقييم جارية، تم الرد على جميع استفسارات العميل", |
|
"critical": True |
|
}, |
|
{ |
|
"id": "awarding", |
|
"name": "ترسية العطاء", |
|
"description": "مرحلة ترسية العطاء وتوقيع العقد", |
|
"order": 5, |
|
"progress": 0, |
|
"status": "not_started", |
|
"start_date": (datetime.now() + timedelta(days=5)).strftime('%Y-%m-%d'), |
|
"end_date": (datetime.now() + timedelta(days=15)).strftime('%Y-%m-%d'), |
|
"actual_end_date": None, |
|
"deliverables": ["خطاب الترسية", "العقد الموقع", "الضمان البنكي النهائي"], |
|
"responsible": "الإدارة القانونية / مدير المشروع", |
|
"notes": "ننتظر نتيجة الترسية", |
|
"critical": True |
|
}, |
|
{ |
|
"id": "mobilization", |
|
"name": "التجهيز", |
|
"description": "تجهيز الموقع وتوفير الموارد", |
|
"order": 6, |
|
"progress": 0, |
|
"status": "not_started", |
|
"start_date": (datetime.now() + timedelta(days=15)).strftime('%Y-%m-%d'), |
|
"end_date": (datetime.now() + timedelta(days=30)).strftime('%Y-%m-%d'), |
|
"actual_end_date": None, |
|
"deliverables": ["تقرير التجهيز", "قائمة الموارد", "خطة التنفيذ التفصيلية"], |
|
"responsible": "قسم العمليات", |
|
"notes": "التجهيز سيبدأ بعد توقيع العقد", |
|
"critical": False |
|
}, |
|
{ |
|
"id": "execution", |
|
"name": "التنفيذ", |
|
"description": "تنفيذ أعمال المشروع", |
|
"order": 7, |
|
"progress": 0, |
|
"status": "not_started", |
|
"start_date": (datetime.now() + timedelta(days=30)).strftime('%Y-%m-%d'), |
|
"end_date": (datetime.now() + timedelta(days=180)).strftime('%Y-%m-%d'), |
|
"actual_end_date": None, |
|
"deliverables": ["تقارير التقدم الدورية", "محاضر الاجتماعات", "الفواتير"], |
|
"responsible": "فريق التنفيذ", |
|
"notes": "التنفيذ سيستمر لمدة 6 أشهر", |
|
"critical": True |
|
}, |
|
{ |
|
"id": "handover", |
|
"name": "التسليم", |
|
"description": "تسليم المشروع للعميل", |
|
"order": 8, |
|
"progress": 0, |
|
"status": "not_started", |
|
"start_date": (datetime.now() + timedelta(days=180)).strftime('%Y-%m-%d'), |
|
"end_date": (datetime.now() + timedelta(days=195)).strftime('%Y-%m-%d'), |
|
"actual_end_date": None, |
|
"deliverables": ["محضر الاستلام", "وثائق الضمان", "دليل التشغيل والصيانة"], |
|
"responsible": "مدير المشروع / العميل", |
|
"notes": "التسليم يشمل فترة الاختبار والتدريب", |
|
"critical": True |
|
}, |
|
] |
|
|
|
|
|
self.load_project_data() |
|
|
|
|
|
self.kpi_data_file = os.path.join(self.tracker_path, f'project_{self.project_id}_kpis.json') |
|
self.load_kpi_data() |
|
|
|
def load_project_data(self): |
|
"""تحميل بيانات حالة المشروع""" |
|
try: |
|
if os.path.exists(self.project_data_file): |
|
with open(self.project_data_file, 'r', encoding='utf-8') as f: |
|
self.project_data = json.load(f) |
|
else: |
|
|
|
self.project_data = { |
|
'project_id': self.project_id, |
|
'project_name': "مشروع إنشاء مبنى إداري", |
|
'project_code': "PC-2025-001", |
|
'client': "وزارة الإسكان", |
|
'location': "الرياض، المملكة العربية السعودية", |
|
'start_date': (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d'), |
|
'end_date': (datetime.now() + timedelta(days=200)).strftime('%Y-%m-%d'), |
|
'budget': 10000000, |
|
'duration': 230, |
|
'elapsed_days': 30, |
|
'overall_progress': 25, |
|
'status': "في التقدم", |
|
'phases': self.default_project_phases, |
|
'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
} |
|
self.save_project_data() |
|
except Exception as e: |
|
st.error(f"خطأ في تحميل بيانات المشروع: {e}") |
|
self.project_data = { |
|
'project_id': self.project_id, |
|
'project_name': "مشروع إنشاء مبنى إداري", |
|
'project_code': "PC-2025-001", |
|
'client': "وزارة الإسكان", |
|
'location': "الرياض، المملكة العربية السعودية", |
|
'start_date': (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d'), |
|
'end_date': (datetime.now() + timedelta(days=200)).strftime('%Y-%m-%d'), |
|
'budget': 10000000, |
|
'duration': 230, |
|
'elapsed_days': 30, |
|
'overall_progress': 25, |
|
'status': "في التقدم", |
|
'phases': self.default_project_phases, |
|
'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
} |
|
|
|
def save_project_data(self): |
|
"""حفظ بيانات حالة المشروع""" |
|
try: |
|
with open(self.project_data_file, 'w', encoding='utf-8') as f: |
|
json.dump(self.project_data, f, ensure_ascii=False, indent=2) |
|
except Exception as e: |
|
st.error(f"خطأ في حفظ بيانات المشروع: {e}") |
|
|
|
def load_kpi_data(self): |
|
"""تحميل بيانات مؤشرات الأداء الرئيسية""" |
|
try: |
|
if os.path.exists(self.kpi_data_file): |
|
with open(self.kpi_data_file, 'r', encoding='utf-8') as f: |
|
self.kpi_data = json.load(f) |
|
else: |
|
|
|
self.kpi_data = { |
|
'spi': 1.05, |
|
'cpi': 0.98, |
|
'quality_score': 92, |
|
'safety_incidents': 0, |
|
'resource_utilization': 85, |
|
'risk_score': 15, |
|
'customer_satisfaction': 90, |
|
'environmental_compliance': 95, |
|
'trends': { |
|
'spi': [0.95, 0.98, 1.02, 1.05], |
|
'cpi': [1.02, 1.00, 0.99, 0.98], |
|
'quality_score': [85, 88, 90, 92], |
|
'risk_score': [25, 22, 18, 15], |
|
'dates': [ |
|
(datetime.now() - timedelta(days=21)).strftime('%Y-%m-%d'), |
|
(datetime.now() - timedelta(days=14)).strftime('%Y-%m-%d'), |
|
(datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'), |
|
datetime.now().strftime('%Y-%m-%d') |
|
] |
|
}, |
|
'issues': [ |
|
{ |
|
'id': 1, |
|
'description': "تأخر في توريد المواد", |
|
'severity': "متوسط", |
|
'status': "قيد المعالجة", |
|
'created_date': (datetime.now() - timedelta(days=10)).strftime('%Y-%m-%d'), |
|
'responsible': "قسم المشتريات", |
|
'resolution': "التنسيق مع المورد البديل" |
|
}, |
|
{ |
|
'id': 2, |
|
'description': "نقص في فريق العمل", |
|
'severity': "منخفض", |
|
'status': "تم الحل", |
|
'created_date': (datetime.now() - timedelta(days=15)).strftime('%Y-%m-%d'), |
|
'responsible': "قسم الموارد البشرية", |
|
'resolution': "تم توظيف فريق إضافي" |
|
} |
|
], |
|
'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
} |
|
self.save_kpi_data() |
|
except Exception as e: |
|
st.error(f"خطأ في تحميل بيانات مؤشرات الأداء: {e}") |
|
self.kpi_data = { |
|
'spi': 1.05, |
|
'cpi': 0.98, |
|
'quality_score': 92, |
|
'safety_incidents': 0, |
|
'resource_utilization': 85, |
|
'risk_score': 15, |
|
'customer_satisfaction': 90, |
|
'environmental_compliance': 95, |
|
'trends': { |
|
'spi': [0.95, 0.98, 1.02, 1.05], |
|
'cpi': [1.02, 1.00, 0.99, 0.98], |
|
'quality_score': [85, 88, 90, 92], |
|
'risk_score': [25, 22, 18, 15], |
|
'dates': [ |
|
(datetime.now() - timedelta(days=21)).strftime('%Y-%m-%d'), |
|
(datetime.now() - timedelta(days=14)).strftime('%Y-%m-%d'), |
|
(datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'), |
|
datetime.now().strftime('%Y-%m-%d') |
|
] |
|
}, |
|
'issues': [ |
|
{ |
|
'id': 1, |
|
'description': "تأخر في توريد المواد", |
|
'severity': "متوسط", |
|
'status': "قيد المعالجة", |
|
'created_date': (datetime.now() - timedelta(days=10)).strftime('%Y-%m-%d'), |
|
'responsible': "قسم المشتريات", |
|
'resolution': "التنسيق مع المورد البديل" |
|
}, |
|
{ |
|
'id': 2, |
|
'description': "نقص في فريق العمل", |
|
'severity': "منخفض", |
|
'status': "تم الحل", |
|
'created_date': (datetime.now() - timedelta(days=15)).strftime('%Y-%m-%d'), |
|
'responsible': "قسم الموارد البشرية", |
|
'resolution': "تم توظيف فريق إضافي" |
|
} |
|
], |
|
'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
} |
|
|
|
def save_kpi_data(self): |
|
"""حفظ بيانات مؤشرات الأداء الرئيسية""" |
|
try: |
|
with open(self.kpi_data_file, 'w', encoding='utf-8') as f: |
|
json.dump(self.kpi_data, f, ensure_ascii=False, indent=2) |
|
except Exception as e: |
|
st.error(f"خطأ في حفظ بيانات مؤشرات الأداء: {e}") |
|
|
|
def update_project_status(self, phase_id, progress, status, actual_end_date=None, notes=None): |
|
"""تحديث حالة مرحلة في المشروع""" |
|
|
|
phase = next((p for p in self.project_data['phases'] if p['id'] == phase_id), None) |
|
if not phase: |
|
return False |
|
|
|
|
|
phase['progress'] = progress |
|
phase['status'] = status |
|
if actual_end_date: |
|
phase['actual_end_date'] = actual_end_date |
|
if notes: |
|
phase['notes'] = notes |
|
|
|
|
|
self._update_overall_progress() |
|
|
|
|
|
self.project_data['last_updated'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
self.save_project_data() |
|
|
|
return True |
|
|
|
def _update_overall_progress(self): |
|
"""تحديث نسبة التقدم الكلية للمشروع""" |
|
total_weight = len(self.project_data['phases']) |
|
total_progress = sum(phase['progress'] for phase in self.project_data['phases']) |
|
|
|
self.project_data['overall_progress'] = round(total_progress / total_weight) |
|
|
|
|
|
start_date = datetime.strptime(self.project_data['start_date'], '%Y-%m-%d') |
|
self.project_data['elapsed_days'] = (datetime.now() - start_date).days |
|
|
|
def add_project_issue(self, description, severity, responsible, resolution=None): |
|
"""إضافة مشكلة جديدة للمشروع""" |
|
|
|
new_id = max([issue['id'] for issue in self.kpi_data['issues']], default=0) + 1 |
|
|
|
|
|
new_issue = { |
|
'id': new_id, |
|
'description': description, |
|
'severity': severity, |
|
'status': "قيد المعالجة", |
|
'created_date': datetime.now().strftime('%Y-%m-%d'), |
|
'responsible': responsible, |
|
'resolution': resolution or "" |
|
} |
|
|
|
self.kpi_data['issues'].append(new_issue) |
|
self.kpi_data['last_updated'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
self.save_kpi_data() |
|
|
|
return new_issue |
|
|
|
def update_issue_status(self, issue_id, status, resolution=None): |
|
"""تحديث حالة مشكلة في المشروع""" |
|
|
|
issue = next((i for i in self.kpi_data['issues'] if i['id'] == issue_id), None) |
|
if not issue: |
|
return False |
|
|
|
|
|
issue['status'] = status |
|
if resolution: |
|
issue['resolution'] = resolution |
|
|
|
|
|
self.kpi_data['last_updated'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
self.save_kpi_data() |
|
|
|
return True |
|
|
|
def update_kpi_values(self, kpi_updates): |
|
"""تحديث قيم مؤشرات الأداء الرئيسية""" |
|
|
|
for key, value in kpi_updates.items(): |
|
if key in self.kpi_data and key != 'trends' and key != 'issues': |
|
self.kpi_data[key] = value |
|
|
|
|
|
for key, value in kpi_updates.items(): |
|
if key in self.kpi_data and key != 'trends' and key != 'issues' and key in self.kpi_data['trends']: |
|
|
|
self.kpi_data['trends'][key].append(value) |
|
if len(self.kpi_data['trends'][key]) > 5: |
|
self.kpi_data['trends'][key].pop(0) |
|
|
|
|
|
today = datetime.now().strftime('%Y-%m-%d') |
|
if today not in self.kpi_data['trends']['dates']: |
|
self.kpi_data['trends']['dates'].append(today) |
|
if len(self.kpi_data['trends']['dates']) > 5: |
|
self.kpi_data['trends']['dates'].pop(0) |
|
|
|
|
|
self.kpi_data['last_updated'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
self.save_kpi_data() |
|
|
|
return True |
|
|
|
def render_project_status_dashboard(self): |
|
"""عرض لوحة تحكم حالة المشروع""" |
|
st.markdown("<h2 class='module-title'>متتبع حالة المشروع المتحرك</h2>", unsafe_allow_html=True) |
|
|
|
st.markdown(""" |
|
<div class="module-description"> |
|
متتبع حالة المشروع المتحرك يوفر عرضاً تفاعلياً ومرئياً لحالة المشروع ومراحله المختلفة، |
|
مع إمكانية متابعة مؤشرات الأداء الرئيسية وتتبع التقدم بشكل مباشر. |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
self._render_project_info() |
|
|
|
|
|
self._render_kpi_cards() |
|
|
|
|
|
self._render_project_progress() |
|
|
|
|
|
self._render_phases_timeline() |
|
|
|
|
|
self._render_kpi_trends() |
|
|
|
|
|
self._render_issues_table() |
|
|
|
|
|
if st.checkbox("تحديث حالة المشروع"): |
|
self._render_update_panel() |
|
|
|
def _render_project_info(self): |
|
"""عرض معلومات المشروع""" |
|
st.markdown("<h3 class='dashboard-section-title'>معلومات المشروع</h3>", unsafe_allow_html=True) |
|
|
|
col1, col2, col3 = st.columns([2, 1, 1]) |
|
|
|
with col1: |
|
st.markdown(f""" |
|
<div class="project-info-card"> |
|
<div class="project-name">{self.project_data['project_name']}</div> |
|
<div class="project-code">رمز المشروع: {self.project_data['project_code']}</div> |
|
<div class="project-client">العميل: {self.project_data['client']}</div> |
|
<div class="project-location">الموقع: {self.project_data['location']}</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
with col2: |
|
|
|
end_date = datetime.strptime(self.project_data['end_date'], '%Y-%m-%d') |
|
remaining_days = (end_date - datetime.now()).days |
|
|
|
st.markdown(f""" |
|
<div class="date-info-card"> |
|
<div class="date-title">تاريخ البدء</div> |
|
<div class="date-value">{self.project_data['start_date']}</div> |
|
<div class="date-title">تاريخ الانتهاء</div> |
|
<div class="date-value">{self.project_data['end_date']}</div> |
|
<div class="date-title">الأيام المتبقية</div> |
|
<div class="date-value remaining">{remaining_days} يوم</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
with col3: |
|
|
|
progress = self.project_data['overall_progress'] |
|
progress_color = "green" if progress >= 80 else "orange" if progress >= 50 else "red" |
|
|
|
st.markdown(f""" |
|
<div class="progress-info-card"> |
|
<div class="progress-title">التقدم الكلي</div> |
|
<div class="progress-value" style="color: {progress_color};">{progress}%</div> |
|
<div class="progress-title">الميزانية</div> |
|
<div class="progress-value">{self.project_data['budget']:,} ريال</div> |
|
<div class="progress-title">آخر تحديث</div> |
|
<div class="progress-value update-time">{self.project_data['last_updated']}</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
def _render_kpi_cards(self): |
|
"""عرض بطاقات مؤشرات الأداء الرئيسية""" |
|
st.markdown("<h3 class='dashboard-section-title'>مؤشرات الأداء الرئيسية</h3>", unsafe_allow_html=True) |
|
|
|
col1, col2, col3, col4 = st.columns(4) |
|
|
|
with col1: |
|
|
|
spi = self.kpi_data['spi'] |
|
spi_color = "green" if spi >= 1.0 else "red" |
|
spi_icon = "⬆️" if spi >= 1.0 else "⬇️" |
|
spi_trend = round((spi - self.kpi_data['trends']['spi'][0]) * 100, 1) |
|
|
|
st.markdown(f""" |
|
<div class="kpi-card"> |
|
<div class="kpi-title">مؤشر أداء الجدول الزمني</div> |
|
<div class="kpi-value" style="color: {spi_color};">{spi}</div> |
|
<div class="kpi-trend"> |
|
{spi_icon} {spi_trend}% |
|
</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
quality = self.kpi_data['quality_score'] |
|
quality_color = "green" if quality >= 90 else "orange" if quality >= 80 else "red" |
|
quality_icon = "⬆️" if quality >= 90 else "➡️" if quality >= 80 else "⬇️" |
|
|
|
st.markdown(f""" |
|
<div class="kpi-card"> |
|
<div class="kpi-title">درجة الجودة</div> |
|
<div class="kpi-value" style="color: {quality_color};">{quality}%</div> |
|
<div class="kpi-description"> |
|
{quality_icon} ممتاز |
|
</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
with col2: |
|
|
|
cpi = self.kpi_data['cpi'] |
|
cpi_color = "green" if cpi >= 1.0 else "red" |
|
cpi_icon = "⬆️" if cpi >= 1.0 else "⬇️" |
|
cpi_trend = round((cpi - self.kpi_data['trends']['cpi'][0]) * 100, 1) |
|
|
|
st.markdown(f""" |
|
<div class="kpi-card"> |
|
<div class="kpi-title">مؤشر أداء التكلفة</div> |
|
<div class="kpi-value" style="color: {cpi_color};">{cpi}</div> |
|
<div class="kpi-trend"> |
|
{cpi_icon} {cpi_trend}% |
|
</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
risk = self.kpi_data['risk_score'] |
|
risk_color = "green" if risk < 20 else "orange" if risk < 40 else "red" |
|
risk_icon = "⬇️" if risk < 20 else "➡️" if risk < 40 else "⬆️" |
|
|
|
st.markdown(f""" |
|
<div class="kpi-card"> |
|
<div class="kpi-title">درجة المخاطر</div> |
|
<div class="kpi-value" style="color: {risk_color};">{risk}%</div> |
|
<div class="kpi-description"> |
|
{risk_icon} منخفضة |
|
</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
with col3: |
|
|
|
resources = self.kpi_data['resource_utilization'] |
|
resources_color = "green" if resources >= 85 else "orange" if resources >= 70 else "red" |
|
|
|
st.markdown(f""" |
|
<div class="kpi-card"> |
|
<div class="kpi-title">استغلال الموارد</div> |
|
<div class="kpi-value" style="color: {resources_color};">{resources}%</div> |
|
<div class="kpi-description"> |
|
استغلال فعال للموارد |
|
</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
safety = self.kpi_data['safety_incidents'] |
|
safety_color = "green" if safety == 0 else "orange" if safety < 3 else "red" |
|
|
|
st.markdown(f""" |
|
<div class="kpi-card"> |
|
<div class="kpi-title">حوادث السلامة</div> |
|
<div class="kpi-value" style="color: {safety_color};">{safety}</div> |
|
<div class="kpi-description"> |
|
سجل سلامة ممتاز |
|
</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
with col4: |
|
|
|
satisfaction = self.kpi_data['customer_satisfaction'] |
|
satisfaction_color = "green" if satisfaction >= 90 else "orange" if satisfaction >= 75 else "red" |
|
|
|
st.markdown(f""" |
|
<div class="kpi-card"> |
|
<div class="kpi-title">رضا العميل</div> |
|
<div class="kpi-value" style="color: {satisfaction_color};">{satisfaction}%</div> |
|
<div class="kpi-description"> |
|
مستوى رضا ممتاز |
|
</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
compliance = self.kpi_data['environmental_compliance'] |
|
compliance_color = "green" if compliance >= 90 else "orange" if compliance >= 75 else "red" |
|
|
|
st.markdown(f""" |
|
<div class="kpi-card"> |
|
<div class="kpi-title">الامتثال البيئي</div> |
|
<div class="kpi-value" style="color: {compliance_color};">{compliance}%</div> |
|
<div class="kpi-description"> |
|
امتثال كامل للمعايير |
|
</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
def _render_project_progress(self): |
|
"""عرض تقدم المشروع""" |
|
st.markdown("<h3 class='dashboard-section-title'>تقدم المشروع</h3>", unsafe_allow_html=True) |
|
|
|
|
|
st.markdown("<div class='progress-section-title'>التقدم الكلي للمشروع</div>", unsafe_allow_html=True) |
|
|
|
|
|
start_date = datetime.strptime(self.project_data['start_date'], '%Y-%m-%d') |
|
end_date = datetime.strptime(self.project_data['end_date'], '%Y-%m-%d') |
|
total_days = (end_date - start_date).days |
|
elapsed_days = self.project_data['elapsed_days'] |
|
time_percentage = min(100, max(0, round((elapsed_days / total_days) * 100))) |
|
|
|
|
|
progress_percentage = self.project_data['overall_progress'] |
|
|
|
|
|
progress_status = "ahead" if progress_percentage > time_percentage else "behind" if progress_percentage < time_percentage else "on-track" |
|
progress_color = "#00b894" if progress_status == "ahead" else "#d63031" if progress_status == "behind" else "#fdcb6e" |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
x=[start_date, end_date], |
|
y=[0, 100], |
|
mode='lines', |
|
name='التقدم المخطط', |
|
line=dict(color='rgba(0, 0, 0, 0.3)', width=2, dash='dash'), |
|
hoverinfo='text', |
|
hovertext=['بداية المشروع', 'نهاية المشروع'] |
|
)) |
|
|
|
|
|
current_date = start_date + timedelta(days=elapsed_days) |
|
fig.add_trace(go.Scatter( |
|
x=[current_date], |
|
y=[time_percentage], |
|
mode='markers', |
|
name='الوقت المنقضي', |
|
marker=dict(color='#2d3436', size=12, symbol='diamond'), |
|
hoverinfo='text', |
|
hovertext=[f'الوقت المنقضي: {time_percentage}%'] |
|
)) |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
x=[current_date], |
|
y=[progress_percentage], |
|
mode='markers', |
|
name='التقدم الفعلي', |
|
marker=dict(color=progress_color, size=16, symbol='circle'), |
|
hoverinfo='text', |
|
hovertext=[f'التقدم الفعلي: {progress_percentage}%'] |
|
)) |
|
|
|
|
|
fig.update_layout( |
|
title=f"التقدم: {progress_percentage}% - الوقت المنقضي: {time_percentage}%", |
|
xaxis_title="التاريخ", |
|
yaxis_title="نسبة التقدم (%)", |
|
legend=dict( |
|
x=0, |
|
y=1.1, |
|
orientation='h' |
|
), |
|
height=300, |
|
margin=dict(l=20, r=20, t=50, b=20), |
|
xaxis=dict(showgrid=False), |
|
yaxis=dict(range=[0, 100]), |
|
hovermode='closest', |
|
plot_bgcolor='rgba(255, 255, 255, 0.8)' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
status_message = "المشروع متقدم عن الجدول الزمني" if progress_status == "ahead" else "المشروع متأخر عن الجدول الزمني" if progress_status == "behind" else "المشروع في الموعد المحدد" |
|
status_color = "green" if progress_status == "ahead" else "red" if progress_status == "behind" else "orange" |
|
|
|
st.markdown(f""" |
|
<div class="progress-status" style="color: {status_color};"> |
|
{status_message} (الفرق: {abs(progress_percentage - time_percentage)}%) |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
st.markdown("<div class='progress-section-title'>تقدم مراحل المشروع</div>", unsafe_allow_html=True) |
|
|
|
|
|
sorted_phases = sorted(self.project_data['phases'], key=lambda x: x['order']) |
|
|
|
|
|
phase_labels = [phase['name'] for phase in sorted_phases] |
|
phase_progress = [phase['progress'] for phase in sorted_phases] |
|
phase_colors = [ |
|
"#00b894" if phase['status'] == "completed" else |
|
"#fdcb6e" if phase['status'] == "in_progress" else |
|
"#d63031" if phase['status'] == "delayed" else |
|
"#a29bfe" if phase['status'] == "on_hold" else |
|
"#dfe6e9" |
|
for phase in sorted_phases |
|
] |
|
|
|
|
|
fig = go.Figure() |
|
|
|
fig.add_trace(go.Bar( |
|
y=phase_labels, |
|
x=phase_progress, |
|
orientation='h', |
|
marker=dict( |
|
color=phase_colors |
|
), |
|
hoverinfo='text', |
|
hovertext=[f"{phase['name']}: {phase['progress']}% - {self._get_status_text(phase['status'])}" for phase in sorted_phases] |
|
)) |
|
|
|
fig.update_layout( |
|
title="تقدم مراحل المشروع", |
|
xaxis_title="نسبة التقدم (%)", |
|
yaxis=dict( |
|
categoryorder='array', |
|
categoryarray=phase_labels |
|
), |
|
xaxis=dict(range=[0, 100]), |
|
height=400, |
|
margin=dict(l=20, r=20, t=50, b=20), |
|
plot_bgcolor='rgba(255, 255, 255, 0.8)' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
col1, col2, col3, col4, col5 = st.columns(5) |
|
with col1: |
|
st.markdown('<div class="legend-item"><span class="legend-color" style="background-color: #00b894;"></span> مكتمل</div>', unsafe_allow_html=True) |
|
with col2: |
|
st.markdown('<div class="legend-item"><span class="legend-color" style="background-color: #fdcb6e;"></span> قيد التنفيذ</div>', unsafe_allow_html=True) |
|
with col3: |
|
st.markdown('<div class="legend-item"><span class="legend-color" style="background-color: #d63031;"></span> متأخر</div>', unsafe_allow_html=True) |
|
with col4: |
|
st.markdown('<div class="legend-item"><span class="legend-color" style="background-color: #a29bfe;"></span> متوقف</div>', unsafe_allow_html=True) |
|
with col5: |
|
st.markdown('<div class="legend-item"><span class="legend-color" style="background-color: #dfe6e9;"></span> لم يبدأ</div>', unsafe_allow_html=True) |
|
|
|
def _render_phases_timeline(self): |
|
"""عرض الجدول الزمني للمراحل""" |
|
st.markdown("<h3 class='dashboard-section-title'>الجدول الزمني للمراحل</h3>", unsafe_allow_html=True) |
|
|
|
|
|
sorted_phases = sorted(self.project_data['phases'], key=lambda x: x['order']) |
|
|
|
|
|
for phase in sorted_phases: |
|
phase['start_date_obj'] = datetime.strptime(phase['start_date'], '%Y-%m-%d') |
|
phase['end_date_obj'] = datetime.strptime(phase['end_date'], '%Y-%m-%d') |
|
if phase['actual_end_date']: |
|
phase['actual_end_date_obj'] = datetime.strptime(phase['actual_end_date'], '%Y-%m-%d') |
|
else: |
|
phase['actual_end_date_obj'] = None |
|
|
|
|
|
min_date = min(phase['start_date_obj'] for phase in sorted_phases) |
|
max_date = max(phase['end_date_obj'] for phase in sorted_phases) |
|
|
|
|
|
date_range = (max_date - min_date).days |
|
min_date = min_date - timedelta(days=date_range * 0.05) |
|
max_date = max_date + timedelta(days=date_range * 0.05) |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
current_date = datetime.now() |
|
fig.add_shape( |
|
type="line", |
|
x0=current_date, |
|
y0=-0.5, |
|
x1=current_date, |
|
y1=len(sorted_phases) - 0.5, |
|
line=dict( |
|
color="red", |
|
width=2, |
|
dash="dash", |
|
), |
|
name="اليوم" |
|
) |
|
|
|
|
|
for i, phase in enumerate(sorted_phases): |
|
|
|
fig.add_trace(go.Bar( |
|
x=[phase['end_date_obj'] - phase['start_date_obj']], |
|
y=[phase['name']], |
|
orientation='h', |
|
base=[phase['start_date_obj']], |
|
marker=dict( |
|
color='rgba(171, 214, 255, 0.8)', |
|
line=dict(color='rgba(50, 136, 229, 1.0)', width=1) |
|
), |
|
name=phase['name'] + " (مخطط)", |
|
hoverinfo='text', |
|
hovertext=[f"{phase['name']} (مخطط)<br>البداية: {phase['start_date']}<br>النهاية: {phase['end_date']}<br>المدة: {(phase['end_date_obj'] - phase['start_date_obj']).days} يوم"] |
|
)) |
|
|
|
|
|
if phase['status'] == "completed": |
|
color = 'rgba(0, 184, 148, 0.8)' |
|
line_color = 'rgba(0, 150, 136, 1.0)' |
|
elif phase['status'] == "in_progress": |
|
color = 'rgba(253, 203, 110, 0.8)' |
|
line_color = 'rgba(225, 177, 44, 1.0)' |
|
elif phase['status'] == "delayed": |
|
color = 'rgba(214, 48, 49, 0.8)' |
|
line_color = 'rgba(192, 57, 43, 1.0)' |
|
elif phase['status'] == "on_hold": |
|
color = 'rgba(162, 155, 254, 0.8)' |
|
line_color = 'rgba(108, 92, 231, 1.0)' |
|
else: |
|
continue |
|
|
|
|
|
if phase['status'] in ["completed", "in_progress", "delayed", "on_hold"]: |
|
if phase['status'] == "completed" and phase['actual_end_date_obj']: |
|
|
|
duration = phase['actual_end_date_obj'] - phase['start_date_obj'] |
|
fig.add_trace(go.Bar( |
|
x=[duration], |
|
y=[phase['name']], |
|
orientation='h', |
|
base=[phase['start_date_obj']], |
|
marker=dict( |
|
color=color, |
|
line=dict(color=line_color, width=1) |
|
), |
|
name=phase['name'] + " (فعلي)", |
|
hoverinfo='text', |
|
hovertext=[f"{phase['name']} (فعلي)<br>البداية: {phase['start_date']}<br>النهاية الفعلية: {phase['actual_end_date']}<br>المدة: {duration.days} يوم<br>التقدم: {phase['progress']}%"] |
|
)) |
|
else: |
|
|
|
if current_date > phase['start_date_obj']: |
|
duration = current_date - phase['start_date_obj'] |
|
fig.add_trace(go.Bar( |
|
x=[duration], |
|
y=[phase['name']], |
|
orientation='h', |
|
base=[phase['start_date_obj']], |
|
marker=dict( |
|
color=color, |
|
line=dict(color=line_color, width=1) |
|
), |
|
name=phase['name'] + " (فعلي)", |
|
hoverinfo='text', |
|
hovertext=[f"{phase['name']} (فعلي)<br>البداية: {phase['start_date']}<br>حتى تاريخه: {current_date.strftime('%Y-%m-%d')}<br>المدة: {duration.days} يوم<br>التقدم: {phase['progress']}%"] |
|
)) |
|
|
|
|
|
fig.update_layout( |
|
title="الجدول الزمني للمراحل", |
|
xaxis=dict(type='date', range=[min_date, max_date], title='التاريخ'), |
|
yaxis=dict(title=None, autorange="reversed"), |
|
height=400, |
|
margin=dict(l=20, r=20, t=50, b=20), |
|
barmode='overlay', |
|
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1), |
|
plot_bgcolor='rgba(255, 255, 255, 0.8)' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
with st.expander("عرض تفاصيل المراحل"): |
|
for phase in sorted_phases: |
|
status_class = self._get_status_class(phase['status']) |
|
status_text = self._get_status_text(phase['status']) |
|
critical_badge = '<span class="critical-badge">مسار حرج</span>' if phase.get('critical', False) else '' |
|
|
|
st.markdown(f""" |
|
<div class="phase-details-card {status_class}"> |
|
<div class="phase-details-header"> |
|
<div class="phase-details-name">{phase['name']} {critical_badge}</div> |
|
<div class="phase-details-status">{status_text}</div> |
|
</div> |
|
<div class="phase-details-progress"> |
|
<div class="phase-details-progress-bar"> |
|
<div class="phase-details-progress-fill" style="width: {phase['progress']}%;"></div> |
|
</div> |
|
<div class="phase-details-progress-text">{phase['progress']}%</div> |
|
</div> |
|
<div class="phase-details-info"> |
|
<div class="phase-details-dates"> |
|
<div>البداية: {phase['start_date']}</div> |
|
<div>النهاية (مخطط): {phase['end_date']}</div> |
|
{f'<div>النهاية (فعلي): {phase["actual_end_date"]}</div>' if phase['actual_end_date'] else ''} |
|
</div> |
|
<div class="phase-details-responsible"> |
|
<div>المسؤول: {phase['responsible']}</div> |
|
<div>التسليمات: {', '.join(phase['deliverables'])}</div> |
|
</div> |
|
</div> |
|
<div class="phase-details-notes">{phase['notes']}</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
def _render_kpi_trends(self): |
|
"""عرض اتجاهات مؤشرات الأداء الرئيسية""" |
|
st.markdown("<h3 class='dashboard-section-title'>اتجاهات مؤشرات الأداء</h3>", unsafe_allow_html=True) |
|
|
|
tabs = st.tabs(["أداء الجدول الزمني والتكلفة", "الجودة والمخاطر"]) |
|
|
|
with tabs[0]: |
|
|
|
dates = [datetime.strptime(date, '%Y-%m-%d') for date in self.kpi_data['trends']['dates']] |
|
spi_values = self.kpi_data['trends']['spi'] |
|
cpi_values = self.kpi_data['trends']['cpi'] |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
x=dates, |
|
y=spi_values, |
|
mode='lines+markers', |
|
name='مؤشر أداء الجدول الزمني (SPI)', |
|
line=dict(color='#00b894', width=2), |
|
marker=dict(size=8) |
|
)) |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
x=dates, |
|
y=cpi_values, |
|
mode='lines+markers', |
|
name='مؤشر أداء التكلفة (CPI)', |
|
line=dict(color='#0984e3', width=2), |
|
marker=dict(size=8) |
|
)) |
|
|
|
|
|
fig.add_shape( |
|
type="line", |
|
x0=min(dates), |
|
y0=1.0, |
|
x1=max(dates), |
|
y1=1.0, |
|
line=dict( |
|
color="#636e72", |
|
width=1, |
|
dash="dash", |
|
) |
|
) |
|
|
|
|
|
fig.update_layout( |
|
title="اتجاهات مؤشرات أداء الجدول الزمني والتكلفة", |
|
xaxis_title="التاريخ", |
|
yaxis_title="القيمة", |
|
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1), |
|
height=350, |
|
margin=dict(l=20, r=20, t=50, b=20), |
|
yaxis=dict(range=[0.8, 1.2], zeroline=False), |
|
hovermode='x unified', |
|
plot_bgcolor='rgba(255, 255, 255, 0.8)' |
|
) |
|
|
|
fig.update_yaxes(tickformat=".2f") |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
with col1: |
|
st.markdown(""" |
|
<div class="kpi-explanation"> |
|
<h4>مؤشر أداء الجدول الزمني (SPI)</h4> |
|
<ul> |
|
<li><strong>SPI > 1.0:</strong> المشروع متقدم عن الجدول الزمني</li> |
|
<li><strong>SPI = 1.0:</strong> المشروع في الموعد المحدد</li> |
|
<li><strong>SPI < 1.0:</strong> المشروع متأخر عن الجدول الزمني</li> |
|
</ul> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
with col2: |
|
st.markdown(""" |
|
<div class="kpi-explanation"> |
|
<h4>مؤشر أداء التكلفة (CPI)</h4> |
|
<ul> |
|
<li><strong>CPI > 1.0:</strong> المشروع أقل من الميزانية</li> |
|
<li><strong>CPI = 1.0:</strong> المشروع ضمن الميزانية</li> |
|
<li><strong>CPI < 1.0:</strong> المشروع تجاوز الميزانية</li> |
|
</ul> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
with tabs[1]: |
|
|
|
dates = [datetime.strptime(date, '%Y-%m-%d') for date in self.kpi_data['trends']['dates']] |
|
quality_values = self.kpi_data['trends']['quality_score'] |
|
risk_values = self.kpi_data['trends']['risk_score'] |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
x=dates, |
|
y=quality_values, |
|
mode='lines+markers', |
|
name='درجة الجودة (%)', |
|
line=dict(color='#00b894', width=2), |
|
marker=dict(size=8) |
|
)) |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
x=dates, |
|
y=risk_values, |
|
mode='lines+markers', |
|
name='درجة المخاطر (%)', |
|
line=dict(color='#d63031', width=2), |
|
marker=dict(size=8), |
|
yaxis="y2" |
|
)) |
|
|
|
|
|
fig.update_layout( |
|
title="اتجاهات الجودة والمخاطر", |
|
xaxis_title="التاريخ", |
|
yaxis=dict( |
|
title="درجة الجودة (%)", |
|
range=[0, 100], |
|
side="left", |
|
zeroline=False |
|
), |
|
yaxis2=dict( |
|
title="درجة المخاطر (%)", |
|
range=[0, 100], |
|
side="right", |
|
overlaying="y", |
|
zeroline=False |
|
), |
|
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1), |
|
height=350, |
|
margin=dict(l=20, r=20, t=50, b=20), |
|
hovermode='x unified', |
|
plot_bgcolor='rgba(255, 255, 255, 0.8)' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
with col1: |
|
st.markdown(""" |
|
<div class="kpi-explanation"> |
|
<h4>درجة الجودة</h4> |
|
<ul> |
|
<li><strong>90-100%:</strong> ممتاز</li> |
|
<li><strong>80-89%:</strong> جيد جداً</li> |
|
<li><strong>70-79%:</strong> جيد</li> |
|
<li><strong>< 70%:</strong> تحتاج إلى تحسين</li> |
|
</ul> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
with col2: |
|
st.markdown(""" |
|
<div class="kpi-explanation"> |
|
<h4>درجة المخاطر</h4> |
|
<ul> |
|
<li><strong>0-20%:</strong> منخفضة</li> |
|
<li><strong>21-40%:</strong> متوسطة</li> |
|
<li><strong>41-70%:</strong> عالية</li> |
|
<li><strong>> 70%:</strong> حرجة</li> |
|
</ul> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
def _render_issues_table(self): |
|
"""عرض جدول المشكلات""" |
|
st.markdown("<h3 class='dashboard-section-title'>مشكلات المشروع</h3>", unsafe_allow_html=True) |
|
|
|
if not self.kpi_data['issues']: |
|
st.info("لا توجد مشكلات مسجلة للمشروع.") |
|
return |
|
|
|
|
|
issues_data = pd.DataFrame(self.kpi_data['issues']) |
|
|
|
|
|
st.markdown(""" |
|
<style> |
|
.issue-severity { |
|
padding: 3px 8px; |
|
border-radius: 10px; |
|
font-size: 0.8rem; |
|
color: white; |
|
display: inline-block; |
|
text-align: center; |
|
min-width: 80px; |
|
} |
|
.high { |
|
background-color: #d63031; |
|
} |
|
.medium { |
|
background-color: #fdcb6e; |
|
color: black; |
|
} |
|
.low { |
|
background-color: #00b894; |
|
} |
|
|
|
.issue-status { |
|
padding: 3px 8px; |
|
border-radius: 10px; |
|
font-size: 0.8rem; |
|
display: inline-block; |
|
text-align: center; |
|
min-width: 80px; |
|
} |
|
.resolved { |
|
background-color: #00b894; |
|
color: white; |
|
} |
|
.in-progress { |
|
background-color: #fdcb6e; |
|
color: black; |
|
} |
|
.open { |
|
background-color: #d63031; |
|
color: white; |
|
} |
|
.on-hold { |
|
background-color: #a29bfe; |
|
color: white; |
|
} |
|
</style> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
for issue in self.kpi_data['issues']: |
|
|
|
severity_class = "" |
|
if issue['severity'] == "عالي": |
|
severity_class = "high" |
|
elif issue['severity'] == "متوسط": |
|
severity_class = "medium" |
|
else: |
|
severity_class = "low" |
|
|
|
|
|
status_class = "" |
|
if issue['status'] == "تم الحل": |
|
status_class = "resolved" |
|
elif issue['status'] == "قيد المعالجة": |
|
status_class = "in-progress" |
|
elif issue['status'] == "معلق": |
|
status_class = "on-hold" |
|
else: |
|
status_class = "open" |
|
|
|
st.markdown(f""" |
|
<div class="issue-card"> |
|
<div class="issue-header"> |
|
<div class="issue-title">#{issue['id']} - {issue['description']}</div> |
|
<div class="issue-meta"> |
|
<span class="issue-severity {severity_class}">{issue['severity']}</span> |
|
<span class="issue-status {status_class}">{issue['status']}</span> |
|
</div> |
|
</div> |
|
<div class="issue-details"> |
|
<div class="issue-info"> |
|
<div class="issue-date">تاريخ الإنشاء: {issue['created_date']}</div> |
|
<div class="issue-responsible">المسؤول: {issue['responsible']}</div> |
|
</div> |
|
<div class="issue-resolution"> |
|
<div>خطة المعالجة: {issue['resolution']}</div> |
|
</div> |
|
</div> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
if st.checkbox("إضافة مشكلة جديدة"): |
|
with st.form("add_issue_form"): |
|
description = st.text_input("وصف المشكلة") |
|
|
|
col1, col2 = st.columns(2) |
|
with col1: |
|
severity = st.selectbox("الخطورة", ["منخفض", "متوسط", "عالي"]) |
|
with col2: |
|
responsible = st.text_input("المسؤول") |
|
|
|
resolution = st.text_area("خطة المعالجة") |
|
|
|
submitted = st.form_submit_button("إضافة المشكلة") |
|
if submitted: |
|
if description and responsible: |
|
self.add_project_issue(description, severity, responsible, resolution) |
|
st.success("تمت إضافة المشكلة بنجاح") |
|
st.rerun() |
|
else: |
|
st.error("يرجى ملء جميع الحقول المطلوبة") |
|
|
|
def _render_update_panel(self): |
|
"""عرض لوحة تحديث حالة المشروع""" |
|
st.markdown("<h3 class='dashboard-section-title'>تحديث حالة المشروع</h3>", unsafe_allow_html=True) |
|
|
|
tabs = st.tabs(["تحديث مرحلة", "تحديث مؤشرات الأداء", "تحديث حالة مشكلة"]) |
|
|
|
with tabs[0]: |
|
|
|
sorted_phases = sorted(self.project_data['phases'], key=lambda x: x['order']) |
|
phase_names = [phase['name'] for phase in sorted_phases] |
|
phase_ids = [phase['id'] for phase in sorted_phases] |
|
|
|
selected_phase_index = st.selectbox("اختر المرحلة", range(len(phase_names)), format_func=lambda i: phase_names[i]) |
|
selected_phase = sorted_phases[selected_phase_index] |
|
|
|
with st.form("update_phase_form"): |
|
st.markdown(f"**تحديث مرحلة: {selected_phase['name']}**") |
|
|
|
col1, col2 = st.columns(2) |
|
with col1: |
|
progress = st.slider("نسبة التقدم", 0, 100, selected_phase['progress']) |
|
with col2: |
|
status = st.selectbox("الحالة", [ |
|
"not_started", "in_progress", "completed", "delayed", "on_hold" |
|
], |
|
index=["not_started", "in_progress", "completed", "delayed", "on_hold"].index(selected_phase['status']), |
|
format_func=lambda s: self._get_status_text(s)) |
|
|
|
actual_end_date = None |
|
if status == "completed": |
|
actual_end_date = st.date_input("تاريخ الانتهاء الفعلي", |
|
value=datetime.now() if not selected_phase['actual_end_date'] else datetime.strptime(selected_phase['actual_end_date'], '%Y-%m-%d')) |
|
actual_end_date = actual_end_date.strftime('%Y-%m-%d') |
|
|
|
notes = st.text_area("ملاحظات", selected_phase['notes']) |
|
|
|
submitted = st.form_submit_button("تحديث المرحلة") |
|
if submitted: |
|
success = self.update_project_status(selected_phase['id'], progress, status, actual_end_date, notes) |
|
if success: |
|
st.success("تم تحديث حالة المرحلة بنجاح") |
|
st.rerun() |
|
else: |
|
st.error("حدث خطأ أثناء تحديث حالة المرحلة") |
|
|
|
with tabs[1]: |
|
|
|
with st.form("update_kpi_form"): |
|
st.markdown("**تحديث مؤشرات الأداء**") |
|
|
|
col1, col2 = st.columns(2) |
|
with col1: |
|
spi = st.number_input("مؤشر أداء الجدول الزمني (SPI)", 0.1, 2.0, self.kpi_data['spi'], 0.01) |
|
cpi = st.number_input("مؤشر أداء التكلفة (CPI)", 0.1, 2.0, self.kpi_data['cpi'], 0.01) |
|
quality_score = st.slider("درجة الجودة", 0, 100, self.kpi_data['quality_score']) |
|
risk_score = st.slider("درجة المخاطر", 0, 100, self.kpi_data['risk_score']) |
|
|
|
with col2: |
|
resource_utilization = st.slider("استغلال الموارد", 0, 100, self.kpi_data['resource_utilization']) |
|
safety_incidents = st.number_input("حوادث السلامة", 0, 100, self.kpi_data['safety_incidents']) |
|
customer_satisfaction = st.slider("رضا العميل", 0, 100, self.kpi_data['customer_satisfaction']) |
|
environmental_compliance = st.slider("الامتثال البيئي", 0, 100, self.kpi_data['environmental_compliance']) |
|
|
|
submitted = st.form_submit_button("تحديث مؤشرات الأداء") |
|
if submitted: |
|
kpi_updates = { |
|
'spi': spi, |
|
'cpi': cpi, |
|
'quality_score': quality_score, |
|
'risk_score': risk_score, |
|
'resource_utilization': resource_utilization, |
|
'safety_incidents': safety_incidents, |
|
'customer_satisfaction': customer_satisfaction, |
|
'environmental_compliance': environmental_compliance |
|
} |
|
|
|
success = self.update_kpi_values(kpi_updates) |
|
if success: |
|
st.success("تم تحديث مؤشرات الأداء بنجاح") |
|
st.rerun() |
|
else: |
|
st.error("حدث خطأ أثناء تحديث مؤشرات الأداء") |
|
|
|
with tabs[2]: |
|
|
|
if not self.kpi_data['issues']: |
|
st.info("لا توجد مشكلات لتحديثها.") |
|
else: |
|
issue_descriptions = [f"#{issue['id']} - {issue['description']}" for issue in self.kpi_data['issues']] |
|
issue_ids = [issue['id'] for issue in self.kpi_data['issues']] |
|
|
|
selected_issue_index = st.selectbox("اختر المشكلة", range(len(issue_descriptions)), format_func=lambda i: issue_descriptions[i]) |
|
selected_issue = self.kpi_data['issues'][selected_issue_index] |
|
|
|
with st.form("update_issue_form"): |
|
st.markdown(f"**تحديث حالة المشكلة: {selected_issue['description']}**") |
|
|
|
status = st.selectbox("الحالة", [ |
|
"مفتوح", "قيد المعالجة", "تم الحل", "معلق" |
|
], |
|
index=["مفتوح", "قيد المعالجة", "تم الحل", "معلق"].index(selected_issue['status']) if selected_issue['status'] in ["مفتوح", "قيد المعالجة", "تم الحل", "معلق"] else 0) |
|
|
|
resolution = st.text_area("خطة المعالجة / الحل", selected_issue['resolution']) |
|
|
|
submitted = st.form_submit_button("تحديث المشكلة") |
|
if submitted: |
|
success = self.update_issue_status(selected_issue['id'], status, resolution) |
|
if success: |
|
st.success("تم تحديث حالة المشكلة بنجاح") |
|
st.rerun() |
|
else: |
|
st.error("حدث خطأ أثناء تحديث حالة المشكلة") |
|
|
|
def _get_status_text(self, status): |
|
"""الحصول على النص العربي لحالة المرحلة""" |
|
status_map = { |
|
"not_started": "لم تبدأ", |
|
"in_progress": "قيد التنفيذ", |
|
"completed": "مكتملة", |
|
"delayed": "متأخرة", |
|
"on_hold": "متوقفة" |
|
} |
|
return status_map.get(status, status) |
|
|
|
def _get_status_class(self, status): |
|
"""الحصول على فئة CSS لحالة المرحلة""" |
|
status_map = { |
|
"not_started": "not-started", |
|
"in_progress": "in-progress", |
|
"completed": "completed", |
|
"delayed": "delayed", |
|
"on_hold": "on-hold" |
|
} |
|
return status_map.get(status, "") |
|
|
|
def render(self): |
|
"""عرض واجهة متتبع حالة المشروع""" |
|
|
|
st.markdown(""" |
|
<style> |
|
.module-title { |
|
color: #1E88E5; |
|
font-size: 1.8rem; |
|
font-weight: bold; |
|
margin-bottom: 1rem; |
|
text-align: center; |
|
} |
|
|
|
.module-description { |
|
background-color: #f8f9fa; |
|
border-right: 4px solid #1E88E5; |
|
padding: 1rem; |
|
margin-bottom: 1.5rem; |
|
color: #444; |
|
font-size: 1rem; |
|
text-align: right; |
|
} |
|
|
|
.dashboard-section-title { |
|
font-size: 1.3rem; |
|
color: #2c3e50; |
|
margin: 1.5rem 0 1rem 0; |
|
padding-bottom: 0.3rem; |
|
border-bottom: 2px solid #e3f2fd; |
|
} |
|
|
|
.progress-section-title { |
|
font-size: 1.1rem; |
|
color: #1E88E5; |
|
margin: 1rem 0 0.5rem 0; |
|
font-weight: bold; |
|
} |
|
|
|
.project-info-card { |
|
background-color: #ffffff; |
|
padding: 1.2rem; |
|
border-radius: 8px; |
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
|
margin-bottom: 1rem; |
|
height: 100%; |
|
} |
|
|
|
.project-name { |
|
font-size: 1.3rem; |
|
font-weight: bold; |
|
color: #1E88E5; |
|
margin-bottom: 0.5rem; |
|
} |
|
|
|
.project-code, .project-client, .project-location { |
|
font-size: 0.95rem; |
|
color: #444; |
|
margin-bottom: 0.3rem; |
|
} |
|
|
|
.date-info-card, .progress-info-card { |
|
background-color: #ffffff; |
|
padding: 1.2rem; |
|
border-radius: 8px; |
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
|
margin-bottom: 1rem; |
|
height: 100%; |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: space-between; |
|
} |
|
|
|
.date-title, .progress-title { |
|
font-size: 0.85rem; |
|
color: #555; |
|
margin-bottom: 0.2rem; |
|
} |
|
|
|
.date-value, .progress-value { |
|
font-size: 1.1rem; |
|
font-weight: bold; |
|
color: #333; |
|
margin-bottom: 0.8rem; |
|
} |
|
|
|
.date-value.remaining { |
|
color: #1E88E5; |
|
} |
|
|
|
.progress-value.update-time { |
|
font-size: 0.9rem; |
|
color: #777; |
|
} |
|
|
|
.kpi-card { |
|
background-color: white; |
|
border-radius: 8px; |
|
padding: 1rem; |
|
margin-bottom: 1rem; |
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); |
|
text-align: center; |
|
} |
|
|
|
.kpi-title { |
|
font-size: 0.85rem; |
|
color: #555; |
|
margin-bottom: 0.5rem; |
|
} |
|
|
|
.kpi-value { |
|
font-size: 1.6rem; |
|
font-weight: bold; |
|
margin-bottom: 0.3rem; |
|
} |
|
|
|
.kpi-trend, .kpi-description { |
|
font-size: 0.8rem; |
|
color: #666; |
|
} |
|
|
|
.progress-status { |
|
background-color: #f8f9fa; |
|
padding: 0.5rem 1rem; |
|
text-align: center; |
|
border-radius: 4px; |
|
font-weight: bold; |
|
margin: 0.5rem 0 1.5rem 0; |
|
} |
|
|
|
.legend-item { |
|
display: flex; |
|
align-items: center; |
|
margin: 0.3rem 0; |
|
} |
|
|
|
.legend-color { |
|
width: 15px; |
|
height: 15px; |
|
border-radius: 3px; |
|
margin-left: 0.5rem; |
|
display: inline-block; |
|
} |
|
|
|
.phase-details-card { |
|
background-color: #ffffff; |
|
border-radius: 8px; |
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
|
margin-bottom: 1rem; |
|
padding: 1rem; |
|
border-right: 4px solid #ddd; |
|
} |
|
|
|
.phase-details-card.completed { |
|
border-right-color: #00b894; |
|
} |
|
|
|
.phase-details-card.in-progress { |
|
border-right-color: #fdcb6e; |
|
} |
|
|
|
.phase-details-card.delayed { |
|
border-right-color: #d63031; |
|
} |
|
|
|
.phase-details-card.on-hold { |
|
border-right-color: #a29bfe; |
|
} |
|
|
|
.phase-details-card.not-started { |
|
border-right-color: #dfe6e9; |
|
} |
|
|
|
.phase-details-header { |
|
display: flex; |
|
justify-content: space-between; |
|
margin-bottom: 0.5rem; |
|
} |
|
|
|
.phase-details-name { |
|
font-weight: bold; |
|
color: #333; |
|
font-size: 1.1rem; |
|
} |
|
|
|
.critical-badge { |
|
background-color: #d63031; |
|
color: white; |
|
padding: 0.1rem 0.4rem; |
|
border-radius: 4px; |
|
font-size: 0.7rem; |
|
margin-right: 0.5rem; |
|
} |
|
|
|
.phase-details-status { |
|
padding: 0.2rem 0.5rem; |
|
border-radius: 4px; |
|
font-size: 0.8rem; |
|
background-color: #dfe6e9; |
|
color: #2d3436; |
|
} |
|
|
|
.phase-details-card.completed .phase-details-status { |
|
background-color: #00b894; |
|
color: white; |
|
} |
|
|
|
.phase-details-card.in-progress .phase-details-status { |
|
background-color: #fdcb6e; |
|
color: #2d3436; |
|
} |
|
|
|
.phase-details-card.delayed .phase-details-status { |
|
background-color: #d63031; |
|
color: white; |
|
} |
|
|
|
.phase-details-card.on-hold .phase-details-status { |
|
background-color: #a29bfe; |
|
color: white; |
|
} |
|
|
|
.phase-details-progress { |
|
display: flex; |
|
align-items: center; |
|
margin-bottom: 0.8rem; |
|
} |
|
|
|
.phase-details-progress-bar { |
|
flex: 1; |
|
height: 8px; |
|
background-color: #f1f1f1; |
|
border-radius: 4px; |
|
overflow: hidden; |
|
margin-left: 1rem; |
|
} |
|
|
|
.phase-details-progress-fill { |
|
height: 100%; |
|
background-color: #1E88E5; |
|
border-radius: 4px; |
|
} |
|
|
|
.phase-details-card.completed .phase-details-progress-fill { |
|
background-color: #00b894; |
|
} |
|
|
|
.phase-details-card.in-progress .phase-details-progress-fill { |
|
background-color: #fdcb6e; |
|
} |
|
|
|
.phase-details-card.delayed .phase-details-progress-fill { |
|
background-color: #d63031; |
|
} |
|
|
|
.phase-details-card.on-hold .phase-details-progress-fill { |
|
background-color: #a29bfe; |
|
} |
|
|
|
.phase-details-progress-text { |
|
font-weight: bold; |
|
color: #333; |
|
width: 40px; |
|
text-align: left; |
|
} |
|
|
|
.phase-details-info { |
|
display: flex; |
|
margin-bottom: 0.8rem; |
|
} |
|
|
|
.phase-details-dates, .phase-details-responsible { |
|
flex: 1; |
|
font-size: 0.85rem; |
|
color: #555; |
|
} |
|
|
|
.phase-details-notes { |
|
font-size: 0.85rem; |
|
color: #666; |
|
background-color: #f8f9fa; |
|
padding: 0.5rem; |
|
border-radius: 4px; |
|
} |
|
|
|
.kpi-explanation { |
|
background-color: #f8f9fa; |
|
padding: 1rem; |
|
border-radius: 8px; |
|
margin-top: 1rem; |
|
} |
|
|
|
.kpi-explanation h4 { |
|
color: #2c3e50; |
|
font-size: 1rem; |
|
margin-bottom: 0.5rem; |
|
} |
|
|
|
.kpi-explanation ul { |
|
padding-right: 1.5rem; |
|
margin: 0; |
|
} |
|
|
|
.kpi-explanation li { |
|
font-size: 0.85rem; |
|
color: #555; |
|
margin-bottom: 0.3rem; |
|
} |
|
|
|
.issue-card { |
|
background-color: white; |
|
border-radius: 8px; |
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
|
margin-bottom: 1rem; |
|
padding: 1rem; |
|
} |
|
|
|
.issue-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 0.8rem; |
|
} |
|
|
|
.issue-title { |
|
font-weight: bold; |
|
font-size: 1rem; |
|
color: #333; |
|
} |
|
|
|
.issue-meta { |
|
display: flex; |
|
gap: 0.5rem; |
|
} |
|
|
|
.issue-details { |
|
display: flex; |
|
margin-top: 0.8rem; |
|
} |
|
|
|
.issue-info { |
|
flex: 1; |
|
font-size: 0.85rem; |
|
color: #555; |
|
} |
|
|
|
.issue-resolution { |
|
flex: 2; |
|
font-size: 0.85rem; |
|
color: #555; |
|
} |
|
</style> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
self.render_project_status_dashboard() |