|
""" |
|
وحدة مقارنة المستندات - نظام تحليل المناقصات |
|
""" |
|
|
|
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() |
|
|