|
"""
|
|
وحدة مقارنة المستندات - نظام تحليل المناقصات
|
|
"""
|
|
|
|
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()
|
|
|
|
|
|
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),
|
|
None,
|
|
self.ui.COLORS['warning']
|
|
)
|
|
|
|
|
|
st.markdown("### التغييرات بالتفصيل")
|
|
|
|
|
|
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:
|
|
|
|
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()
|
|
|