Wahbi-AI / modules /document_comparison /document_comparison_app.py
EGYADMIN's picture
Upload 46 files
a854c1b verified
raw
history blame
48.4 kB
"""
وحدة مقارنة المستندات - نظام تحليل المناقصات
"""
import streamlit as st
import pandas as pd
import numpy as np
import os
import sys
from pathlib import Path
import difflib
import re
import datetime
# إضافة مسار المشروع للنظام
sys.path.append(str(Path(__file__).parent.parent))
# استيراد محسن واجهة المستخدم
from styling.enhanced_ui import UIEnhancer
class DocumentComparisonApp:
"""تطبيق مقارنة المستندات"""
def __init__(self):
"""تهيئة تطبيق مقارنة المستندات"""
self.ui = UIEnhancer(page_title="مقارنة المستندات - نظام تحليل المناقصات", page_icon="📄")
self.ui.apply_theme_colors()
# بيانات المستندات (نموذجية)
self.documents_data = [
{
"id": "DOC001",
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
"type": "كراسة شروط",
"version": "1.0",
"date": "2025-01-15",
"size": 2.4,
"pages": 45,
"related_entity": "T-2025-001",
"path": "/documents/T-2025-001/specs_v1.pdf"
},
{
"id": "DOC002",
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
"type": "كراسة شروط",
"version": "1.1",
"date": "2025-02-10",
"size": 2.6,
"pages": 48,
"related_entity": "T-2025-001",
"path": "/documents/T-2025-001/specs_v1.1.pdf"
},
{
"id": "DOC003",
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
"type": "كراسة شروط",
"version": "2.0",
"date": "2025-03-05",
"size": 2.8,
"pages": 52,
"related_entity": "T-2025-001",
"path": "/documents/T-2025-001/specs_v2.0.pdf"
},
{
"id": "DOC004",
"name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
"type": "جدول كميات",
"version": "1.0",
"date": "2025-01-15",
"size": 1.2,
"pages": 20,
"related_entity": "T-2025-001",
"path": "/documents/T-2025-001/boq_v1.0.xlsx"
},
{
"id": "DOC005",
"name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
"type": "جدول كميات",
"version": "1.1",
"date": "2025-02-20",
"size": 1.3,
"pages": 22,
"related_entity": "T-2025-001",
"path": "/documents/T-2025-001/boq_v1.1.xlsx"
},
{
"id": "DOC006",
"name": "المخططات - مناقصة إنشاء مبنى إداري",
"type": "مخططات",
"version": "1.0",
"date": "2025-01-15",
"size": 15.6,
"pages": 30,
"related_entity": "T-2025-001",
"path": "/documents/T-2025-001/drawings_v1.0.pdf"
},
{
"id": "DOC007",
"name": "المخططات - مناقصة إنشاء مبنى إداري",
"type": "مخططات",
"version": "2.0",
"date": "2025-03-10",
"size": 18.2,
"pages": 35,
"related_entity": "T-2025-001",
"path": "/documents/T-2025-001/drawings_v2.0.pdf"
},
{
"id": "DOC008",
"name": "كراسة الشروط - مناقصة صيانة طرق",
"type": "كراسة شروط",
"version": "1.0",
"date": "2025-02-05",
"size": 1.8,
"pages": 32,
"related_entity": "T-2025-002",
"path": "/documents/T-2025-002/specs_v1.0.pdf"
},
{
"id": "DOC009",
"name": "كراسة الشروط - مناقصة صيانة طرق",
"type": "كراسة شروط",
"version": "1.1",
"date": "2025-03-15",
"size": 1.9,
"pages": 34,
"related_entity": "T-2025-002",
"path": "/documents/T-2025-002/specs_v1.1.pdf"
},
{
"id": "DOC010",
"name": "جدول الكميات - مناقصة صيانة طرق",
"type": "جدول كميات",
"version": "1.0",
"date": "2025-02-05",
"size": 0.9,
"pages": 15,
"related_entity": "T-2025-002",
"path": "/documents/T-2025-002/boq_v1.0.xlsx"
}
]
# بيانات نموذجية لمحتوى المستندات (للعرض فقط)
self.sample_document_content = {
"DOC001": """
# كراسة الشروط والمواصفات
## مناقصة إنشاء مبنى إداري
### 1. مقدمة
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
### 2. نطاق العمل
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع، ويشمل ذلك:
- أعمال الهيكل الإنشائي
- أعمال التشطيبات الداخلية والخارجية
- أعمال الكهرباء والميكانيكا
- أعمال تنسيق الموقع
### 3. المواصفات الفنية
#### 3.1 أعمال الخرسانة
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 30 نيوتن/مم²
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
#### 3.2 أعمال التشطيبات
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
### 4. الشروط العامة
- مدة التنفيذ: 18 شهراً من تاريخ استلام الموقع
- غرامة التأخير: 0.1% من قيمة العقد عن كل يوم تأخير
- ضمان الأعمال: 10 سنوات للهيكل الإنشائي، 5 سنوات للأعمال الميكانيكية والكهربائية
""",
"DOC002": """
# كراسة الشروط والمواصفات
## مناقصة إنشاء مبنى إداري
### 1. مقدمة
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
### 2. نطاق العمل
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5500 متر مربع، ويشمل ذلك:
- أعمال الهيكل الإنشائي
- أعمال التشطيبات الداخلية والخارجية
- أعمال الكهرباء والميكانيكا
- أعمال تنسيق الموقع
- أعمال أنظمة الأمن والسلامة
### 3. المواصفات الفنية
#### 3.1 أعمال الخرسانة
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 35 نيوتن/مم²
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
#### 3.2 أعمال التشطيبات
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
- يجب استخدام زجاج عاكس للحرارة للواجهات
### 4. الشروط العامة
- مدة التنفيذ: 16 شهراً من تاريخ استلام الموقع
- غرامة التأخير: 0.15% من قيمة العقد عن كل يوم تأخير
- ضمان الأعمال: 10 سنوات للهيكل الإنشائي، 5 سنوات للأعمال الميكانيكية والكهربائية
""",
"DOC003": """
# كراسة الشروط والمواصفات
## مناقصة إنشاء مبنى إداري
### 1. مقدمة
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض وفقاً للمواصفات المعتمدة من الهيئة السعودية للمواصفات والمقاييس.
### 2. نطاق العمل
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 6 طوابق بمساحة إجمالية 6000 متر مربع، ويشمل ذلك:
- أعمال الهيكل الإنشائي
- أعمال التشطيبات الداخلية والخارجية
- أعمال الكهرباء والميكانيكا
- أعمال تنسيق الموقع
- أعمال أنظمة الأمن والسلامة
- أعمال أنظمة المباني الذكية
### 3. المواصفات الفنية
#### 3.1 أعمال الخرسانة
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 40 نيوتن/مم²
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
- يجب استخدام إضافات للخرسانة لزيادة مقاومتها للعوامل الجوية
#### 3.2 أعمال التشطيبات
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
- يجب استخدام زجاج عاكس للحرارة للواجهات
- يجب استخدام مواد صديقة للبيئة
### 4. الشروط العامة
- مدة التنفيذ: 15 شهراً من تاريخ استلام الموقع
- غرامة التأخير: 0.2% من قيمة العقد عن كل يوم تأخير
- ضمان الأعمال: 15 سنوات للهيكل الإنشائي، 7 سنوات للأعمال الميكانيكية والكهربائية
### 5. متطلبات الاستدامة
- يجب أن يحقق المبنى متطلبات الاستدامة وفقاً لمعايير LEED
- يجب توفير أنظمة لترشيد استهلاك الطاقة والمياه
"""
}
def run(self):
"""تشغيل تطبيق مقارنة المستندات"""
# إنشاء قائمة العناصر
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 = self.ui.create_sidebar(menu_items)
# إنشاء ترويسة الصفحة
self.ui.create_header("مقارنة المستندات", "أدوات متقدمة لمقارنة وتحليل المستندات")
# إنشاء علامات تبويب للوظائف المختلفة
tabs = st.tabs(["مقارنة الإصدارات", "مقارنة المستندات", "تحليل التغييرات", "سجل التغييرات"])
# علامة تبويب مقارنة الإصدارات
with tabs[0]:
self.compare_versions()
# علامة تبويب مقارنة المستندات
with tabs[1]:
self.compare_documents()
# علامة تبويب تحليل التغييرات
with tabs[2]:
self.analyze_changes()
# علامة تبويب سجل التغييرات
with tabs[3]:
self.show_change_history()
def compare_versions(self):
"""مقارنة إصدارات المستندات"""
st.markdown("### مقارنة إصدارات المستندات")
# اختيار المناقصة
tender_options = list(set([doc["related_entity"] for doc in self.documents_data]))
selected_tender = st.selectbox(
"اختر المناقصة",
options=tender_options
)
# فلترة المستندات حسب المناقصة المختارة
filtered_docs = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender]
# اختيار نوع المستند
doc_types = list(set([doc["type"] for doc in filtered_docs]))
selected_type = st.selectbox(
"اختر نوع المستند",
options=doc_types
)
# فلترة المستندات حسب النوع المختار
type_filtered_docs = [doc for doc in filtered_docs if doc["type"] == selected_type]
# ترتيب المستندات حسب الإصدار
type_filtered_docs = sorted(type_filtered_docs, key=lambda x: x["version"])
if len(type_filtered_docs) < 2:
st.warning("يجب توفر إصدارين على الأقل للمقارنة")
else:
# اختيار الإصدارات للمقارنة
col1, col2 = st.columns(2)
with col1:
version_options = [f"{doc['name']} (الإصدار {doc['version']})" for doc in type_filtered_docs]
selected_version1_index = st.selectbox(
"الإصدار الأول",
options=range(len(version_options)),
format_func=lambda x: version_options[x]
)
selected_doc1 = type_filtered_docs[selected_version1_index]
with col2:
remaining_indices = [i for i in range(len(type_filtered_docs)) if i != selected_version1_index]
selected_version2_index = st.selectbox(
"الإصدار الثاني",
options=remaining_indices,
format_func=lambda x: version_options[x]
)
selected_doc2 = type_filtered_docs[selected_version2_index]
# زر بدء المقارنة
if st.button("بدء المقارنة", use_container_width=True):
# عرض معلومات المستندات المختارة
st.markdown("### معلومات المستندات المختارة")
col1, col2 = st.columns(2)
with col1:
st.markdown(f"**الإصدار الأول:** {selected_doc1['version']}")
st.markdown(f"**التاريخ:** {selected_doc1['date']}")
st.markdown(f"**عدد الصفحات:** {selected_doc1['pages']}")
st.markdown(f"**الحجم:** {selected_doc1['size']} ميجابايت")
with col2:
st.markdown(f"**الإصدار الثاني:** {selected_doc2['version']}")
st.markdown(f"**التاريخ:** {selected_doc2['date']}")
st.markdown(f"**عدد الصفحات:** {selected_doc2['pages']}")
st.markdown(f"**الحجم:** {selected_doc2['size']} ميجابايت")
# الحصول على محتوى المستندات (في تطبيق حقيقي، سيتم استرجاع المحتوى من الملفات الفعلية)
doc1_content = self.sample_document_content.get(selected_doc1["id"], "محتوى المستند غير متوفر")
doc2_content = self.sample_document_content.get(selected_doc2["id"], "محتوى المستند غير متوفر")
# إجراء المقارنة
self.display_comparison(doc1_content, doc2_content)
def display_comparison(self, text1, text2):
"""عرض نتائج المقارنة بين نصين"""
st.markdown("### نتائج المقارنة")
# تقسيم النصوص إلى أسطر
lines1 = text1.splitlines()
lines2 = text2.splitlines()
# إجراء المقارنة باستخدام difflib
d = difflib.Differ()
diff = list(d.compare(lines1, lines2))
# عرض ملخص التغييرات
added = len([line for line in diff if line.startswith('+ ')])
removed = len([line for line in diff if line.startswith('- ')])
changed = len([line for line in diff if line.startswith('? ')])
col1, col2, col3 = st.columns(3)
with col1:
self.ui.create_metric_card(
"الإضافات",
str(added),
None,
self.ui.COLORS['success']
)
with col2:
self.ui.create_metric_card(
"الحذف",
str(removed),
None,
self.ui.COLORS['danger']
)
with col3:
self.ui.create_metric_card(
"التغييرات",
str(changed // 2), # تقسيم على 2 لأن كل تغيير يظهر مرتين
None,
self.ui.COLORS['warning']
)
# عرض التغييرات بالتفصيل
st.markdown("### التغييرات بالتفصيل")
# إنشاء عرض HTML للتغييرات
html_diff = []
for line in diff:
if line.startswith('+ '):
html_diff.append(f'<div style="background-color: #e6ffe6; padding: 2px 5px; margin: 2px 0; border-left: 3px solid green;">{line[2:]}</div>')
elif line.startswith('- '):
html_diff.append(f'<div style="background-color: #ffe6e6; padding: 2px 5px; margin: 2px 0; border-left: 3px solid red;">{line[2:]}</div>')
elif line.startswith('? '):
# تجاهل أسطر التفاصيل
continue
else:
html_diff.append(f'<div style="padding: 2px 5px; margin: 2px 0;">{line[2:]}</div>')
# عرض التغييرات
st.markdown(''.join(html_diff), unsafe_allow_html=True)
# خيارات إضافية
st.markdown("### خيارات إضافية")
col1, col2, col3 = st.columns(3)
with col1:
if st.button("تصدير التغييرات", use_container_width=True):
st.success("تم تصدير التغييرات بنجاح")
with col2:
if st.button("إنشاء تقرير", use_container_width=True):
st.success("تم إنشاء التقرير بنجاح")
with col3:
if st.button("حفظ المقارنة", use_container_width=True):
st.success("تم حفظ المقارنة بنجاح")
def compare_documents(self):
"""مقارنة مستندات مختلفة"""
st.markdown("### مقارنة مستندات مختلفة")
# اختيار المستند الأول
col1, col2 = st.columns(2)
with col1:
tender1_options = list(set([doc["related_entity"] for doc in self.documents_data]))
selected_tender1 = st.selectbox(
"اختر المناقصة الأولى",
options=tender1_options,
key="tender1"
)
# فلترة المستندات حسب المناقصة المختارة
filtered_docs1 = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender1]
# اختيار المستند
doc_options1 = [f"{doc['name']} (الإصدار {doc['version']})" for doc in filtered_docs1]
selected_doc1_index = st.selectbox(
"اختر المستند الأول",
options=range(len(doc_options1)),
format_func=lambda x: doc_options1[x],
key="doc1"
)
selected_doc1 = filtered_docs1[selected_doc1_index]
with col2:
tender2_options = list(set([doc["related_entity"] for doc in self.documents_data]))
selected_tender2 = st.selectbox(
"اختر المناقصة الثانية",
options=tender2_options,
key="tender2"
)
# فلترة المستندات حسب المناقصة المختارة
filtered_docs2 = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender2]
# اختيار المستند
doc_options2 = [f"{doc['name']} (الإصدار {doc['version']})" for doc in filtered_docs2]
selected_doc2_index = st.selectbox(
"اختر المستند الثاني",
options=range(len(doc_options2)),
format_func=lambda x: doc_options2[x],
key="doc2"
)
selected_doc2 = filtered_docs2[selected_doc2_index]
# خيارات المقارنة
st.markdown("### خيارات المقارنة")
col1, col2, col3 = st.columns(3)
with col1:
comparison_type = st.radio(
"نوع المقارنة",
options=["مقارنة كاملة", "مقارنة الأقسام المتطابقة فقط", "مقارنة الاختلافات فقط"]
)
with col2:
ignore_options = st.multiselect(
"تجاهل",
options=["المسافات", "علامات الترقيم", "حالة الأحرف", "الأرقام"],
default=["المسافات"]
)
with col3:
similarity_threshold = st.slider(
"عتبة التشابه",
min_value=0.0,
max_value=1.0,
value=0.7,
step=0.05
)
# زر بدء المقارنة
if st.button("بدء المقارنة بين المستندات", use_container_width=True):
# عرض معلومات المستندات المختارة
st.markdown("### معلومات المستندات المختارة")
col1, col2 = st.columns(2)
with col1:
st.markdown(f"**المستند الأول:** {selected_doc1['name']}")
st.markdown(f"**الإصدار:** {selected_doc1['version']}")
st.markdown(f"**التاريخ:** {selected_doc1['date']}")
st.markdown(f"**المناقصة:** {selected_doc1['related_entity']}")
with col2:
st.markdown(f"**المستند الثاني:** {selected_doc2['name']}")
st.markdown(f"**الإصدار:** {selected_doc2['version']}")
st.markdown(f"**التاريخ:** {selected_doc2['date']}")
st.markdown(f"**المناقصة:** {selected_doc2['related_entity']}")
# الحصول على محتوى المستندات (في تطبيق حقيقي، سيتم استرجاع المحتوى من الملفات الفعلية)
doc1_content = self.sample_document_content.get(selected_doc1["id"], "محتوى المستند غير متوفر")
doc2_content = self.sample_document_content.get(selected_doc2["id"], "محتوى المستند غير متوفر")
# إجراء المقارنة
self.display_document_comparison(doc1_content, doc2_content, comparison_type, ignore_options, similarity_threshold)
def display_document_comparison(self, text1, text2, comparison_type, ignore_options, similarity_threshold):
"""عرض نتائج المقارنة بين مستندين"""
st.markdown("### نتائج المقارنة بين المستندين")
# تقسيم النصوص إلى أقسام (في هذا المثال، نستخدم العناوين كفواصل للأقسام)
sections1 = self.split_into_sections(text1)
sections2 = self.split_into_sections(text2)
# حساب نسبة التشابه الإجمالية
similarity = difflib.SequenceMatcher(None, text1, text2).ratio()
# عرض نسبة التشابه
st.markdown(f"**نسبة التشابه الإجمالية:** {similarity:.2%}")
# عرض مقارنة الأقسام
st.markdown("### مقارنة الأقسام")
# إنشاء جدول لمقارنة الأقسام
section_comparisons = []
for section1_title, section1_content in sections1.items():
best_match = None
best_similarity = 0
for section2_title, section2_content in sections2.items():
# حساب نسبة التشابه بين عناوين الأقسام
title_similarity = difflib.SequenceMatcher(None, section1_title, section2_title).ratio()
# حساب نسبة التشابه بين محتوى الأقسام
content_similarity = difflib.SequenceMatcher(None, section1_content, section2_content).ratio()
# حساب متوسط نسبة التشابه
avg_similarity = (title_similarity + content_similarity) / 2
if avg_similarity > best_similarity:
best_similarity = avg_similarity
best_match = {
"title": section2_title,
"content": section2_content,
"similarity": avg_similarity
}
# إضافة المقارنة إلى القائمة
if best_match and best_similarity >= similarity_threshold:
section_comparisons.append({
"section1_title": section1_title,
"section2_title": best_match["title"],
"similarity": best_similarity
})
else:
section_comparisons.append({
"section1_title": section1_title,
"section2_title": "غير موجود",
"similarity": 0
})
# إضافة الأقسام الموجودة في المستند الثاني فقط
for section2_title, section2_content in sections2.items():
if not any(comp["section2_title"] == section2_title for comp in section_comparisons):
section_comparisons.append({
"section1_title": "غير موجود",
"section2_title": section2_title,
"similarity": 0
})
# عرض جدول المقارنة
section_df = pd.DataFrame(section_comparisons)
section_df = section_df.rename(columns={
"section1_title": "القسم في المستند الأول",
"section2_title": "القسم في المستند الثاني",
"similarity": "نسبة التشابه"
})
# تنسيق نسبة التشابه
section_df["نسبة التشابه"] = section_df["نسبة التشابه"].apply(lambda x: f"{x:.2%}")
st.dataframe(
section_df,
use_container_width=True,
hide_index=True
)
# عرض تفاصيل المقارنة
st.markdown("### تفاصيل المقارنة")
# اختيار قسم للمقارنة التفصيلية
selected_section = st.selectbox(
"اختر قسماً للمقارنة التفصيلية",
options=[comp["section1_title"] for comp in section_comparisons if comp["section1_title"] != "غير موجود"]
)
# العثور على القسم المقابل في المستند الثاني
matching_comparison = next((comp for comp in section_comparisons if comp["section1_title"] == selected_section), None)
if matching_comparison and matching_comparison["section2_title"] != "غير موجود":
# الحصول على محتوى القسمين
section1_content = sections1[selected_section]
section2_content = sections2[matching_comparison["section2_title"]]
# عرض المقارنة التفصيلية
self.display_comparison(section1_content, section2_content)
else:
st.warning("القسم المحدد غير موجود في المستند الثاني")
def split_into_sections(self, text):
"""تقسيم النص إلى أقسام باستخدام العناوين"""
sections = {}
current_section = None
current_content = []
for line in text.splitlines():
# البحث عن العناوين (الأسطر التي تبدأ بـ #)
if line.strip().startswith('#'):
# حفظ القسم السابق إذا وجد
if current_section:
sections[current_section] = '\n'.join(current_content)
# بدء قسم جديد
current_section = line.strip()
current_content = []
elif current_section:
# إضافة السطر إلى محتوى القسم الحالي
current_content.append(line)
# حفظ القسم الأخير
if current_section:
sections[current_section] = '\n'.join(current_content)
return sections
def analyze_changes(self):
"""تحليل التغييرات في المستندات"""
st.markdown("### تحليل التغييرات في المستندات")
# اختيار المناقصة
tender_options = list(set([doc["related_entity"] for doc in self.documents_data]))
selected_tender = st.selectbox(
"اختر المناقصة",
options=tender_options,
key="analyze_tender"
)
# فلترة المستندات حسب المناقصة المختارة
filtered_docs = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender]
# تجميع المستندات حسب النوع
doc_types = {}
for doc in filtered_docs:
if doc["type"] not in doc_types:
doc_types[doc["type"]] = []
doc_types[doc["type"]].append(doc)
# عرض تحليل التغييرات لكل نوع مستند
for doc_type, docs in doc_types.items():
if len(docs) > 1:
with st.expander(f"تحليل التغييرات في {doc_type}"):
# ترتيب المستندات حسب الإصدار
sorted_docs = sorted(docs, key=lambda x: x["version"])
# عرض معلومات الإصدارات
st.markdown(f"**عدد الإصدارات:** {len(sorted_docs)}")
st.markdown(f"**أول إصدار:** {sorted_docs[0]['version']} ({sorted_docs[0]['date']})")
st.markdown(f"**آخر إصدار:** {sorted_docs[-1]['version']} ({sorted_docs[-1]['date']})")
# حساب التغييرات بين الإصدارات
changes = []
for i in range(1, len(sorted_docs)):
prev_doc = sorted_docs[i-1]
curr_doc = sorted_docs[i]
# حساب التغييرات (في تطبيق حقيقي، سيتم تحليل المحتوى الفعلي)
page_diff = curr_doc["pages"] - prev_doc["pages"]
size_diff = curr_doc["size"] - prev_doc["size"]
changes.append({
"from_version": prev_doc["version"],
"to_version": curr_doc["version"],
"date": curr_doc["date"],
"page_diff": page_diff,
"size_diff": size_diff
})
# عرض جدول التغييرات
changes_df = pd.DataFrame(changes)
changes_df = changes_df.rename(columns={
"from_version": "من الإصدار",
"to_version": "إلى الإصدار",
"date": "تاريخ التغيير",
"page_diff": "التغيير في عدد الصفحات",
"size_diff": "التغيير في الحجم (ميجابايت)"
})
st.dataframe(
changes_df,
use_container_width=True,
hide_index=True
)
# عرض رسم بياني للتغييرات
st.markdown("#### تطور حجم المستند عبر الإصدارات")
versions = [doc["version"] for doc in sorted_docs]
sizes = [doc["size"] for doc in sorted_docs]
chart_data = pd.DataFrame({
"الإصدار": versions,
"الحجم (ميجابايت)": sizes
})
st.line_chart(chart_data.set_index("الإصدار"))
# عرض رسم بياني لعدد الصفحات
st.markdown("#### تطور عدد الصفحات عبر الإصدارات")
pages = [doc["pages"] for doc in sorted_docs]
chart_data = pd.DataFrame({
"الإصدار": versions,
"عدد الصفحات": pages
})
st.line_chart(chart_data.set_index("الإصدار"))
# تحليل التغييرات الشاملة
st.markdown("### تحليل التغييرات الشاملة")
# حساب إجمالي التغييرات (في تطبيق حقيقي، سيتم تحليل المحتوى الفعلي)
total_docs = len(filtered_docs)
total_versions = sum(len(docs) for docs in doc_types.values())
avg_versions = total_versions / len(doc_types) if doc_types else 0
col1, col2, col3 = st.columns(3)
with col1:
self.ui.create_metric_card(
"إجمالي المستندات",
str(total_docs),
None,
self.ui.COLORS['primary']
)
with col2:
self.ui.create_metric_card(
"إجمالي الإصدارات",
str(total_versions),
None,
self.ui.COLORS['secondary']
)
with col3:
self.ui.create_metric_card(
"متوسط الإصدارات لكل نوع",
f"{avg_versions:.1f}",
None,
self.ui.COLORS['accent']
)
# عرض توزيع التغييرات حسب النوع
st.markdown("#### توزيع الإصدارات حسب نوع المستند")
type_counts = {doc_type: len(docs) for doc_type, docs in doc_types.items()}
chart_data = pd.DataFrame({
"نوع المستند": list(type_counts.keys()),
"عدد الإصدارات": list(type_counts.values())
})
st.bar_chart(chart_data.set_index("نوع المستند"))
def show_change_history(self):
"""عرض سجل التغييرات"""
st.markdown("### سجل التغييرات")
# إنشاء بيانات نموذجية لسجل التغييرات
change_history = [
{
"id": "CH001",
"document_id": "DOC001",
"document_name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
"from_version": "1.0",
"to_version": "1.1",
"change_date": "2025-02-10",
"change_type": "تحديث",
"changed_by": "أحمد محمد",
"description": "تحديث المواصفات الفنية وشروط التنفيذ",
"sections_changed": ["نطاق العمل", "المواصفات الفنية", "الشروط العامة"]
},
{
"id": "CH002",
"document_id": "DOC002",
"document_name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
"from_version": "1.1",
"to_version": "2.0",
"change_date": "2025-03-05",
"change_type": "تحديث رئيسي",
"changed_by": "سارة عبدالله",
"description": "إضافة متطلبات الاستدامة وتحديث المواصفات الفنية",
"sections_changed": ["المواصفات الفنية", "الشروط العامة", "متطلبات الاستدامة"]
},
{
"id": "CH003",
"document_id": "DOC004",
"document_name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
"from_version": "1.0",
"to_version": "1.1",
"change_date": "2025-02-20",
"change_type": "تحديث",
"changed_by": "خالد عمر",
"description": "تحديث الكميات وإضافة بنود جديدة",
"sections_changed": ["أعمال الهيكل الإنشائي", "أعمال التشطيبات", "أعمال الكهرباء"]
},
{
"id": "CH004",
"document_id": "DOC006",
"document_name": "المخططات - مناقصة إنشاء مبنى إداري",
"from_version": "1.0",
"to_version": "2.0",
"change_date": "2025-03-10",
"change_type": "تحديث رئيسي",
"changed_by": "محمد علي",
"description": "تحديث المخططات المعمارية والإنشائية",
"sections_changed": ["المخططات المعمارية", "المخططات الإنشائية", "مخططات الكهرباء"]
},
{
"id": "CH005",
"document_id": "DOC008",
"document_name": "كراسة الشروط - مناقصة صيانة طرق",
"from_version": "1.0",
"to_version": "1.1",
"change_date": "2025-03-15",
"change_type": "تحديث",
"changed_by": "فاطمة أحمد",
"description": "تحديث المواصفات الفنية وشروط التنفيذ",
"sections_changed": ["نطاق العمل", "المواصفات الفنية", "الشروط العامة"]
}
]
# إنشاء فلاتر للسجل
col1, col2, col3 = st.columns(3)
with col1:
document_filter = st.selectbox(
"المستند",
options=["الكل"] + list(set([ch["document_name"] for ch in change_history]))
)
with col2:
change_type_filter = st.selectbox(
"نوع التغيير",
options=["الكل"] + list(set([ch["change_type"] for ch in change_history]))
)
with col3:
date_range = st.date_input(
"نطاق التاريخ",
value=(
datetime.datetime.strptime("2025-01-01", "%Y-%m-%d").date(),
datetime.datetime.strptime("2025-12-31", "%Y-%m-%d").date()
)
)
# تطبيق الفلاتر
filtered_history = change_history
if document_filter != "الكل":
filtered_history = [ch for ch in filtered_history if ch["document_name"] == document_filter]
if change_type_filter != "الكل":
filtered_history = [ch for ch in filtered_history if ch["change_type"] == change_type_filter]
if len(date_range) == 2:
start_date, end_date = date_range
filtered_history = [
ch for ch in filtered_history
if start_date <= datetime.datetime.strptime(ch["change_date"], "%Y-%m-%d").date() <= end_date
]
# عرض سجل التغييرات
if not filtered_history:
st.info("لا توجد تغييرات تطابق الفلاتر المحددة")
else:
# تحويل البيانات إلى DataFrame
history_df = pd.DataFrame(filtered_history)
# إعادة ترتيب الأعمدة وتغيير أسمائها
display_df = history_df[[
"id", "document_name", "from_version", "to_version", "change_date", "change_type", "changed_by", "description"
]].rename(columns={
"id": "الرقم",
"document_name": "اسم المستند",
"from_version": "من الإصدار",
"to_version": "إلى الإصدار",
"change_date": "تاريخ التغيير",
"change_type": "نوع التغيير",
"changed_by": "بواسطة",
"description": "الوصف"
})
# عرض الجدول
st.dataframe(
display_df,
use_container_width=True,
hide_index=True
)
# عرض تفاصيل التغيير المحدد
st.markdown("### تفاصيل التغيير")
selected_change_id = st.selectbox(
"اختر تغييراً لعرض التفاصيل",
options=[ch["id"] for ch in filtered_history],
format_func=lambda x: next((f"{ch['id']} - {ch['document_name']} ({ch['from_version']} إلى {ch['to_version']})" for ch in filtered_history if ch["id"] == x), "")
)
# العثور على التغيير المحدد
selected_change = next((ch for ch in filtered_history if ch["id"] == selected_change_id), None)
if selected_change:
col1, col2 = st.columns(2)
with col1:
st.markdown(f"**المستند:** {selected_change['document_name']}")
st.markdown(f"**من الإصدار:** {selected_change['from_version']}")
st.markdown(f"**إلى الإصدار:** {selected_change['to_version']}")
st.markdown(f"**تاريخ التغيير:** {selected_change['change_date']}")
with col2:
st.markdown(f"**نوع التغيير:** {selected_change['change_type']}")
st.markdown(f"**بواسطة:** {selected_change['changed_by']}")
st.markdown(f"**الوصف:** {selected_change['description']}")
# عرض الأقسام التي تم تغييرها
st.markdown("#### الأقسام التي تم تغييرها")
for section in selected_change["sections_changed"]:
st.markdown(f"- {section}")
# أزرار الإجراءات
col1, col2, col3 = st.columns(3)
with col1:
if st.button("عرض التغييرات بالتفصيل", use_container_width=True):
st.success("تم فتح التغييرات بالتفصيل")
with col2:
if st.button("إنشاء تقرير", use_container_width=True):
st.success("تم إنشاء التقرير بنجاح")
with col3:
if st.button("تصدير التغييرات", use_container_width=True):
st.success("تم تصدير التغييرات بنجاح")
# تشغيل التطبيق
if __name__ == "__main__":
doc_comparison_app = DocumentComparisonApp()
doc_comparison_app.run()