+ """, unsafe_allow_html=True)
+
+ # معلومات حول النظام
+ st.markdown("---")
+
+ st.markdown("""
+
+
حول النظام
+
نظام WAHBi للذكاء الاصطناعي هو نظام متكامل لتحليل العقود والمناقصات وإدارة المشاريع، مصمم خصيصاً لشركات المقاولات والبناء. يستخدم النظام تقنيات الذكاء الاصطناعي المتقدمة لتحليل المستندات واستخراج المعلومات المهمة وتقييم المخاطر ودعم اتخاذ القرار.
+
+ """, unsafe_allow_html=True)
+
+ # معلومات الشركة
+ st.markdown("""
+
استخدم هذه الوحدة لتحليل مستندات العقود والمناقصات باستخدام تقنيات الذكاء الاصطناعي المتقدمة.
+ يمكنك تحميل المستندات بتنسيقات PDF أو Word وسيقوم النظام بتحليلها واستخراج المعلومات المهمة مثل الشروط والتكاليف والمخاطر والتزاماتك كمقاول.
استخدم هذه الوحدة لإنشاء تقارير تحليلية متقدمة عن المشاريع والمناقصات والأداء العام.
+ يوفر النظام رؤى وتحليلات متعمقة تساعدك على فهم أداء مشاريعك وتحسين عمليات صنع القرار.
+
+ """, unsafe_allow_html=True)
+
+ # أنواع التقارير
+ st.markdown("### أنواع التقارير")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.markdown("""
+
+
تقارير المشاريع
+
تقارير تفصيلية عن حالة المشاريع وتقدمها ومؤشرات الأداء الرئيسية والمشكلات المحتملة.
+
+ """, unsafe_allow_html=True)
+
+ if st.button("إنشاء تقرير", key="btn_project_report"):
+ # هنا سيتم استدعاء وحدة إنشاء تقارير المشاريع
+ st.session_state.report_type = "project"
+ st.session_state.show_report_form = True
+ st.rerun()
+
+ with col2:
+ st.markdown("""
+
+
تقارير الأداء المالي
+
تحليل مالي للمشاريع يتضمن الإيرادات والتكاليف والأرباح والتدفقات النقدية والانحرافات عن الميزانية.
+
+ """, unsafe_allow_html=True)
+
+ if st.button("إنشاء تقرير", key="btn_financial_report"):
+ # هنا سيتم استدعاء وحدة إنشاء تقارير الأداء المالي
+ st.session_state.report_type = "financial"
+ st.session_state.show_report_form = True
+ st.rerun()
+
+ with col3:
+ st.markdown("""
+
+
تقارير المناقصات
+
تحليل شامل للمناقصات النشطة والمنتهية ونسب الفوز والمنافسين ومقارنة الأسعار.
+
+ """, unsafe_allow_html=True)
+
+ if st.button("إنشاء تقرير", key="btn_tender_report"):
+ # هنا سيتم استدعاء وحدة إنشاء تقارير المناقصات
+ st.session_state.report_type = "tender"
+ st.session_state.show_report_form = True
+ st.rerun()
+
+ # لوحة البيانات
+ st.markdown("### لوحة البيانات التنفيذية")
+
+ col1, col2 = st.columns([2, 1])
+
+ with col1:
+ st.markdown("#### أداء المشاريع حسب القطاع")
+
+ # إنشاء بيانات تجريبية للرسم البياني
+ sectors = ['البنية التحتية', 'السكني', 'التعليمي', 'الصحي', 'النقل']
+ performance = [85, 72, 64, 90, 78]
+
+ # إنشاء رسم بياني شريطي
+ chart_data = pd.DataFrame({'القطاع': sectors, 'الأداء (%)': performance})
+ st.bar_chart(chart_data.set_index('القطاع'), use_container_width=True)
+
+ # عرض بيان توضيحي
+ st.caption("مقارنة أداء المشاريع عبر القطاعات المختلفة (نسبة الإنجاز)")
+
+ with col2:
+ st.markdown("#### المؤشرات الرئيسية")
+
+ # نسبة المشاريع المتأخرة
+ st.markdown("##### نسبة المشاريع المتأخرة")
+ delayed_projects = 15
+ st.progress(delayed_projects / 100)
+ st.markdown(f"
", unsafe_allow_html=True)
+
+ # معدل نجاح المناقصات
+ st.markdown("##### معدل نجاح المناقصات")
+ tender_success = 35
+ st.progress(tender_success / 100)
+ st.markdown(f"
{tender_success}%
", unsafe_allow_html=True)
+
+ # تقارير الأداء
+ st.markdown("### تقارير الأداء الأخيرة")
+
+ # التقرير الأول
+ with st.container():
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ st.markdown("#### التقرير الشهري لمشاريع الربع الأول 2025")
+ st.markdown("تقرير شامل يوضح أداء جميع المشاريع النشطة خلال الربع الأول من عام 2025، بما في ذلك تحليل التكاليف والجدول الزمني والمخاطر.")
+ st.markdown("**تاريخ الإنشاء:** 15 مارس 2025")
+
+ with col2:
+ st.markdown(" ", unsafe_allow_html=True) # إضافة مسافة
+ col2_1, col2_2 = st.columns(2)
+ with col2_1:
+ if st.button("عرض", key="view_report1"):
+ st.session_state.view_report = "quarterly_q1_2025"
+ st.session_state.show_report_viewer = True
+ with col2_2:
+ if st.button("تنزيل", key="download_report1"):
+ st.info("جاري تحميل التقرير...")
+
+ st.markdown("---")
+
+ # التقرير الثاني
+ with st.container():
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ st.markdown("#### تحليل أداء المناقصات 2024-2025")
+ st.markdown("تحليل مقارن لنتائج المناقصات بين عامي 2024 و 2025، يوضح التحسن في معدلات النجاح وتحليل أسباب الخسارة وفرص التحسين.")
+ st.markdown("**تاريخ الإنشاء:** 28 فبراير 2025")
+
+ with col2:
+ st.markdown(" ", unsafe_allow_html=True) # إضافة مسافة
+ col2_1, col2_2 = st.columns(2)
+ with col2_1:
+ if st.button("عرض", key="view_report2"):
+ st.session_state.view_report = "tenders_analysis_2024_2025"
+ st.session_state.show_report_viewer = True
+ with col2_2:
+ if st.button("تنزيل", key="download_report2"):
+ st.info("جاري تحميل التقرير...")
+
+ st.markdown("---")
+
+ # التقرير الثالث
+ with st.container():
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ st.markdown("#### تقرير المخاطر المالية للمشاريع الجارية")
+ st.markdown("تقرير تفصيلي حول المخاطر المالية للمشاريع الجارية، بما في ذلك تحليل التدفقات النقدية والمستحقات المتأخرة والمطالبات المحتملة.")
+ st.markdown("**تاريخ الإنشاء:** 10 فبراير 2025")
+
+ with col2:
+ st.markdown(" ", unsafe_allow_html=True) # إضافة مسافة
+ col2_1, col2_2 = st.columns(2)
+ with col2_1:
+ if st.button("عرض", key="view_report3"):
+ st.session_state.view_report = "financial_risks_2025"
+ st.session_state.show_report_viewer = True
+ with col2_2:
+ if st.button("تنزيل", key="download_report3"):
+ st.info("جاري تحميل التقرير...")
+
+
+def render_ai_assistant():
+ """عرض واجهة المساعد الذكي باستخدام المكون الجديد"""
+ try:
+ from modules.ai_assistant.assistant_app import AssistantApp
+
+ # عرض العنوان والوصف
+ st.markdown("
المساعد الذكي
", unsafe_allow_html=True)
+
+ st.markdown("""
+
+
المساعد الذكي هو واجهة تفاعلية مدعومة بتقنيات الذكاء الاصطناعي لمساعدتك في جميع أنشطة إدارة المشاريع والعقود.
+ يمكنك طرح أسئلة بلغتك الطبيعية والحصول على إجابات فورية، أو طلب مساعدة في مهام محددة مثل تحليل بنود العقد أو تقدير التكاليف.
قم بحفظ التسعير الحالي في المشروع لاستخدامه لاحقاً في التقارير وفي إجمالي التسعير.
+
+ """, unsafe_allow_html=True)
+
+ # زر حفظ التسعير غير المتوازن
+ if st.button("حفظ التسعير غير المتوازن", type="primary", use_container_width=True):
+ st.success("تم حفظ التسعير غير المتوازن بنجاح!")
+ st.balloons() # إضافة تأثير احتفالي عند الحفظ
+
+with col2:
+ # بطاقة تصدير التسعير
+ st.markdown("""
+
+
📊
+
تصدير البيانات
+
قم بتصدير جدول التسعير الحالي بصيغة CSV لاستخدامه في برامج أخرى مثل Excel.
+
+ """, unsafe_allow_html=True)
+
+ # زر تصدير التسعير
+ export_button = st.button("تجهيز ملف للتصدير", use_container_width=True)
+ if export_button:
+ # تحويل البيانات إلى CSV
+ csv = items.to_csv(index=False)
+ st.success("تم تجهيز ملف التصدير بنجاح! يمكنك تنزيله الآن.")
+ # تقديم البيانات للتنزيل
+ st.download_button(
+ label="تنزيل ملف CSV",
+ data=csv,
+ file_name="unbalanced_pricing.csv",
+ mime="text/csv",
+ use_container_width=True
+ )
\ No newline at end of file
diff --git a/docs/technical_docs.md b/docs/technical_docs.md
new file mode 100644
index 0000000000000000000000000000000000000000..2f564c7975880e3a89c0cd9b4bdbe98401a0819f
--- /dev/null
+++ b/docs/technical_docs.md
@@ -0,0 +1,165 @@
+# التوثيق التقني
+## نظام تحليل العقود والمناقصات بالذكاء الاصطناعي - شركة شبه الجزيرة للمقاولات
+
+
+
+
+ إصدار التوثيق: 1.0.2 - تاريخ التحديث: 2025/03/01
+
+
+## جدول المحتويات
+
+1. [نظرة عامة](#نظرة-عامة)
+2. [المعمارية التقنية](#المعمارية-التقنية)
+3. [متطلبات النظام](#متطلبات-النظام)
+4. [الإعداد والتثبيت](#الإعداد-والتثبيت)
+5. [بيئة Hybrid Face](#بيئة-hybrid-face)
+6. [هيكل قاعدة البيانات](#هيكل-قاعدة-البيانات)
+7. [وحدات النظام](#وحدات-النظام)
+8. [واجهات برمجة التطبيقات (APIs)](#واجهات-برمجة-التطبيقات-apis)
+9. [الأمان والمصادقة](#الأمان-والمصادقة)
+10. [الأداء وقابلية التوسع](#الأداء-وقابلية-التوسع)
+11. [استراتيجية النسخ الاحتياطي واستعادة البيانات](#استراتيجية-النسخ-الاحتياطي-واستعادة-البيانات)
+12. [إرشادات التطوير](#إرشادات-التطوير)
+13. [اختبار النظام](#اختبار-النظام)
+14. [التكامل مع الأنظمة الخارجية](#التكامل-مع-الأنظمة-الخارجية)
+15. [سجل التغييرات](#سجل-التغييرات)
+
+## نظرة عامة
+
+### عن النظام
+
+نظام تحليل العقود والمناقصات بالذكاء الاصطناعي هو منصة متكاملة تعتمد على تقنيات الذكاء الاصطناعي ومعالجة اللغة العربية الطبيعية لمساعدة شركة شبه الجزيرة للمقاولات في تحليل وتسعير المناقصات وإدارة المشاريع.
+
+### المكونات الرئيسية
+
+1. **واجهة المستخدم (Frontend)**: تطبيق ويب تفاعلي مبني بواسطة Streamlit
+2. **خدمات الخلفية (Backend)**: مجموعة من الخدمات والوحدات البرمجية بلغة Python
+3. **قاعدة البيانات**: SQLite للتطوير والنشر المحلي، MySQL للنشر المؤسسي
+4. **محركات الذكاء الاصطناعي**: نماذج معالجة اللغة الطبيعية والتعلم الآلي
+5. **خدمات التكامل**: واجهات برمجة للتكامل مع الأنظمة الخارجية
+
+## المعمارية التقنية
+
+### المخطط العام للنظام
+
+```mermaid
+graph TD
+ User[المستخدم] --> UI[واجهة المستخدم Streamlit]
+ UI --> API[طبقة API]
+ API --> Core[النواة]
+ Core --> DB[(قاعدة البيانات)]
+ Core --> NLP[معالجة اللغة العربية]
+ Core --> ML[نماذج التعلم الآلي]
+ Core --> FS[نظام الملفات]
+ Core --> External[أنظمة خارجية]
+
+ subgraph Core Modules
+ NLP
+ ML
+ Doc[تحليل المستندات]
+ Pricing[التسعير]
+ Risk[تحليل المخاطر]
+ Res[إدارة الموارد]
+ Proj[إدارة المشاريع]
+ Rep[التقارير]
+ end
+```
+
+### نمط المعمارية
+
+النظام يعتمد على نمط المعمارية طبقية (Layered Architecture) ونمط وحدات الخدمة (Service Modules):
+
+1. **طبقة العرض**: واجهة المستخدم Streamlit
+2. **طبقة الخدمات**: واجهات برمجة التطبيقات RESTful
+3. **طبقة الأعمال**: وحدات المعالجة المنطقية
+4. **طبقة البيانات**: الوصول إلى قاعدة البيانات وتخزين الملفات
+
+## متطلبات النظام
+
+### متطلبات الأجهزة
+
+| المكون | الحد الأدنى | الموصى به |
+|--------|-------------|-----------|
+| المعالج | Intel Core i5 (8 أنوية) | Intel Core i7 (12 أنوية) أو أعلى |
+| الذاكرة | 16GB RAM | 32GB RAM أو أكثر |
+| التخزين | 10GB + مساحة للمستندات | SSD بسعة 50GB أو أكثر |
+| الشبكة | اتصال إنترنت 10Mbps | اتصال إنترنت 50Mbps أو أسرع |
+| الشاشة | دقة 1080p | دقة 1440p أو أعلى |
+
+### متطلبات البرمجيات
+
+| البرمجيات | الإصدار المطلوب |
+|-----------|-----------------|
+| نظام التشغيل | Windows 10/11، MacOS 12+، Ubuntu 20.04+ |
+| Python | 3.9 أو أحدث |
+| بيئة Hybrid Face | 2.5 أو أحدث |
+| متصفح | Chrome 90+، Firefox 88+، Edge 90+ |
+| MySQL (اختياري) | 8.0 أو أحدث |
+
+### المكتبات الأساسية
+
+```python
+# المكتبات الأساسية المستخدمة
+streamlit==1.10.0
+pandas==1.5.0
+numpy==1.23.0
+scikit-learn==1.1.0
+nltk==3.7.0
+spacy==3.4.0
+transformers==4.20.0
+pyarabic==0.6.15
+sqlalchemy==1.4.40
+plotly==5.9.0
+pymysql==1.0.2
+pdfplumber==0.7.0
+python-docx==0.8.11
+openpyxl==3.0.10
+ezdxf==0.17.2
+```
+
+## الإعداد والتثبيت
+
+### إعداد بيئة التطوير
+
+```bash
+# إنشاء بيئة Python افتراضية
+python -m venv venv
+source venv/bin/activate # Linux/MacOS
+venv\Scripts\activate # Windows
+
+# تثبيت المكتبات المطلوبة
+pip install -r requirements.txt
+pip install -r arabic_support_requirements.txt
+```
+
+### تثبيت نماذج معالجة اللغة العربية
+
+```bash
+# تثبيت نموذج اللغة العربية لـ SpaCy
+python -m spacy download ar_core_news_lg
+
+# تحميل موارد NLTK للغة العربية
+python -m nltk.downloader stopwords
+python -m nltk.downloader punkt
+python -m nltk.downloader wordnet
+```
+
+### إعداد قاعدة البيانات
+
+#### SQLite (للتطوير المحلي)
+
+```bash
+# إنشاء قاعدة بيانات SQLite
+python setup_db.py --mode=local
+```
+
+#### MySQL (للنشر المؤسسي)
+
+```bash
+# إعداد قاعدة بيانات MySQL
+python setup_db.py --mode=enterprise \
+ --db-host=YOUR_DB_HOST \
+ --db-user=YOUR_DB_USER \
+ --db-pass=YOUR_DB_PASS \
+ --db-name=tender_analysis_system
\ No newline at end of file
diff --git a/docs/user_manual.md b/docs/user_manual.md
new file mode 100644
index 0000000000000000000000000000000000000000..ea9bef924d060d015b043b636cb9b493988f5e70
--- /dev/null
+++ b/docs/user_manual.md
@@ -0,0 +1,594 @@
+# دليل المستخدم
+## نظام تحليل العقود والمناقصات بالذكاء الاصطناعي - شركة شبه الجزيرة للمقاولات
+
+
+
+
+ الإصدار 2.0.0
+
+
+## جدول المحتويات
+
+1. [مقدمة](#مقدمة)
+2. [بدء الاستخدام](#بدء-الاستخدام)
+3. [الواجهة الرئيسية](#الواجهة-الرئيسية)
+4. [إدارة المناقصات والعقود](#إدارة-المناقصات-والعقود)
+5. [تحليل المستندات](#تحليل-المستندات)
+6. [نظام التسعير الشامل](#نظام-التسعير-الشامل)
+7. [حاسبة تكاليف البناء](#حاسبة-تكاليف-البناء)
+8. [إدارة الموارد والتكاليف](#إدارة-الموارد-والتكاليف)
+9. [تحليل المخاطر](#تحليل-المخاطر)
+10. [إدارة المشاريع المرساة](#إدارة-المشاريع-المرساة)
+11. [الخرائط والمواقع](#الخرائط-والمواقع)
+12. [الإشعارات الذكية](#الإشعارات-الذكية)
+13. [الجدول الزمني التفاعلي](#الجدول-الزمني-التفاعلي)
+14. [مساعد الذكاء الاصطناعي](#مساعد-الذكاء-الاصطناعي)
+15. [مقارنة المستندات](#مقارنة-المستندات)
+16. [التقارير والتحليلات](#التقارير-والتحليلات)
+17. [إعدادات النظام](#إعدادات-النظام)
+18. [الأسئلة الشائعة](#الأسئلة-الشائعة)
+19. [استكشاف الأخطاء وإصلاحها](#استكشاف-الأخطاء-وإصلاحها)
+
+## مقدمة
+
+### حول النظام
+
+نظام تحليل العقود والمناقصات بالذكاء الاصطناعي هو منصة متكاملة تهدف إلى مساعدة شركة شبه الجزيرة للمقاولات في تحليل وتسعير المناقصات بكفاءة عالية. يعتمد النظام على تقنيات الذكاء الاصطناعي ومعالجة اللغة العربية الطبيعية لتحليل المستندات والمساعدة في عملية التسعير واتخاذ القرارات.
+
+### مزايا النظام
+
+- تحليل متقدم لكراسات الشروط والعقود باللغة العربية
+- تسعير دقيق ومنهجي للمناقصات
+- حاسبة تكاليف بناء شاملة مع مكونات متعددة
+- تحديد المخاطر وتقييمها بشكل آلي
+- إدارة الموارد والتكاليف بكفاءة
+- دعم المحتوى المحلي السعودي
+- جدول زمني تفاعلي مع تتبع المراحل
+- مساعد ذكاء اصطناعي متطور
+- متابعة شاملة للمناقصات والمشاريع
+- تقارير وتحليلات متقدمة لدعم اتخاذ القرار
+- خرائط تفاعلية لمواقع المشاريع
+- نظام إشعارات ذكي
+
+## بدء الاستخدام
+
+### تسجيل الدخول
+
+1. افتح تطبيق نظام تحليل العقود والمناقصات
+2. أدخل اسم المستخدم وكلمة المرور
+3. انقر على زر "تسجيل الدخول"
+
+
+
+### الصلاحيات ومستويات الوصول
+
+النظام يدعم عدة مستويات من الصلاحيات:
+
+| المستوى | الوصف | الصلاحيات |
+|---------|-------|-----------|
+| مدير النظام | المسؤول الرئيسي عن النظام | كامل الصلاحيات |
+| مدير المناقصات | مسؤول عن إدارة المناقصات | إضافة وتعديل وحذف المناقصات، التسعير |
+| محلل عقود | مختص بتحليل العقود والمستندات | قراءة وتحليل المستندات |
+| محاسب | مسؤول عن الجوانب المالية | الوصول للتكاليف والتسعير |
+| مستخدم عادي | مستخدم بصلاحيات محدودة | عرض المناقصات والتقارير فقط |
+
+## الواجهة الرئيسية
+
+### مكونات الواجهة
+
+
+
+1. **شريط القوائم**: للوصول إلى الوظائف الرئيسية
+2. **لوحة المعلومات**: عرض ملخص للمناقصات والمشاريع
+3. **المناقصات النشطة**: قائمة بالمناقصات قيد الدراسة
+4. **المواعيد الهامة**: تنبيهات بالمواعيد النهائية
+5. **المؤشرات الرئيسية**: إحصائيات ومؤشرات أداء رئيسية
+6. **معلومات الشركة**: بيان "هذا النظام يعمل لصالح شركة شبه الجزيرة للمقاولات، جميع الحقوق محفوظة 2025"
+
+### التنقل في النظام
+
+تم تصميم شريط القوائم للوصول السريع إلى جميع وظائف النظام:
+
+- **لوحة المعلومات**: الصفحة الرئيسية
+- **المناقصات والعقود**: إدارة المناقصات وتحليل العقود
+- **التسعير**: نظام التسعير الشامل
+- **حاسبة تكاليف البناء**: حساب تكاليف البناء بالتفصيل
+- **الموارد والتكاليف**: إدارة المواد والمعدات والعمالة
+- **تحليل المخاطر**: تقييم وإدارة المخاطر
+- **المشاريع**: إدارة المشاريع المرساة
+- **الجدول الزمني**: الجدول الزمني التفاعلي للمشاريع
+- **الخرائط**: خرائط مواقع المشاريع
+- **الإشعارات**: نظام الإشعارات الذكي
+- **المساعد الذكي**: مساعد الذكاء الاصطناعي التفاعلي
+- **مقارنة المستندات**: أدوات مقارنة المستندات المتطورة
+- **التقارير**: التقارير والتحليلات
+- **الإعدادات**: إعدادات النظام والمستخدمين
+
+## إدارة المناقصات والعقود
+
+### إضافة مناقصة جديدة
+
+1. انقر على "المناقصات والعقود" من شريط القوائم
+2. اختر "إضافة مناقصة جديدة"
+3. املأ النموذج بالمعلومات المطلوبة:
+ - اسم المناقصة
+ - الجهة المالكة
+ - رقم المناقصة
+ - تاريخ الطرح
+ - تاريخ الإقفال
+ - موقع المشروع
+ - نوع المشروع
+
+
+
+### رفع المستندات
+
+1. من صفحة تفاصيل المناقصة، انقر على "رفع مستند"
+2. اختر نوع المستند:
+ - كراسة شروط
+ - جدول كميات
+ - مخططات
+ - عقد
+ - ملحق
+3. انقر على "استعراض" واختر الملف من جهازك
+4. يدعم النظام صيغ المستندات التالية: PDF, DOCX, XLSX, DWG
+5. **جديد**: يمكنك الآن رفع صور موقع المشروع ومقاطع الفيديو ومعلومات المزايا/المخاطر واستفسارات المالك
+
+### متابعة حالة المناقصات
+
+يوفر النظام لوحة متابعة للمناقصات تعرض:
+
+- المناقصات قيد الدراسة
+- المناقصات المقدمة
+- المناقصات المرساة
+- المناقصات المستبعدة
+
+لكل مناقصة، يعرض النظام:
+- الحالة الحالية
+- نسبة الإنجاز
+- المواعيد النهائية
+- المهام المتبقية
+- **جديد**: مؤقت لبدء الدراسة ومواعيد التسليم النهائية
+
+### معلومات الموقع وسهولة الوصول
+
+**جديد**: يمكنك الآن إضافة معلومات مفصلة عن الموقع وتفاصيل الوصول إليه بجانب زر موقع المشروع، مما يساعد فرق العمل الميدانية.
+
+## تحليل المستندات
+
+### كيفية تحليل المستندات
+
+1. من صفحة تفاصيل المناقصة، اختر المستند المراد تحليله
+2. انقر على زر "تحليل المستند"
+3. اختر نوع التحليل:
+ - تحليل كامل
+ - استخراج البنود والشروط
+ - تحديد المخاطر
+ - استخراج معلومات التسعير
+
+
+
+### مراجعة نتائج التحليل
+
+بعد اكتمال التحليل، يعرض النظام:
+
+1. **البنود المستخرجة**: قائمة بالبنود والشروط المهمة مرتبة حسب أهميتها
+2. **المخاطر المحددة**: المخاطر المحتملة مصنفة حسب نوعها وأهميتها
+3. **المتطلبات الرئيسية**: قائمة بالمتطلبات الأساسية للمناقصة
+4. **الكلمات المفتاحية**: الكلمات والمصطلحات المهمة في المستند
+5. **جديد**: متطلبات المحتوى المحلي في المشاريع السعودية
+
+يمكنك النقر على أي بند لعرض النص الأصلي في المستند وسياقه.
+
+## نظام التسعير الشامل
+
+### بدء عملية التسعير
+
+1. من صفحة تفاصيل المناقصة، انقر على "بدء التسعير"
+2. اختر جدول الكميات المراد تسعيره
+3. حدد نوع التسعير:
+ - تسعير قياسي
+ - تسعير غير متزن
+ - تسعير مختلط
+
+
+
+### تسعير البنود
+
+1. لكل بند في جدول الكميات، يعرض النظام:
+ - وصف البند
+ - الوحدة
+ - الكمية
+ - التكاليف المقدرة (المواد، العمالة، المعدات)
+2. يمكنك تعديل التكاليف يدوياً أو الاعتماد على التقديرات الآلية
+3. النظام يحسب تلقائياً:
+ - المصاريف العامة
+ - هامش الربح
+ - السعر الإجمالي
+
+### التسعير غير المتزن
+
+لتطبيق استراتيجية التسعير غير المتزن:
+
+1. انقر على "التسعير غير المتزن" من صفحة التسعير
+2. اختر نوع الاستراتيجية:
+ - التحميل الأمامي
+ - التحميل الخلفي
+ - التسعير الاستراتيجي
+ - التسعير القائم على المخاطر
+3. عدل المعلمات حسب الحاجة
+4. راجع التغييرات في توزيع التكاليف والأسعار
+
+
+
+### المحتوى المحلي
+
+لحساب وتحسين نسبة المحتوى المحلي:
+
+1. انقر على "المحتوى المحلي" من صفحة التسعير
+2. قم بتقييم المعايير المختلفة:
+ - نسبة الموظفين السعوديين
+ - نسبة المواد المحلية
+ - نسبة المعدات المحلية
+ - نسبة المقاولين من الباطن المحليين
+3. راجع الدرجة الإجمالية للمحتوى المحلي والأفضلية السعرية المقابلة
+
+## حاسبة تكاليف البناء
+
+### نظرة عامة
+
+**جديد**: حاسبة تكاليف البناء المتكاملة تتيح لك حساب تكاليف المشاريع بالتفصيل، مع تقسيم واضح للعناصر المختلفة:
+
+- المواد الخام
+- المعدات
+- العمالة
+- المصاريف الإدارية
+- هوامش الربح
+
+### استخدام الحاسبة
+
+1. انقر على "حاسبة تكاليف البناء" من شريط القوائم
+2. اختر نوع المشروع من القائمة
+3. أدخل مواصفات المشروع الأساسية (المساحة، الموقع، نوع البناء)
+4. استعرض التكاليف المقدرة لكل مكون
+5. عدل البنود حسب الحاجة
+6. راجع تفصيل الأسعار والتكلفة الإجمالية
+
+### كتالوج القوالب الإنشائية
+
+**جديد**: يتضمن النظام الآن كتالوجًا شاملاً للقوالب الإنشائية لمختلف أنواع المشاريع:
+
+1. مباني سكنية
+2. مباني تجارية
+3. مشاريع بنية تحتية
+4. منشآت صناعية
+5. مرافق عامة
+
+استخدم هذه القوالب لبدء حسابات التكلفة بسرعة، ثم قم بتخصيصها حسب احتياجات مشروعك.
+
+## إدارة الموارد والتكاليف
+
+### إدارة المواد
+
+1. انقر على "الموارد والتكاليف" من شريط القوائم
+2. اختر "المواد"
+3. يمكنك:
+ - استعراض قائمة المواد
+ - إضافة مواد جديدة
+ - تحديث أسعار المواد
+ - ربط المواد بالموردين
+ - **جديد**: تقديم طلبات أسعار للمواد الخام
+
+
+
+### إدارة المعدات
+
+1. انقر على "الموارد والتكاليف" من شريط القوائم
+2. اختر "المعدات"
+3. يمكنك:
+ - استعراض قائمة المعدات
+ - تسجيل معدلات الأداء
+ - تحديث أسعار التأجير
+ - تسجيل تكاليف التشغيل
+ - **جديد**: إدارة المعدات الخاصة والمستأجرة
+
+### إدارة العمالة
+
+1. انقر على "الموارد والتكاليف" من شريط القوائم
+2. اختر "العمالة"
+3. يمكنك:
+ - استعراض فئات العمالة
+ - تسجيل معدلات الإنتاجية
+ - تحديث أسعار العمالة
+ - تكوين فرق العمل النموذجية
+
+## تحليل المخاطر
+
+### تقييم المخاطر
+
+1. انقر على "تحليل المخاطر" من شريط القوائم
+2. اختر المناقصة المراد تقييم مخاطرها
+3. يعرض النظام المخاطر المحددة مصنفة إلى:
+ - مخاطر تعاقدية
+ - مخاطر مالية
+ - مخاطر فنية
+ - مخاطر لوجستية
+
+
+
+### إدارة المخاطر
+
+لكل خطر محدد، يمكنك:
+
+1. مراجعة تفاصيل الخطر
+2. تعديل تقييم احتمالية الحدوث والتأثير
+3. إضافة إجراءات التخفيف
+4. تعيين مسؤول المتابعة
+5. تحديد تكلفة التخفيف
+
+## إدارة المشاريع المرساة
+
+### متابعة المشاريع
+
+1. انقر على "المشاريع" من شريط القوائم
+2. اختر المشروع المراد متابعته
+3. يعرض النظام:
+ - ملخص المشروع
+ - حالة التنفيذ
+ - المستخلصات
+ - المراسلات
+
+
+
+### إدارة المستخلصات
+
+1. من صفحة تفاصيل المشروع، انقر على "المستخلصات"
+2. يمكنك:
+ - إنشاء مستخلص جديد
+ - متابعة حالة المستخلصات
+ - الاطلاع على المدفوعات
+
+## الخرائط والمواقع
+
+### نظرة عامة
+
+**جديد**: نظام الخرائط التفاعلي يتيح لك:
+
+1. عرض مواقع جميع المشاريع على خريطة واحدة
+2. تصفية المشاريع حسب الحالة والنوع والمنطقة
+3. عرض معلومات تفصيلية عن كل موقع
+4. تحليل التوزيع الجغرافي للمشاريع
+5. حساب المسافات واتجاهات السير إلى المواقع
+
+### استخدام الخرائط
+
+1. انقر على "الخرائط" من شريط القوائم
+2. استخدم أدوات التصفية لعرض المشاريع المطلوبة
+3. انقر على أي علامة موقع لعرض تفاصيل المشروع
+4. استخدم خيار "تفاصيل الوصول" لعرض معلومات الوصول إلى الموقع
+5. يمكنك تصدير معلومات الموقع أو مشاركتها مع فريق العمل
+
+## الإشعارات الذكية
+
+### نظرة عامة
+
+**جديد**: نظام الإشعارات الذكية يقوم بتنبيهك تلقائيًا بشأن:
+
+1. المواعيد النهائية للمناقصات
+2. تحديثات حالة المناقصات والمشاريع
+3. المهام المستحقة
+4. تغييرات الأسعار في المواد الرئيسية
+5. الفرص الجديدة المحتملة
+
+### إعدادات الإشعارات
+
+1. انقر على "الإعدادات" ثم "إعدادات الإشعارات"
+2. خصص أنواع الإشعارات التي ترغب في تلقيها
+3. حدد طريقة التنبيه (داخل النظام، بريد إلكتروني، رسائل نصية)
+4. ضبط مستوى الأهمية والتكرار
+
+## الجدول الزمني التفاعلي
+
+### نظرة عامة
+
+**جديد**: الجدول الزمني التفاعلي يتيح لك:
+
+1. عرض مراحل المشروع بتنسيق رسومي سهل الفهم
+2. تتبع المراحل والإنجازات الرئيسية
+3. تحديث حالة المهام في الوقت الفعلي
+4. توقع المشكلات المحتملة قبل حدوثها
+5. مشاهدة تأثير التأخيرات على الخطة الزمنية الكلية
+
+### استخدام الجدول الزمني
+
+1. انقر على "الجدول الزمني" من شريط القوائم
+2. اختر المشروع المراد عرض جدوله الزمني
+3. استعرض المراحل والمهام
+4. انقر على أي مرحلة لعرض التفاصيل أو تحديث الحالة
+5. استخدم ميزة "ماذا لو" لتقييم تأثير التغييرات المحتملة
+
+## مساعد الذكاء الاصطناعي
+
+### نظرة عامة
+
+**جديد**: مساعد الذكاء الاصطناعي التفاعلي يمكنه:
+
+1. الإجابة على الأسئلة حول المناقصات والعقود
+2. توفير تحليلات سريعة للمستندات
+3. اقتراح حلول للمشكلات الشائعة
+4. مساعدتك في فهم البنود القانونية المعقدة
+5. توفير ملخصات دقيقة للمستندات الطويلة
+
+### استخدام المساعد
+
+1. انقر على رمز المساعد في أي صفحة من صفحات النظام
+2. اكتب سؤالك أو طلبك بلغة طبيعية
+3. يمكنك تحميل مستند للتحليل أو الإشارة إلى مستند موجود
+4. راجع الإجابة واطرح أسئلة متابعة إذا لزم الأمر
+
+## مقارنة المستندات
+
+### نظرة عامة
+
+**جديد**: أدوات مقارنة المستندات المتطورة تتيح لك:
+
+1. مقارنة نسخ مختلفة من العقود أو المناقصات
+2. تحديد التغييرات بين المستندات بدقة
+3. تقييم تأثير التعديلات على المخاطر والتكاليف
+4. اكتشاف التناقضات بين البنود المختلفة
+5. مقارنة العقود بالنماذج القياسية
+
+### استخدام أدوات المقارنة
+
+1. انقر على "مقارنة المستندات" من شريط القوائم
+2. حدد المستندين المراد مقارنتهما
+3. اختر نوع المقارنة (نصية، هيكلية، دلالية)
+4. راجع نتائج المقارنة مع تمييز الاختلافات
+5. يمكنك تصدير تقرير المقارنة
+
+## التقارير والتحليلات
+
+### إنشاء التقارير
+
+1. انقر على "التقارير" من شريط القوائم
+2. اختر نوع التقرير:
+ - تقرير المناقصات
+ - تقرير المشاريع
+ - تقرير مالي
+ - تقرير المخاطر
+ - **جديد**: تقرير المحتوى المحلي
+ - **جديد**: تقرير توقعات الفرص المستقبلية
+3. حدد معايير التقرير
+4. انقر على "إنشاء التقرير"
+
+
+
+### تصدير التقارير
+
+يمكن تصدير التقارير بصيغ متعددة:
+- PDF
+- Excel
+- Word
+- PowerPoint
+- **جديد**: صيغة JSON للتكامل مع الأنظمة الأخرى
+
+## إعدادات النظام
+
+### نظرة عامة
+
+**جديد**: صفحة الإعدادات المحسنة تتيح لك:
+
+1. تخصيص واجهة المستخدم
+2. تغيير لغة النظام
+3. إدارة إعدادات الإشعارات
+4. تكوين عمليات النسخ الاحتياطي التلقائية
+5. إدارة حسابات المستخدمين والصلاحيات
+
+### الإعدادات الشخصية
+
+1. انقر على "الإعدادات" ثم "الإعدادات الشخصية"
+2. اختر لغة النظام (العربية، الإنجليزية)
+3. خصص الواجهة (الألوان، الخط، ترتيب العناصر)
+4. ضبط إعدادات الإشعارات الشخصية
+
+### إدارة المستخدمين
+
+1. انقر على "الإعدادات" ثم "إدارة المستخدمين" (للمديرين فقط)
+2. استعرض قائمة المستخدمين
+3. أضف مستخدمًا جديدًا أو عدل بيانات مستخدم موجود
+4. حدد صلاحيات الوصول والأدوار
+
+## الأسئلة الشائعة
+
+### أسئلة عامة
+
+**س: كيف يمكنني الحصول على حساب للنظام؟**
+ج: يرجى التواصل مع مدير النظام في شركتك.
+
+**س: هل يمكن استخدام النظام عبر الأجهزة المحمولة؟**
+ج: نعم، النظام متوافق مع جميع الأجهزة بما فيها الهواتف الذكية والأجهزة اللوحية.
+
+**س: هل يمكنني استخدام النظام دون اتصال بالإنترنت؟**
+ج: بعض الوظائف متاحة دون اتصال، لكن معظم الميزات تتطلب اتصالًا بالإنترنت.
+
+### أسئلة عن تحليل المستندات
+
+**س: ما هي أنواع المستندات التي يدعمها النظام؟**
+ج: يدعم النظام مستندات PDF وWord وExcel والمخططات DWG.
+
+**س: هل يستطيع النظام تحليل المستندات الممسوحة ضوئياً؟**
+ج: نعم، يمكن للنظام تحليل المستندات الممسوحة ضوئياً، لكن دقة التحليل تعتمد على جودة المسح.
+
+**س: كم من الوقت يستغرق تحليل مستند كبير؟**
+ج: يعتمد على حجم وتعقيد المستند، لكن معظم المستندات تحلل في غضون دقائق.
+
+### أسئلة عن التسعير وحاسبة التكاليف
+
+**س: كيف يحدد النظام تكاليف المواد والعمالة؟**
+ج: يعتمد النظام على قاعدة بيانات الأسعار المتاحة ومعدلات الأداء المسجلة.
+
+**س: ما هو التسعير غير المتزن؟**
+ج: هو استراتيجية لتوزيع التكاليف بشكل غير متساوٍ على بنود المناقصة لتحقيق ميزة تنافسية أو تحسين التدفق النقدي.
+
+**س: هل يمكن إضافة عناصر مخصصة لحاسبة تكاليف البناء؟**
+ج: نعم، يمكنك إضافة عناصر مخصصة وتعديل المعلمات حسب متطلبات المشروع.
+
+## استكشاف الأخطاء وإصلاحها
+
+### مشاكل تسجيل الدخول
+
+**المشكلة: لا يمكن تسجيل الدخول**
+الحل:
+1. تأكد من صحة اسم المستخدم وكلمة المرور
+2. تأكد من اتصالك بالإنترنت
+3. امسح ذاكرة التخزين المؤقت للمتصفح
+4. إذا استمرت المشكلة، تواصل مع الدعم الفني
+
+### مشاكل تحليل المستندات
+
+**المشكلة: فشل تحليل المستند**
+الحل:
+1. تأكد من أن المستند بتنسيق مدعوم
+2. تحقق من جودة المسح إذا كان المستند ممسوحاً ضوئياً
+3. قلل حجم الملف إذا كان كبيراً جداً
+4. جرب تقسيم المستند إلى أجزاء أصغر
+
+### مشاكل التسعير
+
+**المشكلة: عدم ظهور التكاليف المقدرة**
+الحل:
+1. تأكد من تحديث قاعدة بيانات الأسعار
+2. تحقق من صحة وحدات البنود
+3. تأكد من ربط البنود بالمواد والعمالة المناسبة
+4. أعد تشغيل عملية التسعير
+
+### مشاكل الجدول الزمني
+
+**المشكلة: عدم ظهور بعض المراحل في الجدول الزمني**
+الحل:
+1. تأكد من إضافة جميع المراحل في تفاصيل المشروع
+2. تحقق من تواريخ البدء والانتهاء
+3. تأكد من تسلسل المراحل المنطقي
+4. حاول تحديث الصفحة أو إعادة تحميلها
+
+---
+
+## حول النظام
+
+نظام تحليل العقود والمناقصات بالذكاء الاصطناعي هو منتج متطور تم تصميمه وتطويره خصيصًا لشركة شبه الجزيرة للمقاولات. يعمل النظام على تحسين كفاءة دراسة المناقصات وإدارة المشاريع من خلال الاستفادة من تقنيات الذكاء الاصطناعي وتحليل البيانات المتقدمة.
+
+**مزايا النظام الرئيسية:**
+- تحليل متعمق للمناقصات والعقود باللغة العربية
+- حاسبة تكاليف متكاملة مع تفاصيل دقيقة
+- أدوات متقدمة لإدارة المشاريع
+- تحليل المخاطر الآلي
+- الجدول الزمني التفاعلي
+- نظام الخرائط والمواقع
+- مساعد الذكاء الاصطناعي
+
+---
+
+لمزيد من المساعدة، يرجى التواصل مع:
+- البريد الإلكتروني: support@peninsula-contracting.com
+- رقم الهاتف: +966 123456789
+- نظام التذاكر: https://support.peninsula-contracting.com
\ No newline at end of file
diff --git a/fonts/Amiri-Bold.ttf b/fonts/Amiri-Bold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..74ac7f3ec33a42005d7c376b2a8ecc24be980bc5
--- /dev/null
+++ b/fonts/Amiri-Bold.ttf
@@ -0,0 +1,1814 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Page not found · GitHub · GitHub
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You signed in with another tab or window. Reload to refresh your session.
+ You signed out in another tab or window. Reload to refresh your session.
+ You switched accounts on another tab or window. Reload to refresh your session.
+
+ Dismiss alert
+
+
+
+
+
+
+ You signed in with another tab or window. Reload to refresh your session.
+ You signed out in another tab or window. Reload to refresh your session.
+ You switched accounts on another tab or window. Reload to refresh your session.
+
+ Dismiss alert
+
+
+
+
", unsafe_allow_html=True)
+ st.progress(progress, text=f"المستوى التالي: {next_level_points} نقطة")
+
+ # تقسيم الإنجازات إلى مجموعات
+ st.markdown("
الإنجازات المفتوحة
", unsafe_allow_html=True)
+
+ if not self.user_data['unlocked_achievements']:
+ st.info("لم تقم بفتح أي إنجازات حتى الآن. أكمل المهام للحصول على الإنجازات!")
+ else:
+ # عرض الإنجازات المفتوحة بتنسيق الشبكة
+ cols = st.columns(3)
+ for i, achievement in enumerate(self.user_data['unlocked_achievements']):
+ with cols[i % 3]:
+ self._render_achievement_card(achievement, is_unlocked=True)
+
+ # عرض الإنجازات قيد التقدم
+ st.markdown("
الإنجازات قيد التقدم
", unsafe_allow_html=True)
+
+ if not self.user_data['in_progress_achievements']:
+ st.info("ليس لديك أي إنجازات قيد التقدم حالياً.")
+ else:
+ # عرض الإنجازات قيد التقدم
+ for achievement in self.user_data['in_progress_achievements']:
+ self._render_progress_achievement(achievement)
+
+ # عرض الإنجازات المتاحة
+ st.markdown("
الإنجازات المتاحة
", unsafe_allow_html=True)
+
+ # فلترة الإنجازات غير المفتوحة وغير قيد التقدم
+ unlocked_ids = [a['id'] for a in self.user_data['unlocked_achievements']]
+ in_progress_ids = [a['id'] for a in self.user_data['in_progress_achievements']]
+ available_achievements = [a for a in self.achievements if a['id'] not in unlocked_ids and a['id'] not in in_progress_ids]
+
+ if not available_achievements:
+ st.success("رائع! لقد حققت جميع الإنجازات المتاحة.")
+ else:
+ # تقسيم الإنجازات المتاحة حسب الفئات
+ categories = sorted(set(a['category'] for a in available_achievements))
+ for category in categories:
+ st.markdown(f"
+ نظام الإنجازات يحفزك على إكمال المهام وتحقيق أهداف المشروع من خلال مكافآت
+ وإنجازات قابلة للفتح. اكتسب النقاط وارتقِ بمستواك وافتح إنجازات جديدة كلما تقدمت في استخدام نظام تحليل المناقصات.
+
+ """, unsafe_allow_html=True)
+
+ # عرض صندوق معلومات عند تشغيل الوحدة لأول مرة
+ if not self.user_data['unlocked_achievements'] and not self.user_data['in_progress_achievements']:
+ st.info("""
+ 👋 مرحباً بك في نظام الإنجازات!
+
+ استكشف الإنجازات المتاحة وابدأ في تحقيقها عن طريق إكمال المهام في أنحاء النظام المختلفة.
+ كلما حققت المزيد من الإنجازات، حصلت على نقاط أكثر وارتقيت في المستويات.
+
+ ابدأ الآن بإنشاء مشروع جديد أو تحليل مستند!
+ """)
+
+ # إنشاء علامات تبويب لعرض محتوى مختلف
+ tab1, tab2, tab3 = st.tabs(["الإنجازات", "المستويات والمكافآت", "الإحصائيات"])
+
+ with tab1:
+ self.render_achievements_tab()
+
+ with tab2:
+ st.markdown("
المستويات والمكافآت
", unsafe_allow_html=True)
+
+ # عرض معلومات عن نظام المستويات
+ st.markdown("""
+
+
نظام المستويات يعتمد على النقاط التي تكتسبها من إنجاز المهام وفتح الإنجازات:
+
+
المستوى 1: 0 - 999 نقطة
+
المستوى 2: 1000 - 1999 نقطة
+
المستوى 3: 2000 - 2999 نقطة
+
وهكذا...
+
+
كلما ارتقيت في المستويات، تفتح مكافآت وميزات جديدة في النظام!
+
+ """, unsafe_allow_html=True)
+
+ # عرض قائمة المكافآت
+ st.markdown("
+ """, unsafe_allow_html=True)
+
+ with tab3:
+ st.markdown("
إحصائيات الإنجازات
", unsafe_allow_html=True)
+
+ # إعداد بيانات للرسم البياني
+ categories = {}
+ for achievement in self.achievements:
+ category = achievement['category']
+ if category not in categories:
+ categories[category] = {"total": 0, "unlocked": 0}
+ categories[category]["total"] += 1
+
+ # حساب الإنجازات المفتوحة لكل فئة
+ for achievement in self.user_data['unlocked_achievements']:
+ category = achievement['category']
+ if category in categories:
+ categories[category]["unlocked"] += 1
+
+ # تحويل البيانات إلى DataFrame
+ df = pd.DataFrame([
+ {
+ "الفئة": category,
+ "المفتوحة": data["unlocked"],
+ "الإجمالي": data["total"],
+ "النسبة": round((data["unlocked"] / data["total"]) * 100 if data["total"] > 0 else 0)
+ }
+ for category, data in categories.items()
+ ])
+
+ # عرض البيانات في جدول
+ st.dataframe(
+ df,
+ column_config={
+ "النسبة": st.column_config.ProgressColumn(
+ "نسبة الإنجاز",
+ format="%d%%",
+ min_value=0,
+ max_value=100
+ )
+ },
+ hide_index=True
+ )
+
+ # عرض معلومات إضافية
+ col1, col2, col3 = st.columns(3)
+ with col1:
+ total_points_possible = sum(a['points'] for a in self.achievements)
+ st.metric(
+ "إجمالي النقاط المحتملة",
+ f"{total_points_possible}",
+ f"{int((self.user_data['total_points'] / total_points_possible) * 100)}%"
+ )
+
+ with col2:
+ days_since_first = 0
+ if self.user_data['unlocked_achievements']:
+ first_date = min([
+ datetime.strptime(a.get('unlocked_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S')), '%Y-%m-%d %H:%M:%S')
+ for a in self.user_data['unlocked_achievements']
+ ])
+ days_since_first = (datetime.now() - first_date).days
+
+ st.metric("أيام النشاط", f"{days_since_first}")
+
+ with col3:
+ if self.user_data['unlocked_achievements']:
+ achievements_per_day = round(len(self.user_data['unlocked_achievements']) / max(1, days_since_first), 2)
+ st.metric("معدل الإنجازات اليومي", f"{achievements_per_day}")
+ else:
+ st.metric("معدل الإنجازات اليومي", "0")
+
+ # إضافة CSS مخصص للصفحة
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
\ No newline at end of file
diff --git a/modules/achievements/achievements_app.py b/modules/achievements/achievements_app.py
new file mode 100644
index 0000000000000000000000000000000000000000..908c740742312d5b2f9e45f4c13334b3c436500d
--- /dev/null
+++ b/modules/achievements/achievements_app.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+وحدة تطبيق نظام الإنجازات المحفز لمراحل المشروع
+"""
+
+import os
+import sys
+import streamlit as st
+import pandas as pd
+import numpy as np
+import time
+from datetime import datetime, timedelta
+
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
+
+# استيراد مكونات نظام الإنجازات
+from modules.achievements.achievement_system import AchievementSystem
+
+
+class AchievementsApp:
+ """وحدة تطبيق نظام الإنجازات المحفز لمراحل المشروع"""
+
+ def __init__(self, user_id=None):
+ """تهيئة وحدة تطبيق نظام الإنجازات المحفز"""
+ self.achievement_system = AchievementSystem(user_id)
+
+ def render(self):
+ """عرض واجهة وحدة تطبيق نظام الإنجازات المحفز"""
+ self.achievement_system.render()
+
+ def render_dashboard_summary(self):
+ """عرض ملخص الإنجازات في لوحة التحكم"""
+ self.achievement_system.render_achievements_summary()
+
+
+# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
+if __name__ == "__main__":
+ st.set_page_config(
+ page_title="نظام الإنجازات المحفز | WAHBi AI",
+ page_icon="🏆",
+ layout="wide",
+ initial_sidebar_state="expanded"
+ )
+
+ app = AchievementsApp()
+ app.render()
\ No newline at end of file
diff --git a/modules/ai_assistant/__init__.py b/modules/ai_assistant/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..d6e1f9b02e95d00ffe620203226b5ec04b5495a6
--- /dev/null
+++ b/modules/ai_assistant/__init__.py
@@ -0,0 +1,5 @@
+"""
+وحدة المساعد الذكي
+"""
+
+__version__ = '1.0.0'
\ No newline at end of file
diff --git a/modules/ai_assistant/ai_assistant.py b/modules/ai_assistant/ai_assistant.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ac352b74035d2f4360a95e1fb1913eef4b154c7
--- /dev/null
+++ b/modules/ai_assistant/ai_assistant.py
@@ -0,0 +1,773 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+وحدة المساعد الذكي التفاعلية
+تتيح للمستخدمين التفاعل مع نماذج الذكاء الاصطناعي المتقدمة للحصول على مساعدة في تحليل العقود والمناقصات
+"""
+
+import os
+import sys
+import json
+import re
+import time
+import base64
+import tempfile
+import logging
+from datetime import datetime
+import streamlit as st
+import pandas as pd
+import numpy as np
+import requests
+from io import BytesIO
+from PIL import Image
+import openai
+import plotly.express as px
+import plotly.graph_objects as go
+
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
+
+# استيراد المكونات المساعدة
+from utils.helpers import create_directory_if_not_exists, format_time, get_user_info, render_credits, load_css
+
+
+class AIAssistant:
+ """فئة المساعد الذكي التفاعلية"""
+
+ def __init__(self):
+ """تهيئة المساعد الذكي"""
+ self.conversations_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'assistant_conversations')
+ create_directory_if_not_exists(self.conversations_dir)
+
+ # تهيئة مفتاح OpenAI API
+ self.openai_api_key = os.environ.get("OPENAI_API_KEY")
+ if self.openai_api_key:
+ openai.api_key = self.openai_api_key
+ self.is_api_available = True
+ else:
+ self.is_api_available = False
+
+ # نموذج OpenAI المستخدم
+ self.model = "gpt-4o" # النموذج الأحدث من OpenAI
+
+ # تهيئة حالة المحادثة في الجلسة
+ if "assistant_messages" not in st.session_state:
+ st.session_state.assistant_messages = []
+
+ if "assistant_mode" not in st.session_state:
+ st.session_state.assistant_mode = "general"
+
+ if "document_context" not in st.session_state:
+ st.session_state.document_context = None
+
+ # الأنماط المتاحة للمساعد
+ self.assistant_modes = {
+ "general": "مساعد عام",
+ "contract_analysis": "تحليل العقود",
+ "cost_estimation": "تقدير التكاليف",
+ "risk_assessment": "تقييم المخاطر",
+ "project_planning": "تخطيط المشاريع"
+ }
+
+ # توجيهات النظام للمساعد
+ self.system_prompts = {
+ "general": """
+ أنت مساعد ذكي متخصص في شركة شبه الجزيرة للمقاولات، وتعمل ضمن نظام WAHBi لتحليل العقود والمناقصات.
+ دورك هو مساعدة المستخدمين في:
+ 1. تحليل المستندات والعقود، وتوضيح بنود العقود وفهم الالتزامات والشروط.
+ 2. المساعدة في تسعير المشاريع وحساب التكاليف والموارد.
+ 3. تقييم مخاطر العقود والمشاريع والمساعدة في اتخاذ القرارات.
+ 4. المساعدة في إدارة المشاريع ومتابعة الإنجاز.
+
+ استخدم لغة مهنية واضحة ومباشرة. قدم إجابات دقيقة ومختصرة.
+ عند قيام المستخدم بسؤال عن كيفية استخدام النظام، قم بإرشاده إلى الوحدة المناسبة في النظام.
+
+ معلومات هامة عن وحدات النظام:
+ - وحدة تحليل المستندات: لتحليل العقود والمناقصات باستخدام الذكاء الاصطناعي.
+ - وحدة مقارنة المستندات: لمقارنة نسخ مختلفة من المستندات وتحديد التغييرات.
+ - وحدة التسعير المتكاملة: لحساب تكاليف المشاريع بناءً على الموارد والمواد والعمالة.
+ - وحدة تقييم مخاطر العقود: لتحليل وتقييم المخاطر المحتملة في العقود والمشاريع.
+ - وحدة متتبع حالة المشروع: لمتابعة تقدم المشاريع وعرض مؤشرات الأداء.
+ - وحدة خريطة المشاريع: لعرض مواقع المشاريع على الخريطة بشكل تفاعلي.
+ - وحدة الإشعارات الذكية: لإرسال تنبيهات وإشعارات للمستخدمين حول المشاريع.
+
+ تذكر أن تكون مفيداً ودقيقاً ومهنياً في جميع إجاباتك.
+ """,
+
+ "contract_analysis": """
+ أنت محلل عقود متخصص في تحليل العقود والمناقصات لشركات المقاولات.
+ مهمتك هي تحليل العقود وتحديد:
+ - الالتزامات الرئيسية
+ - المواعيد النهائية والتسليمات
+ - الشروط الجزائية والغرامات
+ - آلية الدفع والمستحقات المالية
+ - الشروط الخاصة والاستثناءات
+ - المخاطر المحتملة وكيفية التخفيف منها
+
+ عند تحليل عقد، قم بتوضيح البنود غير المواتية التي قد تسبب مشاكل مستقبلية.
+ استخدم لغة قانونية دقيقة مع شرح المصطلحات القانونية بلغة مبسطة.
+ قدم توصيات عملية لكيفية التعامل مع بنود العقد وتجنب المخاطر.
+ """,
+
+ "cost_estimation": """
+ أنت خبير في تقدير تكاليف مشاريع البناء والمقاولات.
+ مهمتك هي مساعدة المستخدم في:
+ - تقدير تكاليف المشاريع بناءً على وصف المشروع ومتطلباته
+ - حساب تكاليف المواد والعمالة والمعدات والنفقات العامة
+ - توضيح كيفية تخصيص الميزانية بين مختلف عناصر المشروع
+ - تحديد التكاليف غير المباشرة التي قد يغفل عنها المستخدم
+ - اقتراح طرق لتقليل التكاليف دون التأثير على جودة المشروع
+
+ استخدم أسلوب منهجي في تقدير التكاليف واشرح افتراضاتك بوضوح.
+ قدم نطاقات تقديرية بدلاً من أرقام دقيقة للتكاليف حيثما كان ذلك مناسباً.
+ عند الإشارة إلى تكاليف، وضح ما إذا كانت التكاليف تشمل ضريبة القيمة المضافة أم لا.
+ """,
+
+ "risk_assessment": """
+ أنت خبير في تقييم مخاطر مشاريع البناء والمقاولات.
+ مهمتك هي مساعدة المستخدم في:
+ - تحديد المخاطر المحتملة في المشاريع والعقود
+ - تقييم احتمالية وتأثير كل خطر
+ - اقتراح استراتيجيات للتخفيف من المخاطر
+ - تحليل السيناريوهات المحتملة وخطط الطوارئ
+ - تقديم أفضل الممارسات لإدارة المخاطر في مشاريع المقاولات
+
+ صنف المخاطر إلى فئات (عالية، متوسطة، منخفضة) بناءً على احتماليتها وتأثيرها.
+ اشرح كيف يمكن للشركة أن تحول بعض المخاطر إلى فرص.
+ قدم أمثلة عملية من مشاريع مماثلة لتوضيح كيفية إدارة المخاطر المحددة.
+ """,
+
+ "project_planning": """
+ أنت خبير في تخطيط وإدارة مشاريع البناء والمقاولات.
+ مهمتك هي مساعدة المستخدم في:
+ - تخطيط المشاريع وتقسيمها إلى مراحل ومهام
+ - تحديد الموارد اللازمة والجداول الزمنية
+ - إنشاء مخطط جانت وتحديد المسار الحرج
+ - التخطيط للموارد البشرية والمعدات والمواد
+ - متابعة تقدم المشروع ومؤشرات الأداء
+
+ قدم نصائح عملية لإدارة المشاريع بكفاءة وتجنب التأخيرات.
+ اشرح كيفية التعامل مع التغييرات والمطالبات خلال تنفيذ المشروع.
+ قدم أفضل الممارسات للتواصل مع أصحاب المصلحة وإدارة التوقعات.
+ """
+ }
+
+ def _call_openai_api(self, messages, model=None, max_tokens=2000):
+ """استدعاء OpenAI API للحصول على استجابة"""
+ if not self.is_api_available:
+ return {
+ "choices": [{"message": {"content": "عذراً، مفتاح OpenAI API غير متوفر. يرجى التواصل مع مسؤول النظام."}}]
+ }
+
+ try:
+ if model is None:
+ model = self.model
+
+ response = openai.ChatCompletion.create(
+ model=model,
+ messages=messages,
+ max_tokens=max_tokens,
+ temperature=0.7,
+ top_p=0.9,
+ frequency_penalty=0,
+ presence_penalty=0
+ )
+
+ return response
+ except Exception as e:
+ logging.error(f"خطأ في استدعاء OpenAI API: {e}")
+ return {
+ "choices": [{"message": {"content": f"عذراً، حدث خطأ في الاتصال بـ OpenAI API: {str(e)}"}}]
+ }
+
+ def _call_backend_api(self, endpoint, data):
+ """استدعاء واجهة API الخلفية للنظام"""
+ try:
+ response = requests.post(
+ f"http://localhost:5000/api/{endpoint}",
+ json=data,
+ timeout=60
+ )
+
+ if response.status_code == 200:
+ return response.json()
+ else:
+ logging.error(f"خطأ في استدعاء واجهة API الخلفية: {response.status_code} - {response.text}")
+ return {"error": f"خطأ في استدعاء واجهة API الخلفية: {response.status_code}"}
+ except Exception as e:
+ logging.error(f"خطأ في الاتصال بواجهة API الخلفية: {e}")
+ return {"error": f"خطأ في الاتصال بواجهة API الخلفية: {str(e)}"}
+
+ def _process_user_message(self, user_message, mode=None):
+ """معالجة رسالة المستخدم والحصول على رد من المساعد الذكي"""
+ if mode is None:
+ mode = st.session_state.assistant_mode
+
+ # إنشاء قائمة الرسائل للمحادثة
+ messages = [
+ {"role": "system", "content": self.system_prompts[mode]}
+ ]
+
+ # إضافة سياق المستند إذا كان متاحاً
+ if st.session_state.document_context:
+ messages.append({
+ "role": "system",
+ "content": f"معلومات سياقية عن المستند: {st.session_state.document_context}"
+ })
+
+ # إضافة المحادثة السابقة
+ for msg in st.session_state.assistant_messages:
+ messages.append({
+ "role": msg["role"],
+ "content": msg["content"]
+ })
+
+ # إضافة رسالة المستخدم الحالية
+ messages.append({
+ "role": "user",
+ "content": user_message
+ })
+
+ # استدعاء API
+ response = self._call_openai_api(messages)
+
+ # استخراج الرد
+ assistant_response = response["choices"][0]["message"]["content"]
+
+ # تحديث سجل المحادثة
+ st.session_state.assistant_messages.append({"role": "user", "content": user_message})
+ st.session_state.assistant_messages.append({"role": "assistant", "content": assistant_response})
+
+ return assistant_response
+
+ def _clear_chat(self):
+ """مسح المحادثة الحالية"""
+ st.session_state.assistant_messages = []
+ st.session_state.document_context = None
+
+ def _save_conversation(self):
+ """حفظ المحادثة الحالية"""
+ if not st.session_state.assistant_messages:
+ st.warning("لا توجد محادثة لحفظها.")
+ return False
+
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
+ user_info = get_user_info()
+
+ conversation_data = {
+ "timestamp": timestamp,
+ "user": user_info["username"],
+ "mode": st.session_state.assistant_mode,
+ "messages": st.session_state.assistant_messages,
+ "document_context": st.session_state.document_context
+ }
+
+ filename = f"conversation_{user_info['username']}_{timestamp}.json"
+ file_path = os.path.join(self.conversations_dir, filename)
+
+ try:
+ with open(file_path, 'w', encoding='utf-8') as f:
+ json.dump(conversation_data, f, ensure_ascii=False, indent=2)
+
+ return True
+ except Exception as e:
+ logging.error(f"خطأ في حفظ المحادثة: {e}")
+ return False
+
+ def _load_conversation(self, filename):
+ """تحميل محادثة محفوظة"""
+ file_path = os.path.join(self.conversations_dir, filename)
+
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ conversation_data = json.load(f)
+
+ st.session_state.assistant_messages = conversation_data["messages"]
+ st.session_state.assistant_mode = conversation_data["mode"]
+ st.session_state.document_context = conversation_data.get("document_context")
+
+ return True
+ except Exception as e:
+ logging.error(f"خطأ في تحميل المحادثة: {e}")
+ return False
+
+ def _get_saved_conversations(self):
+ """الحصول على قائمة المحادثات المحفوظة"""
+ conversations = []
+
+ try:
+ for filename in os.listdir(self.conversations_dir):
+ if filename.endswith(".json") and filename.startswith("conversation_"):
+ file_path = os.path.join(self.conversations_dir, filename)
+
+ with open(file_path, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+
+ conversations.append({
+ "filename": filename,
+ "timestamp": data.get("timestamp", ""),
+ "user": data.get("user", ""),
+ "mode": data.get("mode", "general"),
+ "message_count": len(data.get("messages", []))
+ })
+ except Exception as e:
+ logging.error(f"خطأ في قراءة المحادثات المحفوظة: {e}")
+
+ # ترتيب المحادثات حسب التاريخ (الأحدث أولاً)
+ conversations.sort(key=lambda x: x["timestamp"], reverse=True)
+
+ return conversations
+
+ def render_chat_interface(self):
+ """عرض واجهة المحادثة الرئيسية"""
+ st.markdown("
المساعد الذكي
", unsafe_allow_html=True)
+
+ # التحقق من توفر OpenAI API
+ if not self.is_api_available:
+ st.warning("⚠️ مفتاح OpenAI API غير متوفر. لن يكون المساعد الذكي قادراً على الرد. يرجى التواصل مع مسؤول النظام.")
+
+ # إضافة CSS
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+ # عرض أوضاع المساعد
+ st.markdown("#### اختر وضع المساعد الذكي")
+
+ col1, col2, col3, col4, col5 = st.columns(5)
+
+ with col1:
+ if st.button("مساعد عام", key="mode_general",
+ help="مساعد عام للإجابة على الأسئلة المتعلقة بالعقود والمناقصات"):
+ st.session_state.assistant_mode = "general"
+ st.rerun()
+
+ with col2:
+ if st.button("تحليل العقود", key="mode_contract_analysis",
+ help="متخصص في تحليل العقود وتحديد البنود والشروط والمخاطر"):
+ st.session_state.assistant_mode = "contract_analysis"
+ st.rerun()
+
+ with col3:
+ if st.button("تقدير التكاليف", key="mode_cost_estimation",
+ help="متخصص في تقدير تكاليف المشاريع والبنود"):
+ st.session_state.assistant_mode = "cost_estimation"
+ st.rerun()
+
+ with col4:
+ if st.button("تقييم المخاطر", key="mode_risk_assessment",
+ help="متخصص في تحديد وتقييم المخاطر المحتملة في المشاريع والعقود"):
+ st.session_state.assistant_mode = "risk_assessment"
+ st.rerun()
+
+ with col5:
+ if st.button("تخطيط المشاريع", key="mode_project_planning",
+ help="متخصص في تخطيط وإدارة المشاريع وتحديد المراحل والموارد"):
+ st.session_state.assistant_mode = "project_planning"
+ st.rerun()
+
+ st.markdown(f"**الوضع الحالي:** {self.assistant_modes[st.session_state.assistant_mode]}")
+
+ # تحميل سياق من مستند (اختياري)
+ st.markdown("---")
+ with st.expander("إضافة سياق من مستند", expanded=False):
+ context_text = st.text_area(
+ "نص المستند (اختياري)",
+ value=st.session_state.document_context if st.session_state.document_context else "",
+ height=150,
+ help="أضف نص المستند هنا ليتم استخدامه كسياق للمحادثة"
+ )
+
+ uploaded_file = st.file_uploader(
+ "أو قم بتحميل ملف نصي أو PDF",
+ type=["txt", "pdf"],
+ help="يمكنك تحميل ملف نصي أو PDF ليتم استخدامه كسياق للمحادثة"
+ )
+
+ doc_col1, doc_col2 = st.columns(2)
+
+ with doc_col1:
+ if st.button("إضافة السياق", disabled=not context_text and not uploaded_file):
+ if uploaded_file:
+ try:
+ # قراءة الملف المرفوع
+ if uploaded_file.name.endswith(".pdf"):
+ import PyPDF2
+ reader = PyPDF2.PdfReader(uploaded_file)
+ context = ""
+ for page in reader.pages:
+ context += page.extract_text() + "\n"
+ else:
+ context = uploaded_file.read().decode("utf-8")
+
+ st.session_state.document_context = context
+ st.success("تم إضافة نص المستند كسياق للمحادثة بنجاح.")
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء قراءة الملف: {str(e)}")
+ elif context_text:
+ st.session_state.document_context = context_text
+ st.success("تم إضافة نص المستند كسياق للمحادثة بنجاح.")
+
+ with doc_col2:
+ if st.button("مسح السياق", disabled=not st.session_state.document_context):
+ st.session_state.document_context = None
+ st.success("تم مسح سياق المستند بنجاح.")
+
+ # عرض المحادثة
+ st.markdown("---")
+ st.markdown("#### المحادثة مع المساعد الذكي")
+
+ # عرض رسائل المحادثة
+ chat_container = st.container()
+
+ with chat_container:
+ with st.container():
+ if not st.session_state.assistant_messages:
+ st.markdown("""
+
+
مرحباً بك في المساعد الذكي!
+
يمكنك البدء بطرح سؤال أو طلب مساعدة.
+
+ """, unsafe_allow_html=True)
+ else:
+ message_html = ""
+
+ for msg in st.session_state.assistant_messages:
+ if msg["role"] == "user":
+ message_html += f"""
+
+
+
{msg["content"]}
+
+
أ
+
+ """
+ else:
+ message_html += f"""
+
+
W
+
+
{msg["content"]}
+
+
+ """
+
+ st.markdown(f"""
+
+ {message_html}
+
+ """, unsafe_allow_html=True)
+
+ # ادخال الرسالة
+ st.markdown("#### أدخل رسالتك")
+
+ with st.container():
+ with st.form(key="chat_form"):
+ user_message = st.text_area("رسالتك", height=100, placeholder="اكتب سؤالك أو طلبك هنا...")
+
+ col1, col2, col3 = st.columns([2, 2, 1])
+
+ with col1:
+ send_button = st.form_submit_button(
+ "إرسال",
+ help="إرسال الرسالة إلى المساعد الذكي"
+ )
+
+ with col2:
+ suggested_questions = [
+ "كيف يمكنني تحليل بنود الدفع في العقد؟",
+ "ما هي أفضل طريقة لتقدير تكاليف مشروع بناء؟",
+ "كيف أحدد المخاطر المحتملة في مشروع جديد؟",
+ "كيف يمكنني إنشاء جدول زمني فعال للمشروع؟",
+ "ما هي أهم البنود التي يجب الانتباه إليها في عقود المقاولات؟"
+ ]
+
+ if st.session_state.assistant_mode == "contract_analysis":
+ suggested_questions = [
+ "كيف أحدد البنود غير المواتية في العقد؟",
+ "ما هي العناصر الأساسية التي يجب أن يتضمنها عقد المقاولة؟",
+ "كيف أتعامل مع بنود الغرامات والتعويضات؟",
+ "كيف يمكنني التفاوض على تحسين شروط الدفع؟",
+ "ما هي الفروق الرئيسية بين عقد الثمن الثابت وعقد التكلفة زائد أتعاب؟"
+ ]
+ elif st.session_state.assistant_mode == "cost_estimation":
+ suggested_questions = [
+ "كيف أقدر تكلفة المواد في مشروع بناء؟",
+ "ما هي نسبة النفقات العامة المعقولة لمشروع مقاولات؟",
+ "كيف أحسب تكلفة العمالة بدقة؟",
+ "ما هي العوامل التي تؤثر على تكلفة المعدات؟",
+ "كيف أقدر هامش الربح المناسب للمشروع؟"
+ ]
+
+ selected_question = st.selectbox(
+ "أو اختر سؤال مقترح",
+ [""] + suggested_questions,
+ index=0
+ )
+
+ with col3:
+ clear_button = st.form_submit_button(
+ "مسح المحادثة",
+ help="مسح جميع الرسائل في المحادثة الحالية"
+ )
+
+ if send_button and user_message:
+ # معالجة رسالة المستخدم
+ with st.spinner("جاري معالجة الرسالة..."):
+ self._process_user_message(user_message)
+ st.rerun()
+
+ if send_button and selected_question and not user_message:
+ # استخدام السؤال المقترح
+ with st.spinner("جاري معالجة الرسالة..."):
+ self._process_user_message(selected_question)
+ st.rerun()
+
+ if clear_button:
+ self._clear_chat()
+ st.rerun()
+
+ # زر لحفظ المحادثة
+ col1, col2, col3 = st.columns([1, 1, 2])
+
+ with col1:
+ if st.button("حفظ المحادثة", key="save_conversation", disabled=not st.session_state.assistant_messages):
+ if self._save_conversation():
+ st.success("تم حفظ المحادثة بنجاح.")
+ else:
+ st.error("حدث خطأ أثناء حفظ المحادثة.")
+
+ with col2:
+ if st.button("تحميل محادثة سابقة", key="show_load_conversation"):
+ st.session_state.show_conversations = True
+ st.rerun()
+
+ # عرض المحادثات المحفوظة
+ if "show_conversations" in st.session_state and st.session_state.show_conversations:
+ st.markdown("---")
+ st.markdown("#### المحادثات المحفوظة")
+
+ conversations = self._get_saved_conversations()
+
+ if not conversations:
+ st.info("لا توجد محادثات محفوظة.")
+ else:
+ # عرض المحادثات في جدول
+ conversation_data = []
+ for conv in conversations:
+ timestamp = datetime.strptime(conv["timestamp"], "%Y%m%d%H%M%S") if conv["timestamp"] else ""
+ formatted_time = timestamp.strftime("%Y-%m-%d %H:%M:%S") if timestamp else ""
+
+ conversation_data.append({
+ "التاريخ": formatted_time,
+ "المستخدم": conv["user"],
+ "وضع المساعد": self.assistant_modes.get(conv["mode"], "غير معروف"),
+ "عدد الرسائل": conv["message_count"],
+ "الملف": conv["filename"]
+ })
+
+ df = pd.DataFrame(conversation_data)
+ st.dataframe(df, height=300)
+
+ # اختيار محادثة لتحميلها
+ selected_filename = st.selectbox(
+ "اختر محادثة لتحميلها",
+ options=[""] + [conv["filename"] for conv in conversations],
+ format_func=lambda x: next((f"{c['user']} - {datetime.strptime(c['timestamp'], '%Y%m%d%H%M%S').strftime('%Y-%m-%d %H:%M:%S')}" for c in conversations if c["filename"] == x), x),
+ index=0
+ )
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ if st.button("تحميل المحادثة المختارة", disabled=not selected_filename):
+ if self._load_conversation(selected_filename):
+ st.success("تم تحميل المحادثة بنجاح.")
+ st.session_state.show_conversations = False
+ st.rerun()
+ else:
+ st.error("حدث خطأ أثناء تحميل المحادثة.")
+
+ with col2:
+ if st.button("إلغاء", key="cancel_load_conversation"):
+ st.session_state.show_conversations = False
+ st.rerun()
+
+ # عرض المعلومات عن وضع المساعد الحالي
+ st.markdown("---")
+ st.markdown(f"#### معلومات عن وضع المساعد: {self.assistant_modes[st.session_state.assistant_mode]}")
+
+ if st.session_state.assistant_mode == "general":
+ st.markdown("""
+ المساعد العام يمكنه مساعدتك في مجموعة متنوعة من المهام المتعلقة بالعقود والمناقصات وإدارة المشاريع. يمكنه:
+ - الإجابة على الأسئلة العامة حول العقود والمناقصات
+ - توجيهك إلى الوحدات المناسبة في النظام
+ - تقديم معلومات عامة عن إدارة المشاريع وأفضل الممارسات
+ - المساعدة في فهم المصطلحات والمفاهيم المتعلقة بمجال المقاولات
+ """)
+ elif st.session_state.assistant_mode == "contract_analysis":
+ st.markdown("""
+ مساعد تحليل العقود متخصص في:
+ - تحليل بنود العقود وتوضيح معانيها
+ - تحديد الالتزامات والحقوق لكل طرف
+ - تسليط الضوء على البنود غير المواتية أو الغامضة
+ - تقديم توصيات للتفاوض على تحسين شروط العقد
+ - مقارنة العقد مع أفضل الممارسات في القطاع
+ """)
+ elif st.session_state.assistant_mode == "cost_estimation":
+ st.markdown("""
+ مساعد تقدير التكاليف متخصص في:
+ - حساب تكاليف المشاريع بناءً على المتطلبات والمواصفات
+ - تقدير تكاليف المواد والعمالة والمعدات
+ - تحليل التكاليف المباشرة وغير المباشرة
+ - تقديم نصائح لتقليل التكاليف وزيادة الكفاءة
+ - تحديد العوامل التي قد تؤثر على التكلفة الإجمالية
+ """)
+ elif st.session_state.assistant_mode == "risk_assessment":
+ st.markdown("""
+ مساعد تقييم المخاطر متخصص في:
+ - تحديد المخاطر المحتملة في المشاريع والعقود
+ - تقييم احتمالية وتأثير كل خطر
+ - اقتراح استراتيجيات للتخفيف من المخاطر
+ - إنشاء خطط للطوارئ والاستجابة للمخاطر
+ - تحليل تأثير المخاطر على الجدول الزمني والتكلفة
+ """)
+ elif st.session_state.assistant_mode == "project_planning":
+ st.markdown("""
+ مساعد تخطيط المشاريع متخصص في:
+ - تقسيم المشروع إلى مراحل ومهام وأنشطة
+ - تحديد الموارد اللازمة لكل نشاط
+ - إنشاء الجداول الزمنية والمسار الحرج
+ - التخطيط للموارد البشرية والمعدات والمواد
+ - مراقبة تقدم المشروع وإدارة التغييرات
+ """)
+
+ # عرض معلومات حقوق الملكية
+ render_credits()
+
+ def render(self):
+ """عرض واجهة المساعد الذكي الرئيسية"""
+ # تحميل CSS المخصص
+ load_css()
+
+ # عرض واجهة المحادثة
+ self.render_chat_interface()
+
+
+# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
+if __name__ == "__main__":
+ st.set_page_config(
+ page_title="المساعد الذكي | WAHBi AI",
+ page_icon="🤖",
+ layout="wide",
+ initial_sidebar_state="expanded"
+ )
+
+ assistant = AIAssistant()
+ assistant.render()
\ No newline at end of file
diff --git a/modules/ai_assistant/ai_assistant_app.py b/modules/ai_assistant/ai_assistant_app.py
new file mode 100644
index 0000000000000000000000000000000000000000..4f667ece6e9a58e33ef6805b933cd551aeb2d2be
--- /dev/null
+++ b/modules/ai_assistant/ai_assistant_app.py
@@ -0,0 +1,3175 @@
+# -*- coding: utf-8 -*-
+"""
+وحدة المساعد الذكي
+
+هذا الملف يحتوي على الفئة الرئيسية لتطبيق المساعد الذكي مع دعم نموذج Claude AI.
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import plotly.express as px
+import requests
+import json
+import time
+import base64
+import logging
+import os
+from datetime import datetime, timedelta
+import io
+import tempfile
+import random
+from io import BytesIO
+from tempfile import NamedTemporaryFile
+from PIL import Image
+
+# استيراد النماذج المطلوبة
+try:
+ from models.inference import (
+ load_cost_prediction_model,
+ load_document_classifier_model,
+ load_risk_assessment_model,
+ load_local_content_model,
+ load_entity_recognition_model
+ )
+except ImportError:
+ # إنشاء دوال وهمية في حال عدم توفر النماذج
+ def load_cost_prediction_model():
+ return None
+
+ def load_document_classifier_model():
+ return None
+
+ def load_risk_assessment_model():
+ return None
+
+ def load_local_content_model():
+ return None
+
+ def load_entity_recognition_model():
+ return None
+
+try:
+ # استيراد مكتبة pdf2image للتعامل مع ملفات PDF
+ from pdf2image import convert_from_path
+ pdf_conversion_available = True
+except ImportError:
+ pdf_conversion_available = False
+ logging.warning("لم يتم العثور على مكتبة pdf2image. لن يمكن تحويل ملفات PDF إلى صور.")
+
+
+class ClaudeAIService:
+ """
+ فئة خدمة Claude AI للتحليل الذكي
+ """
+ def __init__(self):
+ """تهيئة خدمة Claude AI"""
+ self.api_url = "https://api.anthropic.com/v1/messages"
+
+ def get_api_key(self):
+ """الحصول على مفتاح API من متغيرات البيئة"""
+ api_key = os.environ.get("anthropic")
+ if not api_key:
+ raise ValueError("مفتاح API لـ Claude غير موجود في متغيرات البيئة")
+ return api_key
+
+ def get_available_models(self):
+ """
+ الحصول على قائمة بالنماذج المتاحة
+
+ العوائد:
+ dict: قائمة بالنماذج مع وصفها
+ """
+ return {
+ "claude-3-7-sonnet": "Claude 3.7 Sonnet - نموذج ذكي للمهام المتقدمة",
+ "claude-3-5-haiku": "Claude 3.5 Haiku - أسرع نموذج للمهام اليومية"
+ }
+
+ def get_model_full_name(self, short_name):
+ """
+ تحويل الاسم المختصر للنموذج إلى الاسم الكامل
+
+ المعلمات:
+ short_name: الاسم المختصر للنموذج
+
+ العوائد:
+ str: الاسم الكامل للنموذج
+ """
+ valid_models = {
+ "claude-3-7-sonnet": "claude-3-7-sonnet-20250219",
+ "claude-3-5-haiku": "claude-3-5-haiku-20240307"
+ }
+
+ return valid_models.get(short_name, short_name)
+
+ def analyze_image(self, image_path, prompt, model_name="claude-3-7-sonnet"):
+ """
+ تحليل صورة باستخدام نموذج Claude AI
+
+ المعلمات:
+ image_path: مسار الصورة المراد تحليلها
+ prompt: التوجيه للنموذج
+ model_name: اسم نموذج Claude المراد استخدامه
+
+ العوائد:
+ dict: نتائج التحليل
+ """
+ try:
+ # الحصول على مفتاح API
+ api_key = self.get_api_key()
+
+ # قراءة محتوى الصورة
+ with open(image_path, 'rb') as f:
+ file_content = f.read()
+
+ # تحويل المحتوى إلى Base64
+ file_base64 = base64.b64encode(file_content).decode('utf-8')
+
+ # تحديد نوع الملف من امتداده
+ _, ext = os.path.splitext(image_path)
+ ext = ext.lower()
+
+ if ext in ('.jpg', '.jpeg'):
+ file_type = "image/jpeg"
+ elif ext == '.png':
+ file_type = "image/png"
+ elif ext == '.gif':
+ file_type = "image/gif"
+ elif ext == '.webp':
+ file_type = "image/webp"
+ else:
+ file_type = "image/jpeg" # افتراضي
+
+ # التحقق من اسم النموذج وتصحيحه إذا لزم الأمر
+ model_name = self.get_model_full_name(model_name)
+
+ # إعداد البيانات للطلب
+ headers = {
+ "Content-Type": "application/json",
+ "x-api-key": api_key,
+ "anthropic-version": "2023-06-01"
+ }
+
+ payload = {
+ "model": model_name,
+ "max_tokens": 4096,
+ "messages": [
+ {
+ "role": "user",
+ "content": [
+ {"type": "text", "text": prompt},
+ {
+ "type": "image",
+ "source": {
+ "type": "base64",
+ "media_type": file_type,
+ "data": file_base64
+ }
+ }
+ ]
+ }
+ ]
+ }
+
+ # إرسال الطلب إلى API
+ response = requests.post(
+ self.api_url,
+ headers=headers,
+ json=payload,
+ timeout=60
+ )
+
+ # التحقق من نجاح الطلب
+ if response.status_code != 200:
+ error_message = f"فشل طلب API: {response.status_code}"
+ try:
+ error_details = response.json()
+ error_message += f"\nتفاصيل: {error_details}"
+ except:
+ error_message += f"\nتفاصيل: {response.text}"
+
+ return {"error": error_message}
+
+ # معالجة الاستجابة
+ result = response.json()
+
+ return {
+ "success": True,
+ "content": result["content"][0]["text"],
+ "model": result["model"],
+ "usage": result.get("usage", {})
+ }
+
+ except Exception as e:
+ logging.error(f"خطأ أثناء تحليل الصورة: {str(e)}")
+ import traceback
+ stack_trace = traceback.format_exc()
+ return {"error": f"فشل في تحليل الصورة: {str(e)}\n{stack_trace}"}
+
+ def chat_completion(self, messages, model_name="claude-3-7-sonnet"):
+ """
+ إكمال محادثة باستخدام نموذج Claude AI
+
+ المعلمات:
+ messages: سجل المحادثة
+ model_name: اسم نموذج Claude المراد استخدامه
+
+ العوائد:
+ dict: نتائج الإكمال
+ """
+ try:
+ # الحصول على مفتاح API
+ api_key = self.get_api_key()
+
+ # تحويل رسائل streamlit إلى تنسيق Claude API
+ claude_messages = []
+ for msg in messages:
+ claude_messages.append({
+ "role": msg["role"],
+ "content": msg["content"]
+ })
+
+ # التحقق من اسم النموذج وتصحيحه إذا لزم الأمر
+ model_name = self.get_model_full_name(model_name)
+
+ # إعداد البيانات للطلب
+ headers = {
+ "Content-Type": "application/json",
+ "x-api-key": api_key,
+ "anthropic-version": "2023-06-01"
+ }
+
+ payload = {
+ "model": model_name,
+ "max_tokens": 2048,
+ "messages": claude_messages,
+ "temperature": 0.7
+ }
+
+ # إرسال الطلب إلى API
+ response = requests.post(
+ self.api_url,
+ headers=headers,
+ json=payload,
+ timeout=30
+ )
+
+ # التحقق من نجاح الطلب
+ if response.status_code != 200:
+ error_message = f"فشل طلب API: {response.status_code}"
+ try:
+ error_details = response.json()
+ error_message += f"\nتفاصيل: {error_details}"
+ except:
+ error_message += f"\nتفاصيل: {response.text}"
+
+ return {"error": error_message}
+
+ # معالجة الاستجابة
+ result = response.json()
+
+ return {
+ "success": True,
+ "content": result["content"][0]["text"],
+ "model": result["model"],
+ "usage": result.get("usage", {})
+ }
+
+ except Exception as e:
+ logging.error(f"خطأ أثناء إكمال المحادثة: {str(e)}")
+ import traceback
+ stack_trace = traceback.format_exc()
+ return {"error": f"فشل في إكمال المحادثة: {str(e)}\n{stack_trace}"}
+
+
+class AIAssistantApp:
+ """وحدة المساعد الذكي"""
+
+ def __init__(self):
+ """تهيئة وحدة المساعد الذكي"""
+ # تحميل النماذج عند بدء التشغيل
+ self.cost_model = load_cost_prediction_model()
+ self.document_model = load_document_classifier_model()
+ self.risk_model = load_risk_assessment_model()
+ self.local_content_model = load_local_content_model()
+ self.entity_model = load_entity_recognition_model()
+
+ # إنشاء خدمة Claude AI
+ self.claude_service = ClaudeAIService()
+
+ # تهيئة قائمة الأسئلة والإجابات الشائعة
+ self.faqs = [
+ {
+ "question": "كيف يمكنني إضافة مشروع جديد؟",
+ "answer": "يمكنك إضافة مشروع جديد من خلال الانتقال إلى وحدة إدارة المشاريع، ثم النقر على زر 'إضافة مشروع جديد'، وملء النموذج بالبيانات المطلوبة."
+ },
+ {
+ "question": "ما هي خطوات تسعير المناقصة؟",
+ "answer": "تتضمن خطوات تسعير المناقصة: 1) تحليل مستندات المناقصة، 2) تحديد بنود العمل، 3) تقدير التكاليف المباشرة، 4) إضافة المصاريف العامة والأرباح، 5) احتساب المحتوى المحلي، 6) مراجعة النتائج النهائية."
+ },
+ {
+ "question": "كيف يتم حساب المحتوى المحلي؟",
+ "answer": "يتم حساب المحتوى المحلي بتحديد نسبة المنتجات والخدمات والقوى العاملة المحلية من إجمالي التكاليف. يتم استخدام قاعدة بيانات الموردين المعتمدين وتطبيق معادلات خاصة حسب متطلبات هيئة المحتوى المحلي."
+ },
+ {
+ "question": "كيف يمكنني تصدير التقارير؟",
+ "answer": "يمكنك تصدير التقارير من وحدة التقارير والتحليلات، حيث يوجد زر 'تصدير' في كل تقرير. يمكن تصدير التقارير بتنسيقات مختلفة مثل Excel و PDF و CSV."
+ },
+ {
+ "question": "كيف يمكنني تقييم المخاطر للمشروع؟",
+ "answer": "يمكنك تقييم المخاطر للمشروع من خلال وحدة المخاطر، حيث يمكنك إضافة المخاطر المحتملة وتقييم تأثيرها واحتماليتها، ثم وضع خطة الاستجابة المناسبة."
+ },
+ {
+ "question": "ما هي طرق التسعير المتاحة في النظام؟",
+ "answer": "يوفر النظام أربع طرق للتسعير: 1) التسعير القياسي، 2) التسعير غير المتزن، 3) التسعير التنافسي، 4) التسعير الموجه بالربحية. يمكنك اختيار الطريقة المناسبة حسب طبيعة المشروع واستراتيجية الشركة."
+ },
+ {
+ "question": "كيف يمكنني معالجة مستندات المناقصة ضخمة الحجم؟",
+ "answer": "يمكنك استخدام وحدة تحليل المستندات لمعالجة مستندات المناقصة ضخمة الحجم، حيث تقوم الوحدة بتحليل المستندات واستخراج المعلومات المهمة مثل مواصفات المشروع ومتطلباته وشروطه تلقائياً."
+ }
+ ]
+
+ def render(self):
+ """عرض واجهة وحدة المساعد الذكي"""
+
+ st.markdown("
وحدة المساعد الذكي
", unsafe_allow_html=True)
+
+ tabs = st.tabs([
+ "المساعد الذكي",
+ "التنبؤ بالتكاليف",
+ "تحليل المخاطر",
+ "تحليل المستندات",
+ "المحتوى المحلي",
+ "الأسئلة الشائعة"
+ ])
+
+ with tabs[0]:
+ self._render_ai_assistant_tab()
+
+ with tabs[1]:
+ self._render_cost_prediction_tab()
+
+ with tabs[2]:
+ self._render_risk_analysis_tab()
+
+ with tabs[3]:
+ self._render_document_analysis_tab()
+
+ with tabs[4]:
+ self._render_local_content_tab()
+
+ with tabs[5]:
+ self._render_faq_tab()
+
+ def _render_ai_assistant_tab(self):
+ """عرض تبويب المساعد الذكي مع دعم Claude AI"""
+
+ st.markdown("### المساعد الذكي لتسعير المناقصات")
+
+ # اختيار نموذج Claude
+ claude_models = self.claude_service.get_available_models()
+
+ selected_model = st.radio(
+ "اختر نموذج الذكاء الاصطناعي",
+ options=list(claude_models.keys()),
+ format_func=lambda x: claude_models[x],
+ horizontal=True,
+ key="assistant_ai_model"
+ )
+
+ # عرض واجهة المحادثة
+ st.markdown("""
+
+
+
المساعد الذكي
+
تحدث مع المساعد الذكي للحصول على المساعدة في تسعير المناقصات وتحليل البيانات
+
+
+ """, unsafe_allow_html=True)
+
+ # تهيئة محفوظات المحادثة في حالة الجلسة إذا لم تكن موجودة
+ if 'ai_assistant_messages' not in st.session_state:
+ st.session_state.ai_assistant_messages = [
+ {"role": "assistant", "content": "مرحباً! أنا المساعد الذكي لنظام تسعير المناقصات. كيف يمكنني مساعدتك اليوم؟"}
+ ]
+
+ # عرض محفوظات المحادثة بتنسيق محسن
+ chat_container = st.container()
+ with chat_container:
+ for message in st.session_state.ai_assistant_messages:
+ if message["role"] == "user":
+ st.markdown(f"""
+
+ """, unsafe_allow_html=True)
+
+ # إضافة خيار رفع الملفات
+ uploaded_file = st.file_uploader(
+ "اختياري: ارفع ملفًا للمساعدة (صورة، PDF)",
+ type=["jpg", "jpeg", "png", "pdf"],
+ key="assistant_file_upload"
+ )
+
+ # مربع إدخال الرسالة
+ user_input = st.text_input("اكتب رسالتك هنا", key="ai_assistant_input")
+
+ # التحقق من وجود مفتاح API
+ api_available = True
+ try:
+ self.claude_service.get_api_key()
+ except ValueError:
+ api_available = False
+ st.warning("مفتاح API لـ Claude غير متوفر. يرجى التأكد من تعيين متغير البيئة 'anthropic'.")
+
+ if user_input and api_available:
+ # إضافة رسالة المستخدم إلى المحفوظات
+ st.session_state.ai_assistant_messages.append({"role": "user", "content": user_input})
+
+ # عرض محفوظات المحادثة المحدثة
+ with chat_container:
+ st.markdown(f"""
+
+
+ {user_input}
+
+
+ """, unsafe_allow_html=True)
+
+ # معالجة الرد
+ with st.spinner("جاري التفكير..."):
+ # التحقق مما إذا كان هناك ملف مرفق
+ if uploaded_file:
+ # حفظ الملف المرفوع مؤقتاً
+ with tempfile.NamedTemporaryFile(delete=False, suffix=f".{uploaded_file.name.split('.')[-1]}") as temp_file:
+ temp_file.write(uploaded_file.getbuffer())
+ temp_file_path = temp_file.name
+
+ # إذا كان الملف PDF، تحويله إلى صورة
+ if uploaded_file.name.lower().endswith('.pdf'):
+ if pdf_conversion_available:
+ try:
+ # تحويل الصفحة الأولى فقط
+ images = convert_from_path(temp_file_path, first_page=1, last_page=1)
+ if images:
+ # حفظ الصورة بشكل مؤقت
+ temp_image_path = f"{temp_file_path}_image.jpg"
+ images[0].save(temp_image_path, 'JPEG')
+ # استخدام مسار الصورة بدلاً من PDF
+ os.remove(temp_file_path)
+ temp_file_path = temp_image_path
+ except Exception as e:
+ st.error(f"فشل في تحويل ملف PDF إلى صورة: {str(e)}")
+ else:
+ st.error("تحليل ملفات PDF يتطلب تثبيت مكتبة pdf2image.")
+ response = "عذراً، لا يمكنني تحليل ملفات PDF في الوقت الحالي. يرجى تحويل الملف إلى صورة أو مشاركة المعلومات كنص."
+
+ # تحليل الصورة باستخدام Claude
+ prompt = f"المستخدم قام برفع هذه الصورة وسأل: {user_input}\nقم بتحليل الصورة والرد على سؤال المستخدم بشكل تفصيلي."
+ results = self.claude_service.analyze_image(temp_file_path, prompt, model_name=selected_model)
+
+ # حذف الملف المؤقت
+ try:
+ os.remove(temp_file_path)
+ except:
+ pass
+
+ if "error" in results:
+ response = f"عذراً، حدث خطأ أثناء تحليل الملف: {results['error']}"
+ else:
+ response = results["content"]
+ else:
+ # استخدام خدمة Claude للرد على الرسائل النصية
+ results = self.claude_service.chat_completion(st.session_state.ai_assistant_messages, model_name=selected_model)
+
+ if "error" in results:
+ response = f"عذراً، حدث خطأ أثناء معالجة طلبك: {results['error']}"
+ else:
+ response = results["content"]
+
+ # إضافة رد المساعد إلى المحفوظات
+ st.session_state.ai_assistant_messages.append({"role": "assistant", "content": response})
+
+ # عرض رد المساعد
+ with chat_container:
+ st.markdown(f"""
+
+
+ {response}
+
+
+ """, unsafe_allow_html=True)
+
+ # إعادة تعيين قيمة الإدخال
+ st.text_input("اكتب رسالتك هنا", value="", key="ai_assistant_input_reset")
+
+ def _generate_ai_response(self, user_input, model_name="claude-3-7-sonnet"):
+ """توليد رد المساعد الذكي باستخدام Claude AI"""
+
+ # التحقق من وجود مفتاح API
+ try:
+ self.claude_service.get_api_key()
+ except ValueError:
+ return "عذراً، لا يمكنني الاتصال بخدمة الذكاء الاصطناعي في الوقت الحالي. يرجى التحقق من إعدادات API."
+
+ # البحث في الأسئلة الشائعة أولاً
+ for faq in self.faqs:
+ if any(keyword in user_input.lower() for keyword in faq["question"].lower().split()):
+ return f"{faq['answer']}\n\nهل تحتاج إلى مساعدة أخرى؟"
+
+ # إنشاء محادثة لإرسالها إلى Claude
+ messages = [
+ {"role": "user", "content": user_input}
+ ]
+
+ # استدعاء خدمة Claude
+ results = self.claude_service.chat_completion(messages, model_name=model_name)
+
+ if "error" in results:
+ # إذا فشل الاتصال، استخدم التوليد الافتراضي
+ logging.warning(f"فشل الاتصال بـ Claude AI: {results['error']}. استخدام التوليد الافتراضي.")
+ return self._generate_default_response(user_input)
+ else:
+ return results["content"]
+
+ def _generate_default_response(self, user_input):
+ """توليد رد افتراضي في حالة عدم توفر Claude AI"""
+
+ if "تسعير" in user_input or "سعر" in user_input or "تكلفة" in user_input:
+ return "يمكنك استخدام وحدة التنبؤ بالتكاليف لتقدير تكاليف المشروع بناءً على خصائصه. انتقل إلى تبويب 'التنبؤ بالتكاليف' وأدخل بيانات المشروع لتحصل على تقدير دقيق للتكاليف."
+
+ elif "مخاطر" in user_input or "مخاطرة" in user_input:
+ return "يمكنك استخدام وحدة تحليل المخاطر لتقييم المخاطر المحتملة للمشروع. انتقل إلى تبويب 'تحليل المخاطر' وأدخل بيانات المشروع وعوامل المخاطرة لتحصل على تحليل شامل للمخاطر واستراتيجيات الاستجابة المقترحة."
+
+ elif "مستند" in user_input or "ملف" in user_input or "وثيقة" in user_input or "مناقصة" in user_input:
+ return "يمكنك استخدام وحدة تحليل المستندات لتحليل مستندات المناقصة واستخراج المعلومات المهمة منها. انتقل إلى تبويب 'تحليل المستندات' وقم بتحميل ملفات المناقصة لتحصل على تحليل تفصيلي للمستندات."
+
+ elif "محتوى محلي" in user_input or "محلي" in user_input:
+ return "يمكنك استخدام وحدة المحتوى المحلي لحساب وتحسين نسبة المحتوى المحلي في مشروعك. انتقل إلى تبويب 'المحتوى المحلي' وأدخل بيانات مكونات المشروع لتحصل على تحليل شامل للمحتوى المحلي واقتراحات لتحسينه."
+
+ elif "تقرير" in user_input or "إحصائيات" in user_input or "بيانات" in user_input:
+ return "يمكنك استخدام وحدة التقارير والتحليلات للحصول على تقارير تفصيلية وإحصائيات عن المشاريع. يمكنك الوصول إليها من القائمة الرئيسية للنظام."
+
+ else:
+ return "شكراً لاستفسارك. يمكنني مساعدتك في تسعير المناقصات، وتحليل المخاطر، وتحليل المستندات، وحساب المحتوى المحلي. يرجى توضيح استفسارك أكثر أو اختيار أحد الخيارات في الأعلى للحصول على المساعدة المطلوبة."
+
+ def _render_cost_prediction_tab(self):
+ """عرض تبويب التنبؤ بالتكاليف"""
+
+ st.markdown("### التنبؤ بالتكاليف")
+
+ # عرض نموذج إدخال بيانات المشروع
+ st.markdown("#### بيانات المشروع")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ project_type = st.selectbox(
+ "نوع المشروع",
+ [
+ "مباني سكنية",
+ "مباني تجارية",
+ "مباني حكومية",
+ "مراكز صحية",
+ "مدارس",
+ "بنية تحتية",
+ "طرق",
+ "جسور",
+ "صرف صحي",
+ "مياه",
+ "كهرباء"
+ ],
+ key="cost_project_type"
+ )
+
+ location = st.selectbox(
+ "الموقع",
+ [
+ "الرياض",
+ "جدة",
+ "الدمام",
+ "مكة",
+ "المدينة",
+ "تبوك",
+ "حائل",
+ "عسير",
+ "جازان",
+ "نجران",
+ "الباحة",
+ "الجوف",
+ "القصيم"
+ ],
+ key="cost_location"
+ )
+
+ client_type = st.selectbox(
+ "نوع العميل",
+ [
+ "حكومي",
+ "شبه حكومي",
+ "شركة كبيرة",
+ "شركة متوسطة",
+ "شركة صغيرة",
+ "أفراد"
+ ],
+ key="cost_client_type"
+ )
+
+ with col2:
+ area = st.number_input("المساحة (م²)", min_value=100, max_value=1000000, value=5000, key="cost_area")
+
+ floors = st.number_input("عدد الطوابق", min_value=1, max_value=100, value=3, key="cost_floors")
+
+ duration = st.number_input("مدة التنفيذ (شهور)", min_value=1, max_value=60, value=12, key="cost_duration")
+
+ tender_type = st.selectbox(
+ "نوع المناقصة",
+ [
+ "عامة",
+ "خاصة",
+ "أمر مباشر"
+ ],
+ key="cost_tender_type"
+ )
+
+ st.markdown("#### متغيرات إضافية")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ has_basement = st.checkbox("يتضمن بدروم", key="cost_has_basement")
+ has_special_finishing = st.checkbox("تشطيبات خاصة", key="cost_has_special_finishing")
+
+ with col2:
+ has_landscape = st.checkbox("أعمال تنسيق المواقع", key="cost_has_landscape")
+ has_parking = st.checkbox("مواقف متعددة الطوابق", key="cost_has_parking")
+
+ with col3:
+ has_smart_systems = st.checkbox("أنظمة ذكية", key="cost_has_smart_systems")
+ has_sustainability = st.checkbox("متطلبات استدامة", key="cost_has_sustainability")
+
+ # زر التنبؤ بالتكلفة مع دعم Claude AI
+ col1, col2 = st.columns([1, 3])
+
+ with col1:
+ predict_button = st.button("التنبؤ بالتكلفة", use_container_width=True, key="cost_predict_button")
+
+ with col2:
+ use_claude = st.checkbox("استخدام Claude AI للتحليل المتقدم", value=True, key="cost_use_claude")
+
+ if predict_button:
+ with st.spinner("جاري تحليل البيانات والتنبؤ بالتكاليف..."):
+ # محاكاة وقت المعالجة
+ time.sleep(2)
+
+ # تجهيز البيانات للنموذج
+ features = {
+ 'project_type': project_type,
+ 'location': location,
+ 'area': area,
+ 'floors': floors,
+ 'duration_months': duration,
+ 'tender_type': tender_type,
+ 'client_type': client_type,
+ 'has_basement': has_basement,
+ 'has_special_finishing': has_special_finishing,
+ 'has_landscape': has_landscape,
+ 'has_parking': has_parking,
+ 'has_smart_systems': has_smart_systems,
+ 'has_sustainability': has_sustainability
+ }
+
+ # استدعاء النموذج للتنبؤ
+ cost_prediction_results = self._predict_cost(features)
+
+ # إضافة تحليل إضافي باستخدام Claude AI إذا تم تفعيل الخيار
+ if use_claude:
+ try:
+ # إنشاء نص الميزات للتحليل
+ features_text = f"""
+ بيانات المشروع:
+ - نوع المشروع: {project_type}
+ - الموقع: {location}
+ - المساحة: {area} م²
+ - عدد الطوابق: {floors}
+ - مدة التنفيذ: {duration} شهر
+ - نوع المناقصة: {tender_type}
+ - نوع العميل: {client_type}
+ - يتضمن بدروم: {'نعم' if has_basement else 'لا'}
+ - تشطيبات خاصة: {'نعم' if has_special_finishing else 'لا'}
+ - أعمال تنسيق المواقع: {'نعم' if has_landscape else 'لا'}
+ - مواقف متعددة الطوابق: {'نعم' if has_parking else 'لا'}
+ - أنظمة ذكية: {'نعم' if has_smart_systems else 'لا'}
+ - متطلبات استدامة: {'نعم' if has_sustainability else 'لا'}
+
+ نتائج التنبؤ الأولية:
+ - التكلفة الإجمالية المقدرة: {cost_prediction_results['total_cost']:,.0f} ريال
+ - تكلفة المتر المربع: {cost_prediction_results['cost_per_sqm']:,.0f} ريال/م²
+ - تكلفة المواد: {cost_prediction_results['material_cost']:,.0f} ريال
+ - تكلفة العمالة: {cost_prediction_results['labor_cost']:,.0f} ريال
+ - تكلفة المعدات: {cost_prediction_results['equipment_cost']:,.0f} ريال
+ """
+
+ prompt = f"""تحليل بيانات مشروع وتكاليفه:
+
+ {features_text}
+
+ المطلوب:
+ 1. تحليل التكاليف المتوقعة ومعقوليتها مقارنة بمشاريع مماثلة في السوق السعودي
+ 2. تقديم توصيات وملاحظات لتحسين التكلفة
+ 3. تحديد أي مخاطر محتملة قد تؤثر على التكلفة
+ 4. تقديم نصائح لزيادة فعالية التكلفة
+ 5. تقديم رأي حول مدى تنافسية هذه التكلفة في السوق الحالي
+
+ يرجى تقديم تحليل مهني ومختصر يركز على الجوانب الأكثر أهمية.
+ """
+
+ # استدعاء Claude للتحليل
+ claude_analysis = self.claude_service.chat_completion(
+ [{"role": "user", "content": prompt}]
+ )
+
+ if "error" not in claude_analysis:
+ # إضافة تحليل Claude إلى النتائج
+ cost_prediction_results["claude_analysis"] = claude_analysis["content"]
+ except Exception as e:
+ st.warning(f"تعذر إجراء التحليل المتقدم: {str(e)}")
+
+ # عرض نتائج التنبؤ
+ self._display_cost_prediction_results(cost_prediction_results)
+
+ def _predict_cost(self, features):
+ """التنبؤ بتكاليف المشروع"""
+
+ # في البيئة الحقيقية، سيتم استدعاء نموذج التنبؤ بالتكاليف
+ # محاكاة نتائج التنبؤ للعرض
+
+ # حساب القيمة الأساسية للمتر المربع حسب نوع المشروع
+ base_cost_per_sqm = {
+ "مباني سكنية": 2500,
+ "مباني تجارية": 3000,
+ "مباني حكومية": 3500,
+ "مراكز صحية": 4000,
+ "مدارس": 3200,
+ "بنية تحتية": 2000,
+ "طرق": 1500,
+ "جسور": 5000,
+ "صرف صحي": 2200,
+ "مياه": 2000,
+ "كهرباء": 2500
+ }.get(features['project_type'], 2500)
+
+ # تطبيق معاملات التعديل حسب المتغيرات
+ location_factor = {
+ "الرياض": 1.1,
+ "جدة": 1.15,
+ "الدمام": 1.05,
+ "مكة": 1.2,
+ "المدينة": 1.1,
+ "تبوك": 0.95,
+ "حائل": 0.9,
+ "عسير": 0.95,
+ "جازان": 0.9,
+ "نجران": 0.85,
+ "الباحة": 0.9,
+ "الجوف": 0.85,
+ "القصيم": 0.9
+ }.get(features['location'], 1.0)
+
+ client_factor = {
+ "حكومي": 1.05,
+ "شبه حكومي": 1.0,
+ "شركة كبيرة": 0.95,
+ "شركة متوسطة": 0.9,
+ "شركة صغيرة": 0.85,
+ "أفراد": 0.8
+ }.get(features['client_type'], 1.0)
+
+ tender_factor = {
+ "عامة": 1.0,
+ "خاصة": 0.95,
+ "أمر مباشر": 0.9
+ }.get(features['tender_type'], 1.0)
+
+ # معاملات للميزات الإضافية
+ basement_factor = 1.1 if features['has_basement'] else 1.0
+ special_finishing_factor = 1.2 if features['has_special_finishing'] else 1.0
+ landscape_factor = 1.05 if features['has_landscape'] else 1.0
+ parking_factor = 1.1 if features['has_parking'] else 1.0
+ smart_systems_factor = 1.15 if features['has_smart_systems'] else 1.0
+ sustainability_factor = 1.1 if features['has_sustainability'] else 1.0
+
+ # معامل لعدد الطوابق
+ floors_factor = 1.0 + (features['floors'] - 1) * 0.05
+
+ # حساب التكلفة الإجمالية
+ total_sqm_cost = base_cost_per_sqm * location_factor * client_factor * tender_factor * \
+ basement_factor * special_finishing_factor * landscape_factor * \
+ parking_factor * smart_systems_factor * sustainability_factor * \
+ floors_factor
+
+ total_cost = total_sqm_cost * features['area']
+
+ # حساب التكاليف المفصلة
+ material_cost = total_cost * 0.6
+ labor_cost = total_cost * 0.25
+ equipment_cost = total_cost * 0.15
+
+ # إضافة هامش خطأ عشوائي للمحاكاة
+ error_margin = 0.05 # 5%
+ total_cost = total_cost * (1 + np.random.uniform(-error_margin, error_margin))
+
+ # إعداد النتائج
+ results = {
+ "total_cost": total_cost,
+ "cost_per_sqm": total_cost / features['area'],
+ "material_cost": material_cost,
+ "labor_cost": labor_cost,
+ "equipment_cost": equipment_cost,
+ "breakdown": {
+ "structural_works": total_cost * 0.35,
+ "architectural_works": total_cost * 0.25,
+ "mep_works": total_cost * 0.25,
+ "site_works": total_cost * 0.1,
+ "general_requirements": total_cost * 0.05
+ },
+ "confidence_level": 0.85, # مستوى الثقة في التنبؤ
+ "comparison": {
+ "market_average": total_cost * 1.1,
+ "historical_projects": total_cost * 0.95
+ }
+ }
+
+ return results
+
+ def _display_cost_prediction_results(self, results):
+ """عرض نتائج التنبؤ بالتكاليف"""
+
+ st.markdown("### نتائج التنبؤ بالتكاليف")
+
+ # عرض التكلفة الإجمالية وتكلفة المتر المربع
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric(
+ "التكلفة الإجمالية المتوقعة",
+ f"{results['total_cost']:,.0f} ريال",
+ delta=f"{(results['total_cost'] - results['comparison']['historical_projects']):,.0f} ريال"
+ )
+
+ with col2:
+ st.metric(
+ "تكلفة المتر المربع",
+ f"{results['cost_per_sqm']:,.0f} ريال/م²"
+ )
+
+ with col3:
+ st.metric(
+ "مستوى الثقة في التنبؤ",
+ f"{results['confidence_level'] * 100:.0f}%"
+ )
+
+ # عرض تفصيل التكاليف
+ st.markdown("#### تفصيل التكاليف")
+
+ # رسم مخطط دائري للتكاليف المفصلة
+ fig = px.pie(
+ values=[
+ results['material_cost'],
+ results['labor_cost'],
+ results['equipment_cost']
+ ],
+ names=["تكلفة المواد", "تكلفة العمالة", "تكلفة المعدات"],
+ title="توزيع التكاليف الرئيسية"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # رسم مخطط شريطي لتفصيل الأعمال
+ breakdown_data = pd.DataFrame({
+ 'فئة الأعمال': [
+ "الأعمال الإنشائية",
+ "الأعمال المعمارية",
+ "الأعمال الكهروميكانيكية",
+ "أعمال الموقع",
+ "المتطلبات العامة"
+ ],
+ 'التكلفة': [
+ results['breakdown']['structural_works'],
+ results['breakdown']['architectural_works'],
+ results['breakdown']['mep_works'],
+ results['breakdown']['site_works'],
+ results['breakdown']['general_requirements']
+ ]
+ })
+
+ fig = px.bar(
+ breakdown_data,
+ x='فئة الأعمال',
+ y='التكلفة',
+ title="تفصيل التكاليف حسب فئة الأعمال",
+ text_auto='.3s'
+ )
+
+ fig.update_traces(texttemplate='%{text:,.0f} ريال', textposition='outside')
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض مقارنة مع متوسط السوق
+ st.markdown("#### مقارنة مع متوسط السوق")
+
+ comparison_data = pd.DataFrame({
+ 'المصدر': [
+ "التكلفة المتوقعة",
+ "متوسط السوق",
+ "مشاريع مماثلة سابقة"
+ ],
+ 'التكلفة': [
+ results['total_cost'],
+ results['comparison']['market_average'],
+ results['comparison']['historical_projects']
+ ]
+ })
+
+ fig = px.bar(
+ comparison_data,
+ x='المصدر',
+ y='التكلفة',
+ title="مقارنة التكلفة المتوقعة مع السوق",
+ text_auto='.3s',
+ color='المصدر',
+ color_discrete_map={
+ "التكلفة المتوقعة": "#1f77b4",
+ "متوسط السوق": "#ff7f0e",
+ "مشاريع مماثلة سابقة": "#2ca02c"
+ }
+ )
+
+ fig.update_traces(texttemplate='%{text:,.0f} ريال', textposition='outside')
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض تحليل Claude AI إذا كان متوفراً
+ if "claude_analysis" in results:
+ st.markdown("### تحليل Claude AI المتقدم")
+ st.info(results["claude_analysis"])
+
+ # عرض ملاحظات وتوصيات
+ st.markdown("#### ملاحظات وتوصيات")
+
+ st.info("""
+ - تم التنبؤ بالتكاليف بناءً على البيانات المدخلة ونماذج التعلم الآلي المدربة على مشاريع مماثلة.
+ - مستوى الثقة في التنبؤ جيد، ولكن يجب مراجعة التكاليف بشكل تفصيلي قبل اتخاذ القرار النهائي.
+ - تكلفة المتر المربع متوافقة مع متوسط السوق لهذا النوع من المشاريع.
+ - ينصح بمراجعة التصميم لتحسين التكلفة وزيادة الكفاءة.
+ """)
+
+ # زر تصدير التقرير
+ if st.button("تصدير تقرير التكاليف"):
+ st.success("تم تصدير تقرير التكاليف بنجاح!")
+
+ def _render_risk_analysis_tab(self):
+ """عرض تبويب تحليل المخاطر"""
+
+ st.markdown("### تحليل المخاطر")
+
+ # عرض نموذج إدخال بيانات المشروع للمخاطر
+ st.markdown("#### بيانات المشروع")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ project_type = st.selectbox(
+ "نوع المشروع",
+ [
+ "مباني سكنية",
+ "مباني تجارية",
+ "مباني حكومية",
+ "مراكز صحية",
+ "مدارس",
+ "بنية تحتية",
+ "طرق",
+ "جسور",
+ "صرف صحي",
+ "مياه",
+ "كهرباء"
+ ],
+ key="risk_project_type"
+ )
+
+ location = st.selectbox(
+ "الموقع",
+ [
+ "الرياض",
+ "جدة",
+ "الدمام",
+ "مكة",
+ "المدينة",
+ "تبوك",
+ "حائل",
+ "عسير",
+ "جازان",
+ "نجران",
+ "الباحة",
+ "الجوف",
+ "القصيم"
+ ],
+ key="risk_location"
+ )
+
+ with col2:
+ client_type = st.selectbox(
+ "نوع العميل",
+ [
+ "حكومي",
+ "شبه حكومي",
+ "شركة كبيرة",
+ "شركة متوسطة",
+ "شركة صغيرة",
+ "أفراد"
+ ],
+ key="risk_client_type"
+ )
+
+ tender_type = st.selectbox(
+ "نوع المناقصة",
+ [
+ "عامة",
+ "خاصة",
+ "أمر مباشر"
+ ],
+ key="risk_tender_type"
+ )
+
+ st.markdown("#### عوامل المخاطرة")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ payment_terms = st.slider("شروط الدفع (1-10)", 1, 10, 5,
+ help="1: شروط دفع سيئة جداً، 10: شروط دفع ممتازة",
+ key="risk_payment_terms")
+ completion_deadline = st.slider("مهلة الإنجاز (1-10)", 1, 10, 5,
+ help="1: مهلة قصيرة جداً، 10: مهلة مريحة",
+ key="risk_completion_deadline")
+
+ with col2:
+ penalty_clause = st.slider("شروط الغرامات (1-10)", 1, 10, 5,
+ help="1: غرامات مرتفعة جداً، 10: غرامات معقولة",
+ key="risk_penalty_clause")
+ technical_complexity = st.slider("التعقيد الفني (1-10)", 1, 10, 5,
+ help="1: بسيط جداً، 10: معقد للغاية",
+ key="risk_technical_complexity")
+
+ with col3:
+ company_experience = st.slider("خبرة الشركة (1-10)", 1, 10, 7,
+ help="1: لا توجد خبرة، 10: خبرة عالية",
+ key="risk_company_experience")
+ market_volatility = st.slider("تقلبات السوق (1-10)", 1, 10, 5,
+ help="1: مستقر جداً، 10: متقلب للغاية",
+ key="risk_market_volatility")
+
+ # زر تحليل المخاطر مع دعم Claude AI
+ col1, col2 = st.columns([1, 3])
+
+ with col1:
+ analyze_button = st.button("تحليل المخاطر", use_container_width=True, key="risk_analyze_button")
+
+ with col2:
+ # Añadimos un key único para este checkbox
+ use_claude = st.checkbox("استخدام Claude AI للتحليل المتقدم", value=True, key="risk_use_claude")
+
+ if analyze_button:
+ with st.spinner("جاري تحليل المخاطر..."):
+ # محاكاة وقت المعالجة
+ time.sleep(2)
+
+ # تجهيز البيانات للنموذج
+ features = {
+ 'project_type': project_type,
+ 'location': location,
+ 'client_type': client_type,
+ 'tender_type': tender_type,
+ 'payment_terms': payment_terms,
+ 'completion_deadline': completion_deadline,
+ 'penalty_clause': penalty_clause,
+ 'technical_complexity': technical_complexity,
+ 'company_experience': company_experience,
+ 'market_volatility': market_volatility
+ }
+
+ # استدعاء النموذج لتحليل المخاطر
+ risk_analysis_results = self._analyze_risks(features)
+
+ # إضافة تحليل إضافي باستخدام Claude AI إذا تم تفعيل الخيار
+ if use_claude:
+ try:
+ # إنشاء نص الميزات للتحليل
+ features_text = f"""
+ بيانات المشروع:
+ - نوع المشروع: {project_type}
+ - الموقع: {location}
+ - نوع العميل: {client_type}
+ - نوع المناقصة: {tender_type}
+
+ عوامل المخاطرة:
+ - شروط الدفع: {payment_terms}/10
+ - مهلة الإنجاز: {completion_deadline}/10
+ - شروط الغرامات: {penalty_clause}/10
+ - التعقيد الفني: {technical_complexity}/10
+ - خبرة الشركة: {company_experience}/10
+ - تقلبات السوق: {market_volatility}/10
+
+ ملخص التحليل الأولي:
+ - متوسط درجة المخاطرة: {risk_analysis_results['avg_risk_score']:.1f}/10
+ - عدد المخاطر العالية: {risk_analysis_results['high_risks']}
+ - عدد المخاطر المتوسطة: {risk_analysis_results['medium_risks']}
+ - عدد المخاطر المنخفضة: {risk_analysis_results['low_risks']}
+
+ أعلى المخاطر:
+ """
+
+ # إضافة تفاصيل أعلى المخاطر
+ for i, risk in enumerate(risk_analysis_results['top_risks'][:3]):
+ features_text += f"""
+ {i+1}. {risk['name']} ({risk['category']})
+ - الاحتمالية: {risk['probability'] * 100:.0f}%
+ - التأثير: {risk['impact'] * 100:.0f}%
+ - درجة المخاطرة: {risk['risk_score']}/10
+ """
+
+ prompt = f"""تحليل مخاطر مشروع:
+
+ {features_text}
+
+ المطلوب:
+ 1. تحليل عوامل المخاطرة وتأثيرها على المشروع
+ 2. تقديم توصيات إضافية لإدارة المخاطر
+ 3. اقتراح استراتيجيات استجابة للمخاطر الرئيسية
+ 4. تقديم نصائح لتحسين شروط العقد لتقليل المخاطر
+ 5. تقييم مدى ملاءمة المشروع لاستراتيجية الشركة
+
+ يرجى تقديم تحليل مهني ومختصر يركز على الجوانب الأكثر أهمية.
+ """
+
+ # استدعاء Claude للتحليل
+ claude_analysis = self.claude_service.chat_completion(
+ [{"role": "user", "content": prompt}]
+ )
+
+ if "error" not in claude_analysis:
+ # إضافة تحليل Claude إلى النتائج
+ risk_analysis_results["claude_analysis"] = claude_analysis["content"]
+ except Exception as e:
+ st.warning(f"تعذر إجراء التحليل المتقدم: {str(e)}")
+
+ # عرض نتائج تحليل المخاطر
+ self._display_risk_analysis_results(risk_analysis_results)
+
+ def _analyze_risks(self, features):
+ """تحليل مخاطر المشروع"""
+
+ # في البيئة الحقيقية، سيتم استدعاء نموذج تحليل المخاطر
+ # محاكاة نتائج تحليل المخاطر للعرض
+
+ # تعريف قائمة من المخاطر المحتملة
+ potential_risks = [
+ {
+ "id": "R-001",
+ "name": "غرامة تأخير مرتفعة",
+ "category": "مخاطر مالية",
+ "description": "غرامة تأخير مرتفعة تصل إلى 10% من قيمة العقد، مما قد يؤثر سلباً على ربحية المشروع في حال التأخير.",
+ "probability": 0.6,
+ "impact": 0.8,
+ "risk_score": 7.8,
+ "response_strategy": "تخطيط مفصل للمشروع مع وضع مخزون زمني مناسب وتحديد نقاط التسليم المبكر."
+ },
+ {
+ "id": "R-002",
+ "name": "تقلبات أسعار المواد",
+ "category": "مخاطر السوق",
+ "description": "ارتفاع محتمل في أسعار المواد الخام خلال فترة تنفيذ المشروع، مما يؤثر على التكلفة الإجمالية.",
+ "probability": 0.7,
+ "impact": 0.7,
+ "risk_score": 7.5,
+ "response_strategy": "التعاقد المبكر مع الموردين وتثبيت الأسعار، أو إضافة بند تعديل سعري في العقد."
+ },
+ {
+ "id": "R-003",
+ "name": "ضعف تدفق المدفوعات",
+ "category": "مخاطر مالية",
+ "description": "تأخر العميل في سداد المستخلصات مما يؤثر على التدفق النقدي للمشروع.",
+ "probability": 0.5,
+ "impact": 0.8,
+ "risk_score": 7.2,
+ "response_strategy": "التفاوض على شروط دفع واضحة ومواعيد محددة، وإمكانية طلب دفعة مقدمة."
+ },
+ {
+ "id": "R-004",
+ "name": "نقص العمالة الماهرة",
+ "category": "مخاطر الموارد",
+ "description": "صعوبة توفير عمالة ماهرة لتنفيذ أجزاء محددة من المشروع.",
+ "probability": 0.5,
+ "impact": 0.6,
+ "risk_score": 6.5,
+ "response_strategy": "التخطيط المبكر للموارد البشرية وتوقيع عقود مع مقاولي الباطن المتخصصين."
+ },
+ {
+ "id": "R-005",
+ "name": "تغييرات في نطاق العمل",
+ "category": "مخاطر تعاقدية",
+ "description": "طلبات تغيير من العميل تؤدي إلى زيادة نطاق العمل دون تعديل مناسب للتكلفة والجدول الزمني.",
+ "probability": 0.6,
+ "impact": 0.6,
+ "risk_score": 6.0,
+ "response_strategy": "تضمين آلية واضحة لإدارة التغيير في العقد وتقييم تأثير أي تغييرات على التكلفة والزمن."
+ },
+ {
+ "id": "R-006",
+ "name": "مشاكل في الموقع",
+ "category": "مخاطر فنية",
+ "description": "ظروف موقع غير متوقعة تؤثر على تنفيذ الأعمال، مثل مشاكل في التربة أو مرافق تحت الأرض.",
+ "probability": 0.4,
+ "impact": 0.7,
+ "risk_score": 5.8,
+ "response_strategy": "إجراء دراسات واختبارات مفصلة للموقع قبل بدء التنفيذ، وتخصيص احتياطي للطوارئ."
+ },
+ {
+ "id": "R-007",
+ "name": "تضارب في التصاميم",
+ "category": "مخاطر فنية",
+ "description": "تعارض بين مختلف تخصصات التصميم (معماري، إنشائي، كهروميكانيكي) يؤدي إلى تأخير وإعادة عمل.",
+ "probability": 0.4,
+ "impact": 0.6,
+ "risk_score": 5.4,
+ "response_strategy": "مراجعة شاملة للتصاميم قبل البدء في التنفيذ واستخدام نمذجة معلومات البناء (BIM) لكشف التعارضات."
+ },
+ {
+ "id": "R-008",
+ "name": "تأخر الموافقات",
+ "category": "مخاطر تنظيمية",
+ "description": "تأخر في الحصول على الموافقات والتصاريح اللازمة من الجهات المختصة.",
+ "probability": 0.5,
+ "impact": 0.5,
+ "risk_score": 5.0,
+ "response_strategy": "التخطيط المبكر للتصاريح المطلوبة وبناء علاقات جيدة مع الجهات التنظيمية."
+ },
+ {
+ "id": "R-009",
+ "name": "عدم توفر المعدات",
+ "category": "مخاطر الموارد",
+ "description": "صعوبة في توفير المعدات المتخصصة في الوقت المطلوب.",
+ "probability": 0.3,
+ "impact": 0.6,
+ "risk_score": 4.8,
+ "response_strategy": "حجز المعدات مبكراً وتوفير بدائل محتملة في حالة عدم توفر المعدات الأساسية."
+ },
+ {
+ "id": "R-010",
+ "name": "ظروف جوية قاسية",
+ "category": "مخاطر خارجية",
+ "description": "تأثير الظروف الجوية القاسية (حرارة شديدة، أمطار غزيرة، عواصف رملية) على سير العمل.",
+ "probability": 0.3,
+ "impact": 0.5,
+ "risk_score": 4.5,
+ "response_strategy": "تخطيط الجدول الزمني مع مراعاة المواسم وإضافة مخزون زمني للظروف الجوية غير المتوقعة."
+ }
+ ]
+
+ # حساب درجات المخاطرة بناءً على الميزات المدخلة
+ for risk in potential_risks:
+ # تعديل احتمالية حدوث المخاطر بناءً على العوامل المدخلة
+ if risk["id"] == "R-001": # غرامة تأخير
+ risk["probability"] = risk["probability"] * (10 - features["penalty_clause"]) / 10
+ risk["probability"] = risk["probability"] * (10 - features["completion_deadline"]) / 10
+
+ elif risk["id"] == "R-002": # تقلبات أسعار المواد
+ risk["probability"] = risk["probability"] * features["market_volatility"] / 10
+
+ elif risk["id"] == "R-003": # ضعف تدفق المدفوعات
+ risk["probability"] = risk["probability"] * (10 - features["payment_terms"]) / 10
+
+ if features["client_type"] == "حكومي":
+ risk["probability"] = risk["probability"] * 0.6 # احتمالية أقل مع العملاء الحكوميين
+ elif features["client_type"] == "أفراد":
+ risk["probability"] = risk["probability"] * 1.3 # احتمالية أعلى مع العملاء الأفراد
+
+ elif risk["id"] == "R-004": # نقص العمالة الماهرة
+ risk["probability"] = risk["probability"] * features["technical_complexity"] / 10
+
+ elif risk["id"] == "R-005": # تغييرات في نطاق العمل
+ risk["probability"] = risk["probability"] * features["technical_complexity"] / 10
+
+ if features["client_type"] == "حكومي":
+ risk["probability"] = risk["probability"] * 1.2 # احتمالية أعلى للتغييرات مع العملاء الحكوميين
+
+ # تعديل تأثير المخاطر بناءً على العوامل المدخلة
+ if risk["category"] == "مخاطر فنية":
+ risk["impact"] = risk["impact"] * (10 - features["company_experience"]) / 10
+
+ # إعادة حساب درجة المخاطرة
+ risk["risk_score"] = round(risk["probability"] * risk["impact"] * 10, 1)
+
+ # ترتيب المخاطر تنازلياً حسب درجة المخاطرة
+ sorted_risks = sorted(potential_risks, key=lambda x: x["risk_score"], reverse=True)
+
+ # حساب عدد المخاطر حسب شدتها
+ high_risks = sum(1 for risk in sorted_risks if risk["risk_score"] >= 6.0)
+ medium_risks = sum(1 for risk in sorted_risks if 3.0 <= risk["risk_score"] < 6.0)
+ low_risks = sum(1 for risk in sorted_risks if risk["risk_score"] < 3.0)
+
+ # حساب متوسط درجة المخاطرة
+ avg_risk_score = sum(risk["risk_score"] for risk in sorted_risks) / len(sorted_risks)
+
+ # تجهيز النتائج
+ results = {
+ "top_risks": sorted_risks,
+ "high_risks": high_risks,
+ "medium_risks": medium_risks,
+ "low_risks": low_risks,
+ "avg_risk_score": avg_risk_score,
+ "risk_profile": {
+ "financial_risk": sum(risk["risk_score"] for risk in sorted_risks if risk["category"] == "مخاطر مالية") / sum(1 for risk in sorted_risks if risk["category"] == "مخاطر مالية") if sum(1 for risk in sorted_risks if risk["category"] == "مخاطر مالية") > 0 else 0,
+ "technical_risk": sum(risk["risk_score"] for risk in sorted_risks if risk["category"] == "مخاطر فنية") / sum(1 for risk in sorted_risks if risk["category"] == "مخاطر فنية") if sum(1 for risk in sorted_risks if risk["category"] == "مخاطر فنية") > 0 else 0,
+ "market_risk": sum(risk["risk_score"] for risk in sorted_risks if risk["category"] == "مخاطر السوق") / sum(1 for risk in sorted_risks if risk["category"] == "مخاطر السوق") if sum(1 for risk in sorted_risks if risk["category"] == "مخاطر السوق") > 0 else 0,
+ "resource_risk": sum(risk["risk_score"] for risk in sorted_risks if risk["category"] == "مخاطر الموارد") / sum(1 for risk in sorted_risks if risk["category"] == "مخاطر الموارد") if sum(1 for risk in sorted_risks if risk["category"] == "مخاطر الموارد") > 0 else 0,
+ "contract_risk": sum(risk["risk_score"] for risk in sorted_risks if risk["category"] == "مخاطر تعاقدية") / sum(1 for risk in sorted_risks if risk["category"] == "مخاطر تعاقدية") if sum(1 for risk in sorted_risks if risk["category"] == "مخاطر تعاقدية") > 0 else 0,
+ "regulatory_risk": sum(risk["risk_score"] for risk in sorted_risks if risk["category"] == "مخاطر تنظيمية") / sum(1 for risk in sorted_risks if risk["category"] == "مخاطر تنظيمية") if sum(1 for risk in sorted_risks if risk["category"] == "مخاطر تنظيمية") > 0 else 0
+ },
+ "overall_assessment": "",
+ "recommendation": ""
+ }
+
+ # تقييم شامل للمخاطر
+ if avg_risk_score >= 6.0:
+ results["overall_assessment"] = "مشروع عالي المخاطر"
+ results["recommendation"] = "ينصح بإعادة التفاوض على شروط العقد أو إضافة هامش ربح أعلى لتغطية المخاطر."
+ elif avg_risk_score >= 4.0:
+ results["overall_assessment"] = "مشروع متوسط المخاطر"
+ results["recommendation"] = "متابعة دقيقة للمخاطر العالية ووضع خطط استجابة مفصلة لها."
+ else:
+ results["overall_assessment"] = "مشروع منخفض المخاطر"
+ results["recommendation"] = "مراقبة المخاطر بشكل دوري والتركيز على تحسين الأداء."
+
+ return results
+
+ def _display_risk_analysis_results(self, results):
+ """عرض نتائج تحليل المخاطر"""
+
+ st.markdown("### نتائج تحليل المخاطر")
+
+ # عرض ملخص تقييم المخاطر
+ st.markdown(f"#### التقييم العام: {results['overall_assessment']}")
+
+ # عرض الإحصائيات الرئيسية للمخاطر
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ st.metric("متوسط درجة المخاطرة", f"{results['avg_risk_score']:.1f}/10")
+
+ with col2:
+ st.metric("المخاطر العالية", f"{results['high_risks']}")
+
+ with col3:
+ st.metric("المخاطر المتوسطة", f"{results['medium_risks']}")
+
+ with col4:
+ st.metric("المخاطر المنخفضة", f"{results['low_risks']}")
+
+ # عرض ملف المخاطر حسب الفئة
+ st.markdown("#### ملف المخاطر حسب الفئة")
+
+ # تجهيز البيانات للرسم البياني
+ risk_profile_data = pd.DataFrame({
+ 'الفئة': [
+ "مخاطر مالية",
+ "مخاطر فنية",
+ "مخاطر السوق",
+ "مخاطر الموارد",
+ "مخاطر تعاقدية",
+ "مخاطر تنظيمية"
+ ],
+ 'درجة المخاطرة': [
+ results['risk_profile']['financial_risk'],
+ results['risk_profile']['technical_risk'],
+ results['risk_profile']['market_risk'],
+ results['risk_profile']['resource_risk'],
+ results['risk_profile']['contract_risk'],
+ results['risk_profile']['regulatory_risk']
+ ]
+ })
+
+ # رسم مخطط شعاعي لملف المخاطر
+ fig = px.line_polar(
+ risk_profile_data,
+ r='درجة المخاطرة',
+ theta='الفئة',
+ line_close=True,
+ range_r=[0, 10],
+ title="ملف المخاطر حسب الفئة"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض المخاطر الرئيسية
+ st.markdown("#### المخاطر الرئيسية")
+
+ # إنشاء جدول المخاطر
+ risk_table_data = []
+
+ for risk in results['top_risks'][:5]: # عرض أعلى 5 مخاطر فقط
+ risk_level = "عالية" if risk["risk_score"] >= 6.0 else "متوسطة" if risk["risk_score"] >= 3.0 else "منخفضة"
+ risk_color = "red" if risk_level == "عالية" else "orange" if risk_level == "متوسطة" else "green"
+
+ risk_table_data.append({
+ "المعرف": risk["id"],
+ "الوصف": risk["name"],
+ "الفئة": risk["category"],
+ "الاحتمالية": f"{risk['probability'] * 100:.0f}%",
+ "التأثير": f"{risk['impact'] * 100:.0f}%",
+ "درجة المخاطرة": risk["risk_score"],
+ "المستوى": risk_level,
+ "استراتيجية الاستجابة": risk["response_strategy"],
+ "color": risk_color
+ })
+
+ # عرض جدول المخاطر
+ risk_df = pd.DataFrame(risk_table_data)
+
+ # استخدام تنسيق HTML مخصص لعرض المخاطر الرئيسية
+ for index, row in risk_df.iterrows():
+ with st.container():
+ st.markdown(f"""
+
+ """, unsafe_allow_html=True)
+
+ with col2:
+ st.button(f"تفاصيل #{alt['id']}", key=f"alt_details_{alt['id']}")
+
+ with col3:
+ if st.button(f"اختيار #{alt['id']}", key=f"select_alt_{alt['id']}"):
+ st.success(f"تم اختيار {alt['name']} كبديل لـ {selected_component}.")
+
+ # حساب تأثير البدائل على المحتوى المحلي الإجمالي
+ st.markdown("##### تأثير البدائل على المحتوى المحلي الإجمالي")
+
+ # محاكاة البيانات الإجمالية
+ total_value = 10000000
+ current_lc_value = 6000000
+ current_lc_percent = current_lc_value / total_value * 100
+
+ # حساب التأثير لكل بديل
+ impact_data = []
+ for alt in alternatives:
+ # القيمة الحالية للمحتوى المحلي في المكون
+ current_component_lc_value = selected_comp_data["value"] * selected_comp_data["local_content"] / 100
+
+ # القيمة المتوقعة للمحتوى المحلي مع البديل
+ new_component_value = selected_comp_data["value"] * alt["cost_factor"]
+ new_component_lc_value = new_component_value * alt["local_content"] / 100
+
+ # الفرق في قيمة المحتوى المحلي
+ lc_value_diff = new_component_lc_value - current_component_lc_value
+
+ # القيمة الإجمالية الجديدة للمشروع
+ new_total_value = total_value - selected_comp_data["value"] + new_component_value
+
+ # قيمة المحتوى المحلي الإجمالية الجديدة
+ new_total_lc_value = current_lc_value + lc_value_diff
+
+ # نسبة المحتوى المحلي الإجمالية الجديدة
+ new_total_lc_percent = new_total_lc_value / new_total_value * 100
+
+ # إضافة البيانات
+ impact_data.append({
+ "البديل": alt["name"],
+ "نسبة المحتوى المحلي الحالية": current_lc_percent,
+ "نسبة المحتوى المحلي المتوقعة": new_total_lc_percent,
+ "التغير": new_total_lc_percent - current_lc_percent,
+ "القيمة الإجمالية الجديدة": new_total_value,
+ "تقييم الجودة": alt["quality_rating"]
+ })
+
+ # عرض جدول التأثير
+ impact_df = pd.DataFrame(impact_data)
+
+ st.dataframe(
+ impact_df.style.format({
+ "نسبة المحتوى المحلي الحالية": "{:.1f}%",
+ "نسبة المحتوى المحلي المتوقعة": "{:.1f}%",
+ "التغير": "{:+.1f}%",
+ "القيمة الإجمالية الجديدة": "{:,.0f} ريال",
+ "تقييم الجودة": "{:.1f}/5"
+ }),
+ use_container_width=True
+ )
+
+ # مخطط مقارنة للبدائل
+ fig = px.bar(
+ impact_df,
+ x="البديل",
+ y=["نسبة المحتوى المحلي الحالية", "نسبة المحتوى المحلي المتوقعة"],
+ barmode="group",
+ title="مقارنة تأثير البدائل على نسبة المحتوى المحلي الإجمالية",
+ labels={"value": "نسبة المحتوى المحلي (%)", "variable": ""}
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # استخدام Claude AI للتحليل المتقدم
+ if st.checkbox("استخدام Claude AI لتحليل البدائل", value=False, key="lc_optimization_use_claude"):
+ with st.spinner("جاري تحليل البدائل..."):
+ # محاكاة وقت المعالجة
+ time.sleep(2)
+
+ try:
+ # إنشاء نص المدخلات للتحليل
+ prompt = f"""تحليل بدائل المحتوى المحلي لمكون {selected_component}:
+
+ المكون الحالي:
+ - الاسم: {selected_component}
+ - الفئة: {selected_comp_data['category']}
+ - القيمة: {selected_comp_data['value']:,} ريال
+ - نسبة المحتوى المحلي: {selected_comp_data['local_content']}%
+ - المورد: {selected_comp_data['supplier']}
+
+ البدائل المقترحة:
+ 1. {alternatives[0]['name']}:
+ - المحتوى المحلي: {alternatives[0]['local_content']}%
+ - معامل التكلفة: {alternatives[0]['cost_factor']:.2f}
+ - تقييم الجودة: {alternatives[0]['quality_rating']}/5
+ - الوصف: {alternatives[0]['description']}
+
+ 2. {alternatives[1]['name']}:
+ - المحتوى المحلي: {alternatives[1]['local_content']}%
+ - معامل التكلفة: {alternatives[1]['cost_factor']:.2f}
+ - تقييم الجودة: {alternatives[1]['quality_rating']}/5
+ - الوصف: {alternatives[1]['description']}
+
+ 3. {alternatives[2]['name']}:
+ - المحتوى المحلي: {alternatives[2]['local_content']}%
+ - معامل التكلفة: {alternatives[2]['cost_factor']:.2f}
+ - تقييم الجودة: {alternatives[2]['quality_rating']}/5
+ - الوصف: {alternatives[2]['description']}
+
+ المطلوب:
+ 1. تحليل مقارن شامل للبدائل من حيث المحتوى المحلي والتكلفة والجودة
+ 2. تحديد البديل الأفضل مع شرح أسباب اختياره
+ 3. تقديم توصيات إضافية لتحسين المحتوى المحلي لهذا المكون
+ 4. تحديد أي مخاطر محتملة في الانتقال للبديل المقترح
+
+ يرجى تقديم تحليل مهني ومختصر يركز على الجوانب الأكثر أهمية.
+ """
+
+ # استدعاء Claude للتحليل
+ claude_analysis = self.claude_service.chat_completion(
+ [{"role": "user", "content": prompt}]
+ )
+
+ if "error" not in claude_analysis:
+ # عرض تحليل Claude
+ st.markdown("##### تحليل متقدم للبدائل")
+ st.info(claude_analysis["content"])
+ else:
+ st.warning(f"تعذر إجراء التحليل المتقدم: {claude_analysis['error']}")
+ except Exception as e:
+ st.warning(f"تعذر إجراء التحليل المتقدم: {str(e)}")
+
+ # زر تطبيق البديل المختار
+ if st.button("تطبيق البديل المختار على المشروع"):
+ st.success("تم تطبيق البديل المختار على المشروع وتحديث نسبة المحتوى المحلي.")
+
+ def _render_faq_tab(self):
+ """عرض تبويب الأسئلة الشائعة"""
+
+ st.markdown("### الأسئلة الشائعة")
+
+ # البحث في الأسئلة الشائعة
+ search_query = st.text_input("البحث في الأسئلة الشائعة", key="faq_search")
+
+ # فلترة الأسئلة حسب البحث
+ if search_query:
+ filtered_faqs = [
+ faq for faq in self.faqs
+ if search_query.lower() in faq["question"].lower() or search_query.lower() in faq["answer"].lower()
+ ]
+ else:
+ filtered_faqs = self.faqs
+
+ # عرض الأسئلة والأجوبة
+ for i, faq in enumerate(filtered_faqs):
+ with st.expander(faq["question"]):
+ st.markdown(faq["answer"])
+
+ # زر التواصل مع الدعم
+ st.markdown("##### لم تجد إجابة لسؤالك؟")
+ col1, col2 = st.columns(2)
+
+ with col1:
+ if st.button("التواصل مع الدعم الفني", use_container_width=True):
+ st.info("سيتم التواصل معك قريباً من قبل فريق الدعم الفني.")
+
+ with col2:
+ if st.button("طرح سؤال جديد", use_container_width=True):
+ st.text_area("اكتب سؤالك هنا")
+ st.button("إرسال")
\ No newline at end of file
diff --git a/modules/ai_assistant/assistant_app.py b/modules/ai_assistant/assistant_app.py
new file mode 100644
index 0000000000000000000000000000000000000000..8306a78f6923fe730ef0c096c1e95c238e59ebe9
--- /dev/null
+++ b/modules/ai_assistant/assistant_app.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+تطبيق المساعد الذكي التفاعلي
+يتيح للمستخدمين التفاعل مع نماذج الذكاء الاصطناعي المتقدمة للحصول على مساعدة في تحليل العقود والمناقصات
+"""
+
+import os
+import sys
+import streamlit as st
+import pandas as pd
+import numpy as np
+
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
+
+# استيراد مكونات المساعد الذكي
+from modules.ai_assistant.ai_assistant import AIAssistant
+
+
+class AssistantApp:
+ """تطبيق المساعد الذكي التفاعلي"""
+
+ def __init__(self):
+ """تهيئة تطبيق المساعد الذكي"""
+ self.assistant = AIAssistant()
+
+ def render(self):
+ """عرض واجهة المستخدم الرئيسية للتطبيق"""
+ self.assistant.render()
+
+
+# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
+if __name__ == "__main__":
+ st.set_page_config(
+ page_title="المساعد الذكي | WAHBi AI",
+ page_icon="🤖",
+ layout="wide",
+ initial_sidebar_state="expanded"
+ )
+
+ app = AssistantApp()
+ app.render()
\ No newline at end of file
diff --git a/modules/ai_finetuning/__init__.py b/modules/ai_finetuning/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..070419ec30c2df5c5ba01ff2fa55212770dd1391
--- /dev/null
+++ b/modules/ai_finetuning/__init__.py
@@ -0,0 +1 @@
+# ملف تهيئة حزمة ضبط نماذج الذكاء الاصطناعي
\ No newline at end of file
diff --git a/modules/ai_finetuning/finetuning_app.py b/modules/ai_finetuning/finetuning_app.py
new file mode 100644
index 0000000000000000000000000000000000000000..a1dc1a7b8e4982246bd7567707b37e0b762a5b74
--- /dev/null
+++ b/modules/ai_finetuning/finetuning_app.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي للمصطلحات التعاقدية المتخصصة
+"""
+
+import os
+import sys
+import streamlit as st
+import pandas as pd
+import numpy as np
+
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
+
+# استيراد مكونات تخصيص وضبط نماذج الذكاء الاصطناعي
+from modules.ai_finetuning.model_finetuning import ModelFinetuning
+
+
+class FinetuningApp:
+ """وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي"""
+
+ def __init__(self):
+ """تهيئة وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي"""
+ self.model_finetuning = ModelFinetuning()
+
+ def render(self):
+ """عرض واجهة وحدة تطبيق تخصيص وضبط نماذج الذكاء الاصطناعي"""
+ st.markdown("
وحدة تخصيص وضبط نماذج الذكاء الاصطناعي
", unsafe_allow_html=True)
+
+ st.markdown("""
+
+ تمكنك هذه الوحدة من تخصيص وضبط نماذج الذكاء الاصطناعي للتعرف بدقة على المصطلحات التعاقدية والهندسية المتخصصة باللغة العربية.
+ يمكنك إنشاء قاموس للمصطلحات، وإعداد أمثلة التدريب، وتدريب النماذج واختبارها.
+
+ """, unsafe_allow_html=True)
+
+ # عرض نموذج تخصيص وضبط نماذج الذكاء الاصطناعي
+ self.model_finetuning.render()
+
+
+# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
+if __name__ == "__main__":
+ st.set_page_config(
+ page_title="تخصيص وضبط نماذج الذكاء الاصطناعي | WAHBi AI",
+ page_icon="🧠",
+ layout="wide",
+ initial_sidebar_state="expanded"
+ )
+
+ app = FinetuningApp()
+ app.render()
\ No newline at end of file
diff --git a/modules/ai_finetuning/model_finetuning.py b/modules/ai_finetuning/model_finetuning.py
new file mode 100644
index 0000000000000000000000000000000000000000..464b6c2455ca7b925a2920a3fab386449e407243
--- /dev/null
+++ b/modules/ai_finetuning/model_finetuning.py
@@ -0,0 +1,2081 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+وحدة تخصيص وضبط نماذج الذكاء الاصطناعي للمصطلحات التعاقدية المتخصصة
+تتيح هذه الوحدة إمكانية تدريب نماذج الذكاء الاصطناعي على المصطلحات المتخصصة في مجال العقود والمناقصات
+"""
+
+import os
+import sys
+import streamlit as st
+import pandas as pd
+import numpy as np
+import json
+import time
+import datetime
+from typing import List, Dict, Any, Optional, Tuple
+import openai
+import matplotlib.pyplot as plt
+import tempfile
+import csv
+import re
+import random
+from pathlib import Path
+
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
+
+# استيراد مكونات واجهة المستخدم
+from utils.components.header import render_header
+from utils.components.credits import render_credits
+from utils.helpers import format_number, format_currency, styled_button
+
+
+class ModelFinetuning:
+ """فئة تخصيص وضبط نماذج الذكاء الاصطناعي"""
+
+ def __init__(self):
+ """تهيئة وحدة تخصيص وضبط نماذج الذكاء الاصطناعي"""
+ # تهيئة مجلدات حفظ البيانات
+ self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/finetuning"))
+ os.makedirs(self.data_dir, exist_ok=True)
+
+ # تهيئة الملفات والمجلدات الفرعية
+ self.training_data_dir = os.path.join(self.data_dir, "training_data")
+ os.makedirs(self.training_data_dir, exist_ok=True)
+
+ self.models_dir = os.path.join(self.data_dir, "models")
+ os.makedirs(self.models_dir, exist_ok=True)
+
+ self.terminology_file = os.path.join(self.data_dir, "terminology.json")
+
+ # تهيئة حالة الجلسة
+ if 'terminology_data' not in st.session_state:
+ if os.path.exists(self.terminology_file):
+ with open(self.terminology_file, 'r', encoding='utf-8') as f:
+ st.session_state.terminology_data = json.load(f)
+ else:
+ st.session_state.terminology_data = {
+ "terms": [],
+ "training_examples": [],
+ "models": []
+ }
+
+ if 'active_training_job' not in st.session_state:
+ st.session_state.active_training_job = None
+
+ if 'training_results' not in st.session_state:
+ st.session_state.training_results = []
+
+ # ضبط API مفاتيح الذكاء الاصطناعي
+ self.api_key = os.environ.get("OPENAI_API_KEY")
+ self.anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY")
+
+ def render(self):
+ """عرض واجهة وحدة تخصيص وضبط نماذج الذكاء الاصطناعي"""
+ # عرض الشعار والعنوان الرئيسي
+ render_header("تخصيص وضبط نماذج الذكاء الاصطناعي للمصطلحات التعاقدية المتخصصة")
+
+ # تبويبات الوحدة
+ tabs = st.tabs([
+ "قاموس المصطلحات المتخصصة",
+ "إعداد بيانات التدريب",
+ "تدريب النموذج",
+ "اختبار النموذج",
+ "المساعد المتخصص"
+ ])
+
+ # تبويب قاموس المصطلحات المتخصصة
+ with tabs[0]:
+ self._render_terminology_dictionary()
+
+ # تبويب إعداد بيانات التدريب
+ with tabs[1]:
+ self._render_training_data_setup()
+
+ # تبويب تدريب النموذج
+ with tabs[2]:
+ self._render_model_training()
+
+ # تبويب اختبار النموذج
+ with tabs[3]:
+ self._render_model_testing()
+
+ # تبويب المساعد المتخصص
+ with tabs[4]:
+ self._render_specialized_assistant()
+
+ # عرض حقوق النشر
+ render_credits()
+
+ def _render_terminology_dictionary(self):
+ """عرض قاموس المصطلحات المتخصصة"""
+ st.markdown("""
+
+
📚 قاموس المصطلحات المتخصصة
+
أضف وحرر المصطلحات الفنية المتخصصة في مجال العقود والمناقصات باللغة العربية.
+
هذه المصطلحات ستستخدم لتدريب وضبط نماذج الذكاء الاصطناعي للتعرف عليها بدقة عالية.
+
+ """, unsafe_allow_html=True)
+
+ # إضافة مصطلح جديد
+ st.markdown("### إضافة مصطلح جديد")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ term = st.text_input("المصطلح", key="new_term")
+ category = st.selectbox(
+ "الفئة",
+ options=[
+ "شروط تعاقدية", "مواصفات فنية", "مستندات مناقصة",
+ "بنود مالية", "جداول كميات", "ضمانات", "مصطلحات قانونية",
+ "محتوى محلي", "أخرى"
+ ],
+ key="new_term_category"
+ )
+
+ with col2:
+ english_term = st.text_input("المصطلح بالإنجليزية (اختياري)", key="new_term_english")
+ importance = st.slider("مستوى الأهمية", 1, 5, 3, key="new_term_importance")
+
+ definition = st.text_area("التعريف", key="new_term_definition")
+ examples = st.text_area("أمثلة على استخدام المصطلح (فصل بين الأمثلة بسطر جديد)", key="new_term_examples")
+
+ # زر إضافة المصطلح
+ if styled_button("إضافة المصطلح", key="add_term", type="primary", icon="➕"):
+ if not term or not definition:
+ st.error("يرجى تعبئة المصطلح والتعريف على الأقل.")
+ else:
+ # إنشاء كائن المصطلح
+ new_term = {
+ "term": term,
+ "definition": definition,
+ "category": category,
+ "english_term": english_term,
+ "importance": importance,
+ "examples": [ex.strip() for ex in examples.split("\n") if ex.strip()],
+ "added_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ }
+
+ # إضافة المصطلح للقائمة
+ st.session_state.terminology_data["terms"].append(new_term)
+
+ # حفظ البيانات
+ self._save_terminology_data()
+
+ st.success(f"تمت إضافة المصطلح '{term}' بنجاح!")
+ st.rerun()
+
+ # عرض المصطلحات الموجودة
+ st.markdown("### المصطلحات الموجودة")
+
+ terms = st.session_state.terminology_data.get("terms", [])
+
+ if not terms:
+ st.info("لا توجد مصطلحات مضافة. يرجى إضافة مصطلحات جديدة.")
+ else:
+ # تصفية المصطلحات
+ filter_col1, filter_col2 = st.columns(2)
+
+ with filter_col1:
+ filter_category = st.selectbox(
+ "تصفية حسب الفئة",
+ options=["الكل"] + list(set(t.get("category") for t in terms)),
+ key="filter_term_category"
+ )
+
+ with filter_col2:
+ search_query = st.text_input("بحث", key="search_term")
+
+ # تطبيق التصفية
+ filtered_terms = terms
+ if filter_category != "الكل":
+ filtered_terms = [t for t in filtered_terms if t.get("category") == filter_category]
+
+ if search_query:
+ filtered_terms = [
+ t for t in filtered_terms
+ if search_query.lower() in t.get("term", "").lower() or
+ search_query.lower() in t.get("definition", "").lower() or
+ search_query.lower() in t.get("english_term", "").lower()
+ ]
+
+ # عرض المصطلحات المصفاة
+ if not filtered_terms:
+ st.warning("لا توجد مصطلحات تطابق معايير التصفية.")
+ else:
+ # إعداد بيانات للعرض
+ for i, term in enumerate(filtered_terms):
+ with st.expander(f"{term.get('term')} ({term.get('english_term', '')})", expanded=i==0 and len(filtered_terms)<5):
+ term_col1, term_col2 = st.columns([3, 1])
+
+ with term_col1:
+ st.markdown(f"**التعريف:** {term.get('definition')}")
+ st.markdown(f"**الفئة:** {term.get('category')}")
+ st.markdown(f"**المصطلح بالإنجليزية:** {term.get('english_term', '-')}")
+
+ if "examples" in term and term["examples"]:
+ st.markdown("**أمثلة:**")
+ for ex in term["examples"]:
+ st.markdown(f"- {ex}")
+
+ with term_col2:
+ st.markdown(f"**مستوى الأهمية:** {'⭐' * term.get('importance', 3)}")
+ st.markdown(f"**تاريخ الإضافة:** {term.get('added_at', '-')}")
+
+ # أزرار التحرير والحذف
+ if styled_button("تحرير", key=f"edit_term_{i}", type="secondary", icon="✏️"):
+ st.session_state.term_to_edit = i
+
+ if styled_button("حذف", key=f"delete_term_{i}", type="danger", icon="🗑️"):
+ st.session_state.term_to_delete = i
+
+ # معالجة تحرير أو حذف المصطلح
+ if "term_to_edit" in st.session_state:
+ self._render_edit_term_form(st.session_state.term_to_edit, filtered_terms)
+
+ if "term_to_delete" in st.session_state:
+ if st.warning(f"هل أنت متأكد من حذف المصطلح '{filtered_terms[st.session_state.term_to_delete].get('term')}'؟"):
+ if styled_button("تأكيد الحذف", key="confirm_delete", type="danger", icon="🗑️"):
+ # حذف المصطلح
+ term_index = terms.index(filtered_terms[st.session_state.term_to_delete])
+ del st.session_state.terminology_data["terms"][term_index]
+
+ # حفظ البيانات
+ self._save_terminology_data()
+
+ # إعادة ضبط حالة الحذف
+ del st.session_state.term_to_delete
+
+ st.success("تم حذف المصطلح بنجاح!")
+ st.rerun()
+
+ if styled_button("إلغاء", key="cancel_delete", type="secondary", icon="❌"):
+ del st.session_state.term_to_delete
+ st.rerun()
+
+ # تصدير المصطلحات
+ st.markdown("### تصدير وتوريد المصطلحات")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ if styled_button("تصدير المصطلحات إلى CSV", key="export_terms", type="primary", icon="📤"):
+ self._export_terms_to_csv()
+
+ with col2:
+ uploaded_file = st.file_uploader("استيراد المصطلحات من ملف CSV", type=["csv"], key="import_terms_file")
+
+ if uploaded_file is not None:
+ if styled_button("استيراد المصطلحات", key="import_terms", type="success", icon="📥"):
+ self._import_terms_from_csv(uploaded_file)
+
+ def _render_edit_term_form(self, term_index, terms_list):
+ """عرض نموذج تحرير المصطلح"""
+ term = terms_list[term_index]
+
+ st.markdown("### تحرير المصطلح")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ edited_term = st.text_input("المصطلح", value=term.get("term", ""), key="edit_term_name")
+ edited_category = st.selectbox(
+ "الفئة",
+ options=[
+ "شروط تعاقدية", "مواصفات فنية", "مستندات مناقصة",
+ "بنود مالية", "جداول كميات", "ضمانات", "مصطلحات قانونية",
+ "محتوى محلي", "أخرى"
+ ],
+ index=["شروط تعاقدية", "مواصفات فنية", "مستندات مناقصة", "بنود مالية", "جداول كميات", "ضمانات", "مصطلحات قانونية", "محتوى محلي", "أخرى"].index(term.get("category", "أخرى")),
+ key="edit_term_category"
+ )
+
+ with col2:
+ edited_english_term = st.text_input("المصطلح بالإنجليزية (اختياري)", value=term.get("english_term", ""), key="edit_term_english")
+ edited_importance = st.slider("مستوى الأهمية", 1, 5, term.get("importance", 3), key="edit_term_importance")
+
+ edited_definition = st.text_area("التعريف", value=term.get("definition", ""), key="edit_term_definition")
+ edited_examples = st.text_area("أمثلة على استخدام المصطلح (فصل بين الأمثلة بسطر جديد)", value="\n".join(term.get("examples", [])), key="edit_term_examples")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ if styled_button("حفظ التغييرات", key="save_edited_term", type="primary", icon="💾"):
+ if not edited_term or not edited_definition:
+ st.error("يرجى تعبئة المصطلح والتعريف على الأقل.")
+ else:
+ # تحديث المصطلح
+ updated_term = {
+ "term": edited_term,
+ "definition": edited_definition,
+ "category": edited_category,
+ "english_term": edited_english_term,
+ "importance": edited_importance,
+ "examples": [ex.strip() for ex in edited_examples.split("\n") if ex.strip()],
+ "added_at": term.get("added_at", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
+ "updated_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ }
+
+ # الحصول على المؤشر الفعلي في القائمة الكاملة
+ all_terms = st.session_state.terminology_data["terms"]
+ actual_index = all_terms.index(term)
+
+ # تحديث المصطلح
+ st.session_state.terminology_data["terms"][actual_index] = updated_term
+
+ # حفظ البيانات
+ self._save_terminology_data()
+
+ # إعادة ضبط حالة التحرير
+ del st.session_state.term_to_edit
+
+ st.success(f"تم تحديث المصطلح '{edited_term}' بنجاح!")
+ st.rerun()
+
+ with col2:
+ if styled_button("إلغاء", key="cancel_edit_term", type="secondary", icon="❌"):
+ del st.session_state.term_to_edit
+ st.rerun()
+
+ def _export_terms_to_csv(self):
+ """تصدير المصطلحات إلى ملف CSV"""
+ terms = st.session_state.terminology_data.get("terms", [])
+
+ if not terms:
+ st.error("لا توجد مصطلحات للتصدير.")
+ return
+
+ # إنشاء ملف CSV مؤقت
+ with tempfile.NamedTemporaryFile(mode='w+', suffix='.csv', newline='', encoding='utf-8', delete=False) as f:
+ writer = csv.writer(f)
+
+ # كتابة الترويسة
+ writer.writerow([
+ 'المصطلح', 'التعريف', 'الفئة', 'المصطلح بالإنجليزية',
+ 'مستوى الأهمية', 'الأمثلة', 'تاريخ الإضافة'
+ ])
+
+ # كتابة المصطلحات
+ for term in terms:
+ writer.writerow([
+ term.get('term', ''),
+ term.get('definition', ''),
+ term.get('category', ''),
+ term.get('english_term', ''),
+ term.get('importance', 3),
+ '|'.join(term.get('examples', [])),
+ term.get('added_at', '')
+ ])
+
+ # الحصول على مسار الملف
+ csv_path = f.name
+
+ # قراءة الملف وتقديمه للتنزيل
+ with open(csv_path, 'r', encoding='utf-8') as f:
+ csv_data = f.read()
+
+ # تقديم الملف للتنزيل
+ st.download_button(
+ label="تنزيل ملف CSV",
+ data=csv_data,
+ file_name="terminology_dictionary.csv",
+ mime="text/csv"
+ )
+
+ # حذف الملف المؤقت
+ os.unlink(csv_path)
+
+ def _import_terms_from_csv(self, uploaded_file):
+ """استيراد المصطلحات من ملف CSV"""
+ try:
+ # قراءة الملف
+ df = pd.read_csv(uploaded_file, encoding='utf-8')
+
+ # التحقق من وجود الأعمدة المطلوبة
+ required_columns = ['المصطلح', 'التعريف']
+ missing_columns = [col for col in required_columns if col not in df.columns]
+
+ if missing_columns:
+ st.error(f"الملف لا يحتوي على الأعمدة التالية: {', '.join(missing_columns)}")
+ return
+
+ # إضافة المصطلحات
+ terms_added = 0
+ terms_updated = 0
+
+ for _, row in df.iterrows():
+ term = row['المصطلح']
+
+ # البحث عن المصطلح الموجود
+ existing_term = next((t for t in st.session_state.terminology_data["terms"] if t.get("term") == term), None)
+
+ # تحضير كائن المصطلح
+ term_obj = {
+ "term": term,
+ "definition": row.get('التعريف', ''),
+ "category": row.get('الفئة', 'أخرى'),
+ "english_term": row.get('المصطلح بالإنجليزية', ''),
+ "importance": int(row.get('مستوى الأهمية', 3)),
+ "examples": row.get('الأمثلة', '').split('|') if 'الأمثلة' in row else [],
+ "added_at": row.get('تاريخ الإضافة', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
+ }
+
+ if existing_term:
+ # تحديث المصطلح الموجود
+ index = st.session_state.terminology_data["terms"].index(existing_term)
+ st.session_state.terminology_data["terms"][index] = term_obj
+ terms_updated += 1
+ else:
+ # إضافة مصطلح جديد
+ st.session_state.terminology_data["terms"].append(term_obj)
+ terms_added += 1
+
+ # حفظ البيانات
+ self._save_terminology_data()
+
+ st.success(f"تم استيراد المصطلحات بنجاح! (تمت إضافة {terms_added} مصطلح جديد، وتحديث {terms_updated} مصطلح موجود)")
+ st.rerun()
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء استيراد المصطلحات: {str(e)}")
+
+ def _render_training_data_setup(self):
+ """عرض إعداد بيانات التدريب"""
+ st.markdown("""
+
+
🔬 إعداد بيانات التدريب
+
قم بإنشاء وتحرير أمثلة التدريب لضبط نماذج الذكاء الاصطناعي على المصطلحات المتخصصة.
+
يمكنك إنشاء أمثلة يدوياً أو استيرادها من ملف أو توليدها تلقائياً باستخدام نماذج الذكاء الاصطناعي الحالية.
+
+ """, unsafe_allow_html=True)
+
+ # تبويبات إعداد البيانات
+ training_tabs = st.tabs(["أمثلة التدريب الحالية", "إنشاء أمثلة يدوياً", "توليد أمثلة تلقائياً", "استيراد وتصدير البيانات"])
+
+ # عرض أمثلة التدريب الحالية
+ with training_tabs[0]:
+ self._render_existing_training_examples()
+
+ # إنشاء أمثلة يدوياً
+ with training_tabs[1]:
+ self._render_manual_example_creation()
+
+ # توليد أمثلة تلقائياً
+ with training_tabs[2]:
+ self._render_automatic_example_generation()
+
+ # استيراد وتصدير البيانات
+ with training_tabs[3]:
+ self._render_import_export_training_data()
+
+ def _render_existing_training_examples(self):
+ """عرض أمثلة التدريب الحالية"""
+ st.markdown("### أمثلة التدريب الحالية")
+
+ examples = st.session_state.terminology_data.get("training_examples", [])
+
+ if not examples:
+ st.info("لا توجد أمثلة تدريب. يرجى إنشاء أمثلة جديدة.")
+ return
+
+ # عرض إحصائيات البيانات
+ st.markdown("#### إحصائيات البيانات")
+
+ total_examples = len(examples)
+ categories = {}
+ terms_used = set()
+
+ for ex in examples:
+ cat = ex.get("category", "غير مصنف")
+ categories[cat] = categories.get(cat, 0) + 1
+
+ for term in ex.get("terms", []):
+ terms_used.add(term)
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric("إجمالي الأمثلة", total_examples)
+
+ with col2:
+ st.metric("عدد المصطلحات المستخدمة", len(terms_used))
+
+ with col3:
+ st.metric("عدد الفئات", len(categories))
+
+ # عرض توزيع الفئات
+ st.markdown("#### توزيع الأمثلة حسب الفئة")
+
+ categories_df = pd.DataFrame({
+ "الفئة": list(categories.keys()),
+ "عدد الأمثلة": list(categories.values())
+ })
+
+ fig, ax = plt.subplots(figsize=(10, 6))
+ ax.bar(categories_df["الفئة"], categories_df["عدد الأمثلة"])
+ ax.set_title("توزيع أمثلة التدريب حسب الفئة")
+ ax.set_xlabel("الفئة")
+ ax.set_ylabel("عدد الأمثلة")
+
+ # تدوير أسماء الفئات لتسهيل القراءة
+ plt.xticks(rotation=45, ha='right')
+ plt.tight_layout()
+
+ st.pyplot(fig)
+
+ # تصفية الأمثلة
+ st.markdown("#### تصفية الأمثلة")
+
+ filter_col1, filter_col2 = st.columns(2)
+
+ with filter_col1:
+ filter_category = st.selectbox(
+ "تصفية حسب الفئة",
+ options=["الكل"] + list(categories.keys()),
+ key="filter_example_category"
+ )
+
+ with filter_col2:
+ search_query = st.text_input("بحث في النص", key="search_example")
+
+ # تطبيق التصفية
+ filtered_examples = examples
+ if filter_category != "الكل":
+ filtered_examples = [ex for ex in filtered_examples if ex.get("category") == filter_category]
+
+ if search_query:
+ filtered_examples = [
+ ex for ex in filtered_examples
+ if search_query.lower() in ex.get("input", "").lower() or
+ search_query.lower() in ex.get("output", "").lower()
+ ]
+
+ # عرض الأمثلة المصفاة
+ if not filtered_examples:
+ st.warning("لا توجد أمثلة تطابق معايير التصفية.")
+ else:
+ # عرض عدد محدود من الأمثلة في كل صفحة
+ examples_per_page = 10
+ total_pages = (len(filtered_examples) - 1) // examples_per_page + 1
+
+ # التنقل بين الصفحات
+ col1, col2, col3 = st.columns([1, 3, 1])
+
+ with col2:
+ page = st.slider("الصفحة", 1, max(1, total_pages), 1, key="examples_page")
+
+ start_idx = (page - 1) * examples_per_page
+ end_idx = min(start_idx + examples_per_page, len(filtered_examples))
+
+ page_examples = filtered_examples[start_idx:end_idx]
+
+ # عرض الأمثلة
+ for i, example in enumerate(page_examples):
+ example_idx = start_idx + i
+ with st.expander(f"مثال #{example_idx+1} - {example.get('category', 'غير مصنف')}", expanded=i==0 and len(page_examples)<5):
+ ex_col1, ex_col2 = st.columns([3, 1])
+
+ with ex_col1:
+ st.markdown("**النص المدخل:**")
+ st.markdown(f"```\n{example.get('input', '')}\n```")
+
+ st.markdown("**النص المتوقع:**")
+ st.markdown(f"```\n{example.get('output', '')}\n```")
+
+ with ex_col2:
+ st.markdown("**الفئة:** " + example.get('category', 'غير مصنف'))
+ st.markdown("**المصطلحات المستخدمة:**")
+ for term in example.get("terms", []):
+ st.markdown(f"- {term}")
+
+ # تاريخ الإنشاء
+ if "created_at" in example:
+ st.markdown(f"**تاريخ الإنشاء:** {example['created_at']}")
+
+ # أزرار التحرير والحذف
+ if styled_button("تحرير", key=f"edit_example_{example_idx}", type="secondary", icon="✏️"):
+ st.session_state.example_to_edit = example_idx
+
+ if styled_button("حذف", key=f"delete_example_{example_idx}", type="danger", icon="🗑️"):
+ st.session_state.example_to_delete = example_idx
+
+ # معالجة تحرير أو حذف مثال
+ if "example_to_edit" in st.session_state:
+ self._render_edit_example_form(st.session_state.example_to_edit, filtered_examples)
+
+ if "example_to_delete" in st.session_state:
+ if st.warning(f"هل أنت متأكد من حذف المثال #{st.session_state.example_to_delete+1}؟"):
+ if styled_button("تأكيد الحذف", key="confirm_delete_example", type="danger", icon="🗑️"):
+ # حذف المثال
+ example_index = examples.index(filtered_examples[st.session_state.example_to_delete])
+ del st.session_state.terminology_data["training_examples"][example_index]
+
+ # حفظ البيانات
+ self._save_terminology_data()
+
+ # إعادة ضبط حالة الحذف
+ del st.session_state.example_to_delete
+
+ st.success("تم حذف المثال بنجاح!")
+ st.rerun()
+
+ if styled_button("إلغاء", key="cancel_delete_example", type="secondary", icon="❌"):
+ del st.session_state.example_to_delete
+ st.rerun()
+
+ def _render_edit_example_form(self, example_index, examples_list):
+ """عرض نموذج تحرير مثال التدريب"""
+ example = examples_list[example_index]
+
+ st.markdown("### تحرير مثال التدريب")
+
+ # اختيار المصطلحات المستخدمة
+ all_terms = [term.get("term") for term in st.session_state.terminology_data.get("terms", [])]
+ selected_terms = st.multiselect(
+ "المصطلحات المستخدمة في المثال",
+ options=all_terms,
+ default=example.get("terms", []),
+ key="edit_example_terms"
+ )
+
+ # إدخال النص المدخل والمتوقع
+ input_text = st.text_area("النص المدخل", value=example.get("input", ""), key="edit_example_input", height=150)
+ output_text = st.text_area("النص المتوقع", value=example.get("output", ""), key="edit_example_output", height=150)
+
+ # اختيار الفئة
+ category = st.selectbox(
+ "الفئة",
+ options=["شروط تعاقدية", "مواصفات فنية", "مستندات مناقصة", "بنود مالية", "جداول كميات", "ضمانات", "مصطلحات قانونية", "محتوى محلي", "أخرى"],
+ index=["شروط تعاقدية", "مواصفات فنية", "مستندات مناقصة", "بنود مالية", "جداول كميات", "ضمانات", "مصطلحات قانونية", "محتوى محلي", "أخرى"].index(example.get("category", "أخرى")),
+ key="edit_example_category"
+ )
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ if styled_button("حفظ التغييرات", key="save_edited_example", type="primary", icon="💾"):
+ if not input_text or not output_text:
+ st.error("يرجى تعبئة النص المدخل والنص المتوقع.")
+ else:
+ # تحديث المثال
+ updated_example = {
+ "input": input_text,
+ "output": output_text,
+ "category": category,
+ "terms": selected_terms,
+ "created_at": example.get("created_at", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
+ "updated_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ }
+
+ # الحصول على المؤشر الفعلي في القائمة الكاملة
+ all_examples = st.session_state.terminology_data["training_examples"]
+ actual_index = all_examples.index(example)
+
+ # تحديث المثال
+ st.session_state.terminology_data["training_examples"][actual_index] = updated_example
+
+ # حفظ البيانات
+ self._save_terminology_data()
+
+ # إعادة ضبط حالة التحرير
+ del st.session_state.example_to_edit
+
+ st.success("تم تحديث مثال التدريب بنجاح!")
+ st.rerun()
+
+ with col2:
+ if styled_button("إلغاء", key="cancel_edit_example", type="secondary", icon="❌"):
+ del st.session_state.example_to_edit
+ st.rerun()
+
+ def _render_manual_example_creation(self):
+ """عرض نموذج إنشاء أمثلة يدوياً"""
+ st.markdown("### إنشاء مثال تدريب جديد")
+
+ # اختيار المصطلحات المستخدمة
+ all_terms = [term.get("term") for term in st.session_state.terminology_data.get("terms", [])]
+ selected_terms = st.multiselect(
+ "المصطلحات المستخدمة في المثال",
+ options=all_terms,
+ key="new_example_terms"
+ )
+
+ # اختيار الفئة
+ category = st.selectbox(
+ "الفئة",
+ options=["شروط تعاقدية", "مواصفات فنية", "مستندات مناقصة", "بنود مالية", "جداول كميات", "ضمانات", "مصطلحات قانونية", "محتوى محلي", "أخرى"],
+ key="new_example_category"
+ )
+
+ # إدخال النص المدخل والمتوقع
+ st.markdown("**النص المدخل** (نص السؤال أو الطلب)")
+ input_text = st.text_area("", key="new_example_input", height=150, placeholder="مثال: قم بشرح معنى مصطلح 'محتوى محلي' وكيفية حسابه في المشاريع الحكومية.")
+
+ st.markdown("**النص المتوقع** (الإجابة المثالية التي يجب أن يقدمها النموذج)")
+ output_text = st.text_area("", key="new_example_output", height=150, placeholder="مثال: المحتوى المحلي (Local Content) هو نسبة القيمة المحلية المضافة في المنتجات والخدمات المقدمة في المشروع...")
+
+ # عرض تعريفات المصطلحات المختارة للمساعدة
+ if selected_terms:
+ with st.expander("تعريفات المصطلحات المختارة", expanded=True):
+ for term_name in selected_terms:
+ term = next((t for t in st.session_state.terminology_data.get("terms", []) if t.get("term") == term_name), None)
+ if term:
+ st.markdown(f"**{term_name}**: {term.get('definition', '')}")
+
+ # زر إضافة المثال
+ if styled_button("إضافة مثال التدريب", key="add_example", type="primary", icon="➕"):
+ if not input_text or not output_text:
+ st.error("يرجى تعبئة النص المدخل والنص المتوقع.")
+ else:
+ # إنشاء كائن المثال
+ new_example = {
+ "input": input_text,
+ "output": output_text,
+ "category": category,
+ "terms": selected_terms,
+ "created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ }
+
+ # إضافة المثال للقائمة
+ if "training_examples" not in st.session_state.terminology_data:
+ st.session_state.terminology_data["training_examples"] = []
+
+ st.session_state.terminology_data["training_examples"].append(new_example)
+
+ # حفظ البيانات
+ self._save_terminology_data()
+
+ st.success("تم إضافة مثال التدريب بنجاح!")
+ st.rerun()
+
+ def _render_automatic_example_generation(self):
+ """عرض واجهة توليد أمثلة تلقائياً"""
+ st.markdown("### توليد أمثلة تدريب تلقائياً")
+
+ # التحقق من وجود مفاتيح API
+ if not self.api_key and not self.anthropic_api_key:
+ st.warning("لم يتم العثور على مفاتيح API للذكاء الاصطناعي. يرجى إضافة OPENAI_API_KEY أو ANTHROPIC_API_KEY إلى المتغيرات البيئية.")
+ return
+
+ # اختيار نموذج الذكاء الاصطناعي
+ ai_models = []
+
+ if self.api_key:
+ ai_models.extend(["gpt-4o", "gpt-3.5-turbo"])
+
+ if self.anthropic_api_key:
+ ai_models.extend(["claude-3-7-sonnet-20250219"])
+
+ selected_model = st.selectbox(
+ "اختر نموذج الذكاء الاصطناعي",
+ options=ai_models,
+ key="auto_gen_model"
+ )
+
+ # اختيار المصطلحات لتوليد أمثلة حولها
+ all_terms = [term.get("term") for term in st.session_state.terminology_data.get("terms", [])]
+ selected_terms = st.multiselect(
+ "اختر المصطلحات لتوليد أمثلة حولها",
+ options=all_terms,
+ key="auto_gen_terms"
+ )
+
+ # اختيار عدد الأمثلة المراد توليدها
+ num_examples = st.slider("عدد الأمثلة لكل مصطلح", 1, 5, 2, key="auto_gen_count")
+
+ # اختيار الفئات المرغوبة
+ selected_categories = st.multiselect(
+ "اختر الفئات المرغوبة للأمثلة",
+ options=["شروط تعاقدية", "مواصفات فنية", "مستندات مناقصة", "بنود مالية", "جداول كميات", "ضمانات", "مصطلحات قانونية", "محتوى محلي", "أخرى"],
+ default=["شروط تعاقدية", "مستندات مناقصة", "مصطلحات قانونية"],
+ key="auto_gen_categories"
+ )
+
+ # زر توليد الأمثلة
+ if styled_button("توليد الأمثلة", key="generate_examples", type="primary", icon="✨"):
+ if not selected_terms:
+ st.error("يرجى اختيار مصطلح واحد على الأقل.")
+ elif not selected_categories:
+ st.error("يرجى اختيار فئة واحدة على الأقل.")
+ else:
+ # عرض شريط التقدم
+ progress_bar = st.progress(0)
+ status_text = st.empty()
+
+ # تجهيز المصطلحات وتعريفاتها
+ terms_with_definitions = {}
+ for term_name in selected_terms:
+ term = next((t for t in st.session_state.terminology_data.get("terms", []) if t.get("term") == term_name), None)
+ if term:
+ terms_with_definitions[term_name] = term.get('definition', '')
+
+ # توليد الأمثلة
+ generated_examples = []
+ total_iterations = len(selected_terms) * len(selected_categories)
+ current_iteration = 0
+
+ for term_name, definition in terms_with_definitions.items():
+ for category in selected_categories:
+ current_iteration += 1
+ progress = current_iteration / total_iterations
+ progress_bar.progress(progress)
+ status_text.text(f"جاري توليد أمثلة للمصطلح '{term_name}' في الفئة '{category}'...")
+
+ # توليد أمثلة لهذا المصطلح والفئة
+ examples = self._generate_examples_with_ai(
+ term_name,
+ definition,
+ category,
+ num_examples,
+ selected_model
+ )
+
+ generated_examples.extend(examples)
+
+ # إضافة الأمثلة المولدة إلى البيانات
+ if "training_examples" not in st.session_state.terminology_data:
+ st.session_state.terminology_data["training_examples"] = []
+
+ st.session_state.terminology_data["training_examples"].extend(generated_examples)
+
+ # حفظ البيانات
+ self._save_terminology_data()
+
+ # إكمال شريط التقدم
+ progress_bar.progress(1.0)
+ status_text.text(f"تم توليد {len(generated_examples)} مثال بنجاح!")
+
+ st.success(f"تم توليد {len(generated_examples)} مثال بنجاح!")
+ st.rerun()
+
+ def _generate_examples_with_ai(self, term_name, definition, category, num_examples, model):
+ """توليد أمثلة باستخدام الذكاء الاصطناعي"""
+ # تحضير الرسالة
+ prompt = f"""
+ أنت خبير في توليد أمثلة تدريب لضبط نماذج الذكاء الاصطناعي على المصطلحات التعاقدية والهندسية المتخصصة باللغة العربية.
+
+ أريد منك توليد {num_examples} مثال تدريب للمصطلح التالي:
+
+ المصطلح: {term_name}
+ التعريف: {definition}
+ الفئة: {category}
+
+ لكل مثال، قم بتوليد:
+ 1. نص المدخل (سؤال أو طلب حول المصطلح)
+ 2. نص المخرج المتوقع (الإجابة المثالية التي يجب أن يقدمها النموذج)
+
+ تأكد من:
+ - جعل الأمثلة متنوعة وواقعية
+ - تضمين سياقات مختلفة لاستخدام المصطلح
+ - استخدام أسلوب مناسب لوثائق المناقصات والعقود
+ - تضمين تفاصيل تقنية دقيقة عند الحاجة
+
+ قم بإرجاع النتائج بتنسيق JSON كما يلي:
+
+ ```json
+ [
+ {
+ "input": "نص المدخل للمثال الأول",
+ "output": "نص المخرج المتوقع للمثال الأول"
+ },
+ {
+ "input": "نص المدخل للمثال الثاني",
+ "output": "نص المخرج المتوقع للمثال الثاني"
+ },
+ ...
+ ]
+ ```
+
+ أرجع البيانات بتنسيق JSON فقط.
+ """
+
+ try:
+ # استدعاء API المناسب حسب النموذج المختار
+ if "gpt" in model and self.api_key:
+ # استخدام OpenAI API
+ openai.api_key = self.api_key
+
+ response = openai.chat.completions.create(
+ model=model,
+ messages=[
+ {"role": "system", "content": "أنت مساعد محترف متخصص في توليد بيانات تدريب لضبط نماذج الذكاء الاصطناعي."},
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.7,
+ response_format={"type": "json_object"}
+ )
+
+ # استخراج النتيجة
+ result_text = response.choices[0].message.content
+
+ # تنظيف النص واستخراج JSON
+ json_match = re.search(r'```json\s*(.*?)\s*```', result_text, re.DOTALL)
+ if json_match:
+ result_json = json_match.group(1)
+ else:
+ result_json = result_text
+
+ # تحليل JSON
+ examples_data = json.loads(result_json)
+
+ # إذا كان الناتج كائن JSON بخاصية examples
+ if isinstance(examples_data, dict) and "examples" in examples_data:
+ examples_data = examples_data["examples"]
+
+ elif "claude" in model and self.anthropic_api_key:
+ # استخدام Anthropic API
+ from anthropic import Anthropic
+
+ anthropic_client = Anthropic(api_key=self.anthropic_api_key)
+
+ response = anthropic_client.messages.create(
+ model=model,
+ max_tokens=4000,
+ messages=[
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.7
+ )
+
+ # استخراج النتيجة
+ result_text = response.content[0].text
+
+ # تنظيف النص واستخراج JSON
+ json_match = re.search(r'```json\s*(.*?)\s*```', result_text, re.DOTALL)
+ if json_match:
+ result_json = json_match.group(1)
+ else:
+ result_json = result_text
+
+ # تحليل JSON
+ examples_data = json.loads(result_json)
+
+ # إذا كان الناتج كائن JSON بخاصية examples
+ if isinstance(examples_data, dict) and "examples" in examples_data:
+ examples_data = examples_data["examples"]
+
+ else:
+ # في حالة عدم توفر النموذج المطلوب
+ return []
+
+ # تحويل البيانات إلى الصيغة المطلوبة للأمثلة
+ formatted_examples = []
+
+ for example in examples_data:
+ formatted_examples.append({
+ "input": example["input"],
+ "output": example["output"],
+ "category": category,
+ "terms": [term_name],
+ "created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "generated_by": model
+ })
+
+ return formatted_examples
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء توليد الأمثلة: {str(e)}")
+ return []
+
+ def _render_import_export_training_data(self):
+ """عرض واجهة استيراد وتصدير بيانات التدريب"""
+ st.markdown("### استيراد وتصدير بيانات التدريب")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.markdown("#### تصدير بيانات التدريب")
+
+ export_format = st.selectbox(
+ "صيغة التصدير",
+ options=["JSON", "JSONL", "CSV"],
+ key="export_format"
+ )
+
+ if styled_button("تصدير البيانات", key="export_data", type="primary", icon="📤"):
+ self._export_training_data(export_format)
+
+ with col2:
+ st.markdown("#### استيراد بيانات التدريب")
+
+ import_format = st.selectbox(
+ "صيغة الاستيراد",
+ options=["JSON", "JSONL", "CSV"],
+ key="import_format"
+ )
+
+ uploaded_file = st.file_uploader("استيراد بيانات التدريب", type=["json", "jsonl", "csv"], key="import_data_file")
+
+ if uploaded_file is not None:
+ if styled_button("استيراد البيانات", key="import_data", type="success", icon="📥"):
+ self._import_training_data(uploaded_file, import_format)
+
+ def _export_training_data(self, format):
+ """تصدير بيانات التدريب إلى ملف"""
+ examples = st.session_state.terminology_data.get("training_examples", [])
+
+ if not examples:
+ st.error("لا توجد بيانات تدريب للتصدير.")
+ return
+
+ try:
+ if format == "JSON":
+ # تصدير إلى ملف JSON
+ with tempfile.NamedTemporaryFile(mode='w+', suffix='.json', encoding='utf-8', delete=False) as f:
+ json.dump(examples, f, ensure_ascii=False, indent=2)
+ json_path = f.name
+
+ # قراءة الملف وتقديمه للتنزيل
+ with open(json_path, 'r', encoding='utf-8') as f:
+ json_data = f.read()
+
+ st.download_button(
+ label="تنزيل ملف JSON",
+ data=json_data,
+ file_name="training_data.json",
+ mime="application/json"
+ )
+
+ # حذف الملف المؤقت
+ os.unlink(json_path)
+
+ elif format == "JSONL":
+ # تصدير إلى ملف JSONL
+ jsonl_content = ""
+ for example in examples:
+ jsonl_content += json.dumps(example, ensure_ascii=False) + "\n"
+
+ st.download_button(
+ label="تنزيل ملف JSONL",
+ data=jsonl_content,
+ file_name="training_data.jsonl",
+ mime="application/jsonl"
+ )
+
+ elif format == "CSV":
+ # تصدير إلى ملف CSV
+ with tempfile.NamedTemporaryFile(mode='w+', suffix='.csv', newline='', encoding='utf-8', delete=False) as f:
+ writer = csv.writer(f)
+
+ # كتابة الترويسة
+ writer.writerow([
+ 'النص المدخل', 'النص المتوقع', 'الفئة', 'المصطلحات', 'تاريخ الإنشاء', 'تم التوليد بواسطة'
+ ])
+
+ # كتابة البيانات
+ for example in examples:
+ writer.writerow([
+ example.get('input', ''),
+ example.get('output', ''),
+ example.get('category', ''),
+ '|'.join(example.get('terms', [])),
+ example.get('created_at', ''),
+ example.get('generated_by', 'يدوي')
+ ])
+
+ # الحصول على مسار الملف
+ csv_path = f.name
+
+ # قراءة الملف وتقديمه للتنزيل
+ with open(csv_path, 'r', encoding='utf-8') as f:
+ csv_data = f.read()
+
+ st.download_button(
+ label="تنزيل ملف CSV",
+ data=csv_data,
+ file_name="training_data.csv",
+ mime="text/csv"
+ )
+
+ # حذف الملف المؤقت
+ os.unlink(csv_path)
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء تصدير البيانات: {str(e)}")
+
+ def _import_training_data(self, uploaded_file, format):
+ """استيراد بيانات التدريب من ملف"""
+ try:
+ examples = []
+
+ if format == "JSON":
+ # استيراد من ملف JSON
+ content = uploaded_file.read().decode('utf-8')
+ examples = json.loads(content)
+
+ elif format == "JSONL":
+ # استيراد من ملف JSONL
+ content = uploaded_file.read().decode('utf-8')
+
+ for line in content.strip().split('\n'):
+ if line.strip():
+ examples.append(json.loads(line))
+
+ elif format == "CSV":
+ # استيراد من ملف CSV
+ df = pd.read_csv(uploaded_file, encoding='utf-8')
+
+ for _, row in df.iterrows():
+ example = {
+ "input": row.get('النص المدخل', ''),
+ "output": row.get('النص المتوقع', ''),
+ "category": row.get('الفئة', 'أخرى'),
+ "terms": row.get('المصطلحات', '').split('|') if pd.notna(row.get('المصطلحات', '')) else [],
+ "created_at": row.get('تاريخ الإنشاء', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
+ "generated_by": row.get('تم التوليد بواسطة', 'مستورد')
+ }
+
+ examples.append(example)
+
+ # إضافة الأمثلة المستوردة
+ if "training_examples" not in st.session_state.terminology_data:
+ st.session_state.terminology_data["training_examples"] = []
+
+ # فلترة الأمثلة الصحيحة
+ valid_examples = []
+ for ex in examples:
+ if "input" in ex and "output" in ex:
+ valid_examples.append(ex)
+
+ # إضافة الأمثلة وحفظ البيانات
+ if valid_examples:
+ st.session_state.terminology_data["training_examples"].extend(valid_examples)
+ self._save_terminology_data()
+
+ st.success(f"تم استيراد {len(valid_examples)} مثال بنجاح!")
+ st.rerun()
+ else:
+ st.error("لم يتم العثور على أمثلة صالحة في الملف.")
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء استيراد البيانات: {str(e)}")
+
+ def _render_model_training(self):
+ """عرض واجهة تدريب النموذج"""
+ st.markdown("""
+
+
🧠 تدريب النموذج
+
قم بتدريب نموذج الذكاء الاصطناعي على المصطلحات المتخصصة باستخدام أمثلة التدريب.
+
يمكنك اختيار النموذج الأساسي والإعدادات المناسبة لعملية التدريب.
+
+ """, unsafe_allow_html=True)
+
+ # التحقق من وجود بيانات تدريب كافية
+ examples = st.session_state.terminology_data.get("training_examples", [])
+ if len(examples) < 10:
+ st.warning(f"عدد أمثلة التدريب الحالية ({len(examples)}) غير كافٍ للتدريب. يُنصح بوجود 10 أمثلة على الأقل.")
+
+ # تبويبات تدريب النموذج
+ training_tabs = st.tabs(["إعداد التدريب", "نماذج سابقة", "وظائف التدريب النشطة"])
+
+ # تبويب إعداد التدريب
+ with training_tabs[0]:
+ self._render_training_setup()
+
+ # تبويب النماذج السابقة
+ with training_tabs[1]:
+ self._render_previous_models()
+
+ # تبويب وظائف التدريب النشطة
+ with training_tabs[2]:
+ self._render_active_training_jobs()
+
+ def _render_training_setup(self):
+ """عرض إعدادات تدريب النموذج"""
+ st.markdown("### إعداد عملية التدريب")
+
+ # التحقق من وجود مفاتيح API
+ if not self.api_key and not self.anthropic_api_key:
+ st.warning("لم يتم العثور على مفاتيح API للذكاء الاصطناعي. يرجى إضافة OPENAI_API_KEY أو ANTHROPIC_API_KEY إلى المتغيرات البيئية.")
+ return
+
+ # اختيار نموذج الذكاء الاصطناعي الأساسي
+ provider_options = []
+ if self.api_key:
+ provider_options.append("OpenAI")
+ if self.anthropic_api_key:
+ provider_options.append("Anthropic")
+
+ provider = st.selectbox(
+ "مزود الذكاء الاصطناعي",
+ options=provider_options,
+ key="training_provider"
+ )
+
+ # الإعدادات حسب المزود
+ if provider == "OpenAI":
+ # نماذج OpenAI المتاحة للضبط
+ base_model = st.selectbox(
+ "النموذج الأساسي",
+ options=["gpt-3.5-turbo-0125", "gpt-4o-mini"],
+ key="openai_base_model"
+ )
+
+ # إعدادات التدريب
+ col1, col2 = st.columns(2)
+
+ with col1:
+ n_epochs = st.slider("عدد الحقب (Epochs)", 1, 4, 2, key="openai_epochs")
+ batch_size = st.selectbox("حجم الدفعة (Batch Size)", options=[1, 2, 4, 8], index=1, key="openai_batch_size")
+
+ with col2:
+ learning_rate_multiplier = st.slider("مضاعف معدل التعلم", 0.1, 2.0, 1.0, 0.1, key="openai_lr")
+ suffix = st.text_input("لاحقة اسم النموذج", value="arabic-contracts-expert", key="openai_suffix")
+
+ # زر بدء التدريب
+ if styled_button("بدء التدريب", key="start_openai_training", type="primary", icon="🚀"):
+ # التحقق من وجود بيانات كافية
+ examples = st.session_state.terminology_data.get("training_examples", [])
+ if len(examples) < 10:
+ st.error("عدد أمثلة التدريب الحالية قليل جداً. يُفضل وجود على الأقل 10 أمثلة للتدريب.")
+ else:
+ # التأكيد على بدء التدريب
+ confirm = st.warning(f"سيتم بدء عملية تدريب نموذج {base_model} باستخدام {len(examples)} مثال. هل أنت متأكد؟")
+ if styled_button("تأكيد بدء التدريب", key="confirm_openai_training", type="success", icon="✅"):
+ # توجيه بيانات التدريب لصيغة OpenAI
+ formatted_data = self._format_training_data_for_openai(examples)
+
+ # بدء التدريب
+ self._start_openai_training(
+ base_model=base_model,
+ training_data=formatted_data,
+ n_epochs=n_epochs,
+ batch_size=batch_size,
+ learning_rate_multiplier=learning_rate_multiplier,
+ suffix=suffix
+ )
+
+ elif provider == "Anthropic":
+ st.info("ضبط نماذج Anthropic غير متاح حالياً في واجهة البرمجة العامة. يمكنك استخدام أمثلة التدريب مع المساعد المتخصص.")
+
+ def _format_training_data_for_openai(self, examples):
+ """تنسيق بيانات التدريب لواجهة برمجة OpenAI"""
+ formatted_examples = []
+
+ for example in examples:
+ formatted_examples.append({
+ "messages": [
+ {"role": "user", "content": example.get("input", "")},
+ {"role": "assistant", "content": example.get("output", "")}
+ ]
+ })
+
+ return formatted_examples
+
+ def _start_openai_training(self, base_model, training_data, n_epochs, batch_size, learning_rate_multiplier, suffix):
+ """بدء عملية تدريب نموذج OpenAI"""
+ try:
+ # تهيئة واجهة برمجة OpenAI
+ openai.api_key = self.api_key
+
+ # إنشاء ملف تدريب
+ training_file_path = os.path.join(self.training_data_dir, f"training_data_{int(time.time())}.jsonl")
+
+ with open(training_file_path, 'w', encoding='utf-8') as f:
+ for example in training_data:
+ f.write(json.dumps(example, ensure_ascii=False) + "\n")
+
+ # رفع ملف التدريب إلى OpenAI
+ with open(training_file_path, 'rb') as f:
+ response = openai.files.create(
+ file=f,
+ purpose="fine-tune"
+ )
+
+ file_id = response.id
+
+ # بدء وظيفة التدريب
+ response = openai.fine_tuning.jobs.create(
+ training_file=file_id,
+ model=base_model,
+ hyperparameters={
+ "n_epochs": n_epochs,
+ "batch_size": batch_size,
+ "learning_rate_multiplier": learning_rate_multiplier
+ },
+ suffix=suffix
+ )
+
+ job_id = response.id
+
+ # تخزين معلومات وظيفة التدريب
+ training_job = {
+ "job_id": job_id,
+ "provider": "OpenAI",
+ "base_model": base_model,
+ "n_epochs": n_epochs,
+ "batch_size": batch_size,
+ "learning_rate_multiplier": learning_rate_multiplier,
+ "suffix": suffix,
+ "status": "running",
+ "file_id": file_id,
+ "file_path": training_file_path,
+ "examples_count": len(training_data),
+ "started_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "finished_at": None,
+ "fine_tuned_model": None
+ }
+
+ # إضافة الوظيفة إلى حالة الجلسة
+ st.session_state.active_training_job = training_job
+
+ # إضافة الوظيفة إلى قائمة وظائف التدريب
+ if "training_jobs" not in st.session_state.terminology_data:
+ st.session_state.terminology_data["training_jobs"] = []
+
+ st.session_state.terminology_data["training_jobs"].append(training_job)
+
+ # حفظ البيانات
+ self._save_terminology_data()
+
+ st.success(f"تم بدء وظيفة التدريب بنجاح! معرف الوظيفة: {job_id}")
+ st.info("يمكنك متابعة حالة التدريب من تبويب 'وظائف التدريب النشطة'.")
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء بدء عملية التدريب: {str(e)}")
+
+ def _render_previous_models(self):
+ """عرض النماذج المدربة سابقاً"""
+ st.markdown("### النماذج المدربة سابقاً")
+
+ # الحصول على النماذج المدربة
+ models = st.session_state.terminology_data.get("models", [])
+
+ if not models:
+ st.info("لا توجد نماذج مدربة سابقاً.")
+ return
+
+ # عرض النماذج
+ for i, model in enumerate(models):
+ with st.expander(f"{model.get('model_id')} - {model.get('base_model')}", expanded=i==0):
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ st.markdown(f"**معرف النموذج:** {model.get('model_id')}")
+ st.markdown(f"**النموذج الأساسي:** {model.get('base_model')}")
+ st.markdown(f"**عدد أمثلة التدريب:** {model.get('examples_count')}")
+ st.markdown(f"**تاريخ الإنشاء:** {model.get('created_at')}")
+
+ # عرض مؤشرات الأداء إن وجدت
+ if "metrics" in model:
+ st.markdown("#### مؤشرات الأداء")
+
+ metrics = model.get("metrics", {})
+ for metric_name, metric_value in metrics.items():
+ st.markdown(f"**{metric_name}:** {metric_value}")
+
+ with col2:
+ # أزرار الاستخدام والحذف
+ if styled_button("استخدام النموذج", key=f"use_model_{i}", type="primary", icon="✅"):
+ st.session_state.selected_model = model.get('model_id')
+ st.success(f"تم اختيار النموذج {model.get('model_id')} للاستخدام.")
+
+ if styled_button("حذف النموذج", key=f"delete_model_{i}", type="danger", icon="🗑️"):
+ st.session_state.model_to_delete = i
+
+ # عرض الوصف والملاحظات
+ st.markdown(f"**الوصف:** {model.get('description', 'لا يوجد وصف.')}")
+
+ # عرض النماذج المستخدمة في التدريب
+ if "examples_preview" in model and model["examples_preview"]:
+ with st.expander("عينة من أمثلة التدريب"):
+ for j, example in enumerate(model["examples_preview"]):
+ st.markdown(f"**مثال #{j+1}**")
+ st.markdown(f"**المدخل:** {example.get('input')}")
+ st.markdown(f"**المخرج:** {example.get('output')}")
+ st.markdown("---")
+
+ # معالجة حذف النموذج
+ if "model_to_delete" in st.session_state:
+ if st.warning(f"هل أنت متأكد من حذف النموذج '{models[st.session_state.model_to_delete].get('model_id')}'؟"):
+ if styled_button("تأكيد الحذف", key="confirm_delete_model", type="danger", icon="🗑️"):
+ # حذف النموذج
+ del st.session_state.terminology_data["models"][st.session_state.model_to_delete]
+
+ # حفظ البيانات
+ self._save_terminology_data()
+
+ # إعادة ضبط حالة الحذف
+ del st.session_state.model_to_delete
+
+ st.success("تم حذف النموذج بنجاح!")
+ st.rerun()
+
+ if styled_button("إلغاء", key="cancel_delete_model", type="secondary", icon="❌"):
+ del st.session_state.model_to_delete
+ st.rerun()
+
+ def _render_active_training_jobs(self):
+ """عرض وظائف التدريب النشطة"""
+ st.markdown("### وظائف التدريب النشطة")
+
+ # الحصول على وظائف التدريب
+ jobs = st.session_state.terminology_data.get("training_jobs", [])
+
+ # فرز الوظائف حسب الحالة
+ active_jobs = [job for job in jobs if job.get("status") in ["running", "validating_files", "queued"]]
+ completed_jobs = [job for job in jobs if job.get("status") == "succeeded"]
+ failed_jobs = [job for job in jobs if job.get("status") in ["failed", "cancelled"]]
+
+ # زر تحديث حالة الوظائف
+ if styled_button("تحديث حالة الوظائف", key="refresh_jobs", type="primary", icon="🔄"):
+ self._refresh_training_jobs_status()
+
+ # عرض الوظائف النشطة
+ if active_jobs:
+ st.markdown("#### الوظائف النشطة")
+
+ for i, job in enumerate(active_jobs):
+ with st.expander(f"{job.get('job_id')} - {job.get('base_model')}", expanded=True):
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ st.markdown(f"**معرف الوظيفة:** {job.get('job_id')}")
+ st.markdown(f"**النموذج الأساسي:** {job.get('base_model')}")
+ st.markdown(f"**الحالة:** {job.get('status')}")
+ st.markdown(f"**تاريخ البدء:** {job.get('started_at')}")
+
+ # عرض تقدم التدريب إن وجد
+ if "progress" in job:
+ progress = job.get("progress", 0)
+ st.progress(progress)
+ st.markdown(f"**التقدم:** {progress*100:.1f}%")
+
+ with col2:
+ # زر إلغاء الوظيفة
+ if styled_button("إلغاء الوظيفة", key=f"cancel_job_{i}", type="danger", icon="⛔"):
+ st.session_state.job_to_cancel = i
+
+ # عرض معلومات إضافية
+ st.markdown(f"**عدد الحقب:** {job.get('n_epochs')}")
+ st.markdown(f"**حجم الدفعة:** {job.get('batch_size')}")
+ st.markdown(f"**مضاعف معدل التعلم:** {job.get('learning_rate_multiplier')}")
+ else:
+ st.info("لا توجد وظائف تدريب نشطة حالياً.")
+
+ # عرض الوظائف المكتملة
+ if completed_jobs:
+ st.markdown("#### الوظائف المكتملة")
+
+ for i, job in enumerate(completed_jobs):
+ with st.expander(f"{job.get('job_id')} - {job.get('base_model')}", expanded=False):
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ st.markdown(f"**معرف الوظيفة:** {job.get('job_id')}")
+ st.markdown(f"**النموذج الأساسي:** {job.get('base_model')}")
+ st.markdown(f"**تاريخ البدء:** {job.get('started_at')}")
+ st.markdown(f"**تاريخ الانتهاء:** {job.get('finished_at')}")
+ st.markdown(f"**النموذج المدرب:** {job.get('fine_tuned_model')}")
+
+ with col2:
+ # زر استخدام النموذج المدرب
+ if styled_button("استخدام النموذج", key=f"use_trained_model_{i}", type="primary", icon="✅"):
+ st.session_state.selected_model = job.get('fine_tuned_model')
+ st.success(f"تم اختيار النموذج {job.get('fine_tuned_model')} للاستخدام.")
+
+ # زر حذف الوظيفة
+ if styled_button("حذف الوظيفة", key=f"delete_completed_job_{i}", type="danger", icon="🗑️"):
+ st.session_state.completed_job_to_delete = len(active_jobs) + i
+
+ # عرض الوظائف الفاشلة
+ if failed_jobs:
+ st.markdown("#### الوظائف الفاشلة")
+
+ for i, job in enumerate(failed_jobs):
+ with st.expander(f"{job.get('job_id')} - {job.get('base_model')}", expanded=False):
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ st.markdown(f"**معرف الوظيفة:** {job.get('job_id')}")
+ st.markdown(f"**النموذج الأساسي:** {job.get('base_model')}")
+ st.markdown(f"**الحالة:** {job.get('status')}")
+ st.markdown(f"**تاريخ البدء:** {job.get('started_at')}")
+
+ # عرض سبب الفشل إن وجد
+ if "error" in job:
+ st.error(f"سبب الفشل: {job.get('error')}")
+
+ with col2:
+ # زر حذف الوظيفة
+ if styled_button("حذف الوظيفة", key=f"delete_failed_job_{i}", type="danger", icon="🗑️"):
+ st.session_state.failed_job_to_delete = len(active_jobs) + len(completed_jobs) + i
+
+ # معالجة إلغاء الوظيفة
+ if "job_to_cancel" in st.session_state:
+ if st.warning(f"هل أنت متأكد من إلغاء وظيفة التدريب '{active_jobs[st.session_state.job_to_cancel].get('job_id')}'؟"):
+ if styled_button("تأكيد الإلغاء", key="confirm_cancel_job", type="danger", icon="🗑️"):
+ # إلغاء الوظيفة
+ self._cancel_training_job(active_jobs[st.session_state.job_to_cancel])
+
+ # إعادة ضبط حالة الإلغاء
+ del st.session_state.job_to_cancel
+
+ st.success("تم إلغاء وظيفة التدريب بنجاح!")
+ st.rerun()
+
+ if styled_button("إلغاء", key="cancel_job_cancellation", type="secondary", icon="❌"):
+ del st.session_state.job_to_cancel
+ st.rerun()
+
+ # معالجة حذف الوظائف المكتملة
+ if "completed_job_to_delete" in st.session_state:
+ idx = st.session_state.completed_job_to_delete
+ if 0 <= idx < len(jobs):
+ if st.warning(f"هل أنت متأكد من حذف وظيفة التدريب '{jobs[idx].get('job_id')}'؟"):
+ if styled_button("تأكيد الحذف", key="confirm_delete_completed_job", type="danger", icon="🗑️"):
+ # حذف الوظيفة
+ del st.session_state.terminology_data["training_jobs"][idx]
+
+ # حفظ البيانات
+ self._save_terminology_data()
+
+ # إعادة ضبط حالة الحذف
+ del st.session_state.completed_job_to_delete
+
+ st.success("تم حذف وظيفة التدريب بنجاح!")
+ st.rerun()
+
+ if styled_button("إلغاء", key="cancel_completed_job_deletion", type="secondary", icon="❌"):
+ del st.session_state.completed_job_to_delete
+ st.rerun()
+
+ # معالجة حذف الوظائف الفاشلة
+ if "failed_job_to_delete" in st.session_state:
+ idx = st.session_state.failed_job_to_delete
+ if 0 <= idx < len(jobs):
+ if st.warning(f"هل أنت متأكد من حذف وظيفة التدريب '{jobs[idx].get('job_id')}'؟"):
+ if styled_button("تأكيد الحذف", key="confirm_delete_failed_job", type="danger", icon="🗑️"):
+ # حذف الوظيفة
+ del st.session_state.terminology_data["training_jobs"][idx]
+
+ # حفظ البيانات
+ self._save_terminology_data()
+
+ # إعادة ضبط حالة الحذف
+ del st.session_state.failed_job_to_delete
+
+ st.success("تم حذف وظيفة التدريب بنجاح!")
+ st.rerun()
+
+ if styled_button("إلغاء", key="cancel_failed_job_deletion", type="secondary", icon="❌"):
+ del st.session_state.failed_job_to_delete
+ st.rerun()
+
+ def _refresh_training_jobs_status(self):
+ """تحديث حالة وظائف التدريب"""
+ jobs = st.session_state.terminology_data.get("training_jobs", [])
+
+ # فلترة الوظائف النشطة
+ active_jobs = [job for job in jobs if job.get("status") in ["running", "validating_files", "queued"]]
+
+ if not active_jobs:
+ st.info("لا توجد وظائف تدريب نشطة للتحديث.")
+ return
+
+ try:
+ # تحديث حالة كل وظيفة نشطة
+ for job in active_jobs:
+ if job.get("provider") == "OpenAI" and self.api_key:
+ # تحديث حالة وظيفة OpenAI
+ job_id = job.get("job_id")
+
+ # استعلام عن حالة الوظيفة
+ openai.api_key = self.api_key
+ response = openai.fine_tuning.jobs.retrieve(job_id)
+
+ # تحديث حالة الوظيفة
+ job["status"] = response.status
+
+ # تحديث التقدم إن وجد
+ if hasattr(response, "progress") and response.progress is not None:
+ job["progress"] = response.progress
+
+ # إذا اكتملت الوظيفة، تحديث معلومات النموذج المدرب
+ if response.status == "succeeded":
+ job["finished_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ job["fine_tuned_model"] = response.fine_tuned_model
+
+ # إضافة النموذج المدرب إلى قائمة النماذج
+ self._add_trained_model(job, response)
+
+ # إذا فشلت الوظيفة، تسجيل سبب الفشل
+ elif response.status == "failed" and hasattr(response, "error"):
+ job["error"] = response.error
+
+ # حفظ البيانات
+ self._save_terminology_data()
+
+ st.success("تم تحديث حالة وظائف التدريب بنجاح!")
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء تحديث حالة وظائف التدريب: {str(e)}")
+
+ def _cancel_training_job(self, job):
+ """إلغاء وظيفة تدريب"""
+ try:
+ if job.get("provider") == "OpenAI" and self.api_key:
+ # إلغاء وظيفة OpenAI
+ job_id = job.get("job_id")
+
+ # استدعاء واجهة برمجة OpenAI
+ openai.api_key = self.api_key
+ openai.fine_tuning.jobs.cancel(job_id)
+
+ # تحديث حالة الوظيفة
+ job["status"] = "cancelled"
+ job["finished_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+ # حفظ البيانات
+ self._save_terminology_data()
+
+ return True
+
+ return False
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء إلغاء وظيفة التدريب: {str(e)}")
+ return False
+
+ def _add_trained_model(self, job, response):
+ """إضافة النموذج المدرب إلى قائمة النماذج"""
+ # إنشاء كائن النموذج
+ model = {
+ "model_id": response.fine_tuned_model,
+ "base_model": job.get("base_model"),
+ "provider": job.get("provider"),
+ "training_job_id": job.get("job_id"),
+ "description": f"النموذج المدرب على المصطلحات المتخصصة في {job.get('suffix')}",
+ "examples_count": job.get("examples_count"),
+ "created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "metrics": {}
+ }
+
+ # إضافة مؤشرات الأداء إن وجدت
+ if hasattr(response, "result_files") and response.result_files:
+ # تنزيل ملف النتائج وقراءة مؤشرات الأداء
+ pass
+
+ # إضافة عينة من أمثلة التدريب
+ examples = st.session_state.terminology_data.get("training_examples", [])
+ if examples:
+ # أخذ 5 أمثلة كعينة
+ sample_examples = random.sample(examples, min(5, len(examples)))
+ model["examples_preview"] = sample_examples
+
+ # إضافة النموذج إلى قائمة النماذج
+ if "models" not in st.session_state.terminology_data:
+ st.session_state.terminology_data["models"] = []
+
+ st.session_state.terminology_data["models"].append(model)
+
+ def _render_model_testing(self):
+ """عرض واجهة اختبار النموذج"""
+ st.markdown("""
+
+
🧪 اختبار النموذج
+
اختبر نموذج الذكاء الاصطناعي المدرب على المصطلحات المتخصصة.
+
يمكنك تجريب أسئلة مختلفة ومقارنة النتائج مع النماذج الأخرى.
+
+ """, unsafe_allow_html=True)
+
+ # التحقق من وجود مفاتيح API
+ if not self.api_key and not self.anthropic_api_key:
+ st.warning("لم يتم العثور على مفاتيح API للذكاء الاصطناعي. يرجى إضافة OPENAI_API_KEY أو ANTHROPIC_API_KEY إلى المتغيرات البيئية.")
+ return
+
+ # الحصول على قائمة النماذج المتاحة
+ available_models = []
+
+ # OpenAI
+ if self.api_key:
+ available_models.extend(["gpt-4o", "gpt-3.5-turbo"])
+
+ # إضافة النماذج المدربة إن وجدت
+ for model in st.session_state.terminology_data.get("models", []):
+ if model.get("provider") == "OpenAI":
+ available_models.append(model.get("model_id"))
+
+ # Anthropic
+ if self.anthropic_api_key:
+ available_models.extend(["claude-3-7-sonnet-20250219"])
+
+ # اختيار النموذج
+ selected_model = st.selectbox(
+ "اختر النموذج",
+ options=available_models,
+ key="test_model"
+ )
+
+ # إدخال النص للاختبار
+ test_input = st.text_area(
+ "أدخل نص الاختبار",
+ value="ما هو مفهوم المحتوى المحلي في المشاريع الحكومية وكيف يتم حسابه؟",
+ height=150,
+ key="test_input"
+ )
+
+ # خيارات متقدمة
+ with st.expander("خيارات متقدمة"):
+ temperature = st.slider("درجة الإبداعية (Temperature)", 0.0, 1.0, 0.7, 0.1, key="test_temperature")
+ max_tokens = st.slider("الحد الأقصى للرموز (Max Tokens)", 100, 2000, 500, 100, key="test_max_tokens")
+
+ # تحميل المصطلحات والتعريفات
+ terms_with_definitions = {}
+ for term in st.session_state.terminology_data.get("terms", []):
+ terms_with_definitions[term.get("term")] = term.get("definition")
+
+ # زر إجراء الاختبار
+ if styled_button("إجراء الاختبار", key="run_test", type="primary", icon="🧪"):
+ if not test_input:
+ st.error("يرجى إدخال نص للاختبار.")
+ else:
+ # عرض شريط التقدم
+ with st.spinner("جاري معالجة النص..."):
+ # إجراء الاختبار
+ response = self._test_model(
+ model=selected_model,
+ input_text=test_input,
+ temperature=temperature,
+ max_tokens=max_tokens,
+ terms_with_definitions=terms_with_definitions
+ )
+
+ # عرض النتيجة
+ st.markdown("### نتيجة الاختبار")
+ st.markdown(response)
+
+ # تحليل الاستجابة لاكتشاف المصطلحات المستخدمة
+ used_terms = []
+ for term in terms_with_definitions:
+ if term in response:
+ used_terms.append(term)
+
+ if used_terms:
+ st.markdown("### المصطلحات المكتشفة في الاستجابة")
+ for term in used_terms:
+ st.markdown(f"- **{term}**: {terms_with_definitions[term]}")
+
+ def _test_model(self, model, input_text, temperature, max_tokens, terms_with_definitions):
+ """اختبار النموذج"""
+ try:
+ # تجهيز المحتوى النظامي
+ system_prompt = "أنت مساعد متخصص في عقود المقاولات والمناقصات باللغة العربية. قم بالإجابة بدقة على الأسئلة والطلبات مع مراعاة المصطلحات الفنية المتخصصة."
+
+ # إضافة المصطلحات إلى المحتوى النظامي
+ if terms_with_definitions:
+ system_prompt += "\n\nفيما يلي قائمة بالمصطلحات المتخصصة وتعريفاتها:\n\n"
+ for term, definition in terms_with_definitions.items():
+ system_prompt += f"- {term}: {definition}\n"
+
+ # OpenAI
+ if "gpt" in model or any(model_data.get("model_id") == model for model_data in st.session_state.terminology_data.get("models", [])):
+ # استخدام OpenAI API
+ openai.api_key = self.api_key
+
+ response = openai.chat.completions.create(
+ model=model,
+ messages=[
+ {"role": "system", "content": system_prompt},
+ {"role": "user", "content": input_text}
+ ],
+ temperature=temperature,
+ max_tokens=max_tokens
+ )
+
+ return response.choices[0].message.content
+
+ # Anthropic
+ elif "claude" in model and self.anthropic_api_key:
+ # استخدام Anthropic API
+ from anthropic import Anthropic
+
+ anthropic_client = Anthropic(api_key=self.anthropic_api_key)
+
+ response = anthropic_client.messages.create(
+ model=model,
+ max_tokens=max_tokens,
+ messages=[
+ {"role": "system", "content": system_prompt},
+ {"role": "user", "content": input_text}
+ ],
+ temperature=temperature
+ )
+
+ return response.content[0].text
+
+ else:
+ return "النموذج المختار غير مدعوم حالياً."
+
+ except Exception as e:
+ return f"حدث خطأ أثناء اختبار النموذج: {str(e)}"
+
+ def _render_specialized_assistant(self):
+ """عرض واجهة المساعد المتخصص"""
+ st.markdown("""
+
+
🤖 المساعد المتخصص
+
استخدم المساعد الذكي المتخصص في المصطلحات التعاقدية الهندسية.
+
يمكنك طرح أسئلة حول المصطلحات وتفسيراتها واستخداماتها.
+
+ """, unsafe_allow_html=True)
+
+ # التحقق من وجود مفاتيح API
+ if not self.api_key and not self.anthropic_api_key:
+ st.warning("لم يتم العثور على مفاتيح API للذكاء الاصطناعي. يرجى إضافة OPENAI_API_KEY أو ANTHROPIC_API_KEY إلى المتغيرات البيئية.")
+ return
+
+ # الحصول على قائمة النماذج المتاحة
+ available_models = []
+
+ # OpenAI
+ if self.api_key:
+ available_models.extend(["gpt-4o", "gpt-3.5-turbo"])
+
+ # إضافة النماذج المدربة إن وجدت
+ for model in st.session_state.terminology_data.get("models", []):
+ if model.get("provider") == "OpenAI":
+ available_models.append(model.get("model_id"))
+
+ # Anthropic
+ if self.anthropic_api_key:
+ available_models.extend(["claude-3-7-sonnet-20250219"])
+
+ # تهيئة حالة المحادثة
+ if "chat_history" not in st.session_state:
+ st.session_state.chat_history = []
+
+ if "assistant_model" not in st.session_state:
+ st.session_state.assistant_model = available_models[0] if available_models else ""
+
+ # اختيار النموذج
+ selected_model = st.selectbox(
+ "اختر نموذج المساعد",
+ options=available_models,
+ index=available_models.index(st.session_state.assistant_model) if st.session_state.assistant_model in available_models else 0,
+ key="assistant_model_selector"
+ )
+
+ # تحديث النموذج المختار
+ if selected_model != st.session_state.assistant_model:
+ st.session_state.assistant_model = selected_model
+ st.rerun()
+
+ # عرض المحادثة
+ st.markdown("### المحادثة")
+
+ for message in st.session_state.chat_history:
+ if message["role"] == "user":
+ st.markdown(f"""
+
+ """, unsafe_allow_html=True)
+
+ # عرض تحليل التغييرات القانونية
+ st.markdown("
تحليل التغييرات القانونية
", unsafe_allow_html=True)
+
+ if legal_changes:
+ tabs = st.tabs([change["label"] for change in legal_changes])
+
+ for i, tab in enumerate(tabs):
+ with tab:
+ st.markdown(f"**عدد التغييرات: {legal_changes[i]['count']}**")
+
+ for j, change in enumerate(legal_changes[i]["changes"]):
+ col1, col2 = st.columns(2)
+ with col1:
+ st.markdown(f"**{title1}:**")
+ st.markdown(f"
{change['doc1_text']}
", unsafe_allow_html=True)
+ with col2:
+ st.markdown(f"**{title2}:**")
+ st.markdown(f"
{change['doc2_text']}
", unsafe_allow_html=True)
+
+ if j < len(legal_changes[i]["changes"]) - 1:
+ st.markdown("---")
+ else:
+ st.info("لم يتم اكتشاف تغييرات قانونية هامة بين المستندين.")
+
+ # عرض الرسوم البيانية للتغييرات
+ st.markdown("
رسوم بيانية للتغييرات
", unsafe_allow_html=True)
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ # رسم بياني لتوزيع أنواع التغييرات في الفقرات
+ stats = comparison_report["statistics"]
+ fig = px.pie(
+ names=["فقرات متطابقة", "فقرات معدلة", "فقرات محذوفة", "فقرات مضافة"],
+ values=[
+ stats["doc1_paragraphs"] - stats["removed_paragraphs"] - stats["modified_paragraphs"],
+ stats["modified_paragraphs"],
+ stats["removed_paragraphs"],
+ stats["added_paragraphs"]
+ ],
+ title="توزيع التغييرات في الفقرات",
+ color_discrete_sequence=["#00b894", "#fdcb6e", "#d63031", "#0984e3"]
+ )
+
+ fig.update_layout(
+ font=dict(family="Arial, sans-serif", size=14),
+ height=350
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ with col2:
+ # رسم بياني للكلمات المضافة والمحذوفة الأكثر تكراراً
+ words_data = []
+
+ for word, count in comparison_report["statistics"]["top_removed_words"]:
+ if len(word) > 1: # تجاهل الأحرف المفردة
+ words_data.append({"word": word, "count": count, "type": "محذوفة"})
+
+ for word, count in comparison_report["statistics"]["top_added_words"]:
+ if len(word) > 1: # تجاهل الأحرف المفردة
+ words_data.append({"word": word, "count": count, "type": "مضافة"})
+
+ if words_data:
+ words_df = pd.DataFrame(words_data)
+
+ fig = px.bar(
+ words_df,
+ x="word",
+ y="count",
+ color="type",
+ title="الكلمات المضافة والمحذوفة الأكثر تكراراً",
+ labels={"word": "الكلمة", "count": "عدد المرات", "type": "النوع"},
+ color_discrete_map={"محذوفة": "#d63031", "مضافة": "#0984e3"}
+ )
+
+ fig.update_layout(
+ font=dict(family="Arial, sans-serif", size=14),
+ height=350
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+ else:
+ st.info("لا توجد بيانات كافية للكلمات المضافة والمحذوفة.")
+
+ # عرض تحليل الأسعار والتواريخ
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.markdown("
تحليل التغييرات في الأسعار
", unsafe_allow_html=True)
+
+ if price_changes["doc1_prices_count"] > 0 or price_changes["doc2_prices_count"] > 0:
+ price_change_direction = "زيادة" if price_changes["total_change_percentage"] > 0 else "نقص"
+ price_change_color = "#d63031" if price_changes["total_change_percentage"] > 0 else "#00b894"
+
+ st.markdown(f"""
+
+
تغيير في إجمالي الأسعار بنسبة {abs(price_changes['total_change_percentage']):.2f}% ({price_change_direction})
+ """, unsafe_allow_html=True)
+
+ # عرض التواريخ المحذوفة والمضافة
+ if date_changes["removed_dates"]:
+ st.markdown("**التواريخ المحذوفة:**")
+ for date in date_changes["removed_dates"][:10]: # عرض أول 10 فقط إذا كان هناك الكثير
+ st.markdown(f"
{date}
", unsafe_allow_html=True)
+
+ if date_changes["added_dates"]:
+ st.markdown("**التواريخ المضافة:**")
+ for date in date_changes["added_dates"][:10]: # عرض أول 10 فقط
+ st.markdown(f"
+ استخدم هذه الأدوات لمقارنة مستندات العقود بشكل متقدم، واكتشاف التغييرات والفروقات بين نسخ المستندات المختلفة،
+ مع تحليل التغييرات القانونية والمالية والتواريخ.
+
+ """, unsafe_allow_html=True)
+
+ # إنشاء علامات التبويب للأدوات المختلفة
+ tabs = st.tabs([
+ "مقارنة نصية مباشرة",
+ "مقارنة ملفات PDF",
+ "عرض تقارير المقارنة السابقة"
+ ])
+
+ with tabs[0]:
+ st.markdown("### مقارنة نصية مباشرة")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ title1 = st.text_input("عنوان المستند الأول", key="text_title1")
+ text1 = st.text_area("نص المستند الأول", height=300, key="text_input1")
+
+ with col2:
+ title2 = st.text_input("عنوان المستند الثاني", key="text_title2")
+ text2 = st.text_area("نص المستند الثاني", height=300, key="text_input2")
+
+ if st.button("قارن النصوص", key="compare_text_btn"):
+ if text1 and text2:
+ self.render_document_comparison(
+ text1,
+ text2,
+ title1 or "المستند الأول",
+ title2 or "المستند الثاني"
+ )
+ else:
+ st.warning("يرجى إدخال نص المستندين للمقارنة")
+
+ with tabs[1]:
+ st.markdown("### مقارنة ملفات PDF")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ title1_pdf = st.text_input("عنوان المستند الأول", key="pdf_title1")
+ uploaded_file1 = st.file_uploader("تحميل المستند الأول (PDF)", type=["pdf"], key="pdf_upload1")
+
+ with col2:
+ title2_pdf = st.text_input("عنوان المستند الثاني", key="pdf_title2")
+ uploaded_file2 = st.file_uploader("تحميل المستند الثاني (PDF)", type=["pdf"], key="pdf_upload2")
+
+ if st.button("قارن ملفات PDF", key="compare_pdf_btn"):
+ if uploaded_file1 is not None and uploaded_file2 is not None:
+ with st.spinner("جاري استخراج النصوص من ملفات PDF..."):
+ text1_pdf = self._extract_text_from_pdf(uploaded_file1)
+ text2_pdf = self._extract_text_from_pdf(uploaded_file2)
+
+ if text1_pdf and text2_pdf:
+ self.render_document_comparison(
+ text1_pdf,
+ text2_pdf,
+ title1_pdf or uploaded_file1.name,
+ title2_pdf or uploaded_file2.name
+ )
+ else:
+ st.error("تعذر استخراج النص من ملفات PDF. يرجى التأكد من أن الملفات تحتوي على نصوص قابلة للاستخراج.")
+ else:
+ st.warning("يرجى تحميل ملفي PDF للمقارنة")
+
+ with tabs[2]:
+ st.markdown("### تقارير المقارنة السابقة")
+
+ # الحصول على تقارير المقارنة المحفوظة
+ reports = self.get_comparison_reports()
+
+ if reports:
+ # عرض التقارير في جدول
+ report_data = []
+ for report in reports:
+ report_data.append({
+ "التاريخ": report["timestamp"],
+ "المستند الأول": report["title1"],
+ "المستند الثاني": report["title2"],
+ "نسبة التشابه": f"{report['similarity']}%",
+ "الملف": report["filename"]
+ })
+
+ report_df = pd.DataFrame(report_data)
+ st.dataframe(report_df)
+
+ # اختيار تقرير لعرضه
+ selected_report = st.selectbox(
+ "اختر تقريراً لعرضه",
+ options=[f"{r['title1']} و {r['title2']} ({r['timestamp']})" for r in reports],
+ format_func=lambda x: x
+ )
+
+ report_index = next((i for i, r in enumerate(reports) if f"{r['title1']} و {r['title2']} ({r['timestamp']})" == selected_report), None)
+
+ if report_index is not None and st.button("عرض التقرير المحدد"):
+ selected_filename = reports[report_index]["filename"]
+ report_data = self.load_comparison_report(selected_filename)
+
+ if report_data:
+ st.success(f"تم تحميل تقرير المقارنة بنجاح")
+
+ # عرض ملخص التقرير
+ st.markdown(f"### ملخص تقرير المقارنة")
+ st.markdown(f"**نسبة التشابه:** {report_data['similarity']}%")
+ st.markdown(f"**تاريخ المقارنة:** {report_data['timestamp']}")
+ st.markdown(f"**ملخص التغييرات:** {report_data['summary']}")
+
+ # استخراج الاختلافات الرئيسية
+ key_differences = self.extract_key_differences(report_data)
+
+ if key_differences:
+ st.markdown("### الاختلافات الرئيسية")
+
+ for diff in key_differences:
+ st.markdown(f"#### {diff['label']} ({diff['count']})")
+
+ if diff["type"] == "added_paragraphs":
+ for item in diff["items"][:5]: # عرض أول 5 فقط
+ st.markdown(f"
{item}
", unsafe_allow_html=True)
+
+ elif diff["type"] == "removed_paragraphs":
+ for item in diff["items"][:5]:
+ st.markdown(f"
{item}
", unsafe_allow_html=True)
+
+ elif diff["type"] == "modified_paragraphs":
+ for item in diff["items"][:3]:
+ col1, col2 = st.columns(2)
+ with col1:
+ st.markdown(f"**{report_data['title1']}:**")
+ st.markdown(f"
{item['doc1_text']}
", unsafe_allow_html=True)
+ with col2:
+ st.markdown(f"**{report_data['title2']}:**")
+ st.markdown(f"
{item['doc2_text']}
", unsafe_allow_html=True)
+
+ elif diff["type"] in ["added_words", "removed_words"]:
+ # عرض الكلمات في شكل جدول
+ word_data = []
+ for word, count in diff["items"]:
+ if len(word) > 1: # تجاهل الأحرف المفردة
+ word_data.append({"الكلمة": word, "عدد المرات": count})
+
+ if word_data:
+ word_df = pd.DataFrame(word_data)
+ st.dataframe(word_df)
+
+ # تحليل التغييرات القانونية
+ legal_changes = self.analyze_legal_changes(report_data)
+
+ if legal_changes:
+ st.markdown("### تحليل التغييرات القانونية")
+
+ for change in legal_changes[:3]: # عرض أهم 3 فئات فقط
+ st.markdown(f"#### {change['label']} ({change['count']})")
+
+ for item in change["changes"][:2]: # عرض أول مثالين فقط
+ col1, col2 = st.columns(2)
+ with col1:
+ st.markdown(f"**{report_data['title1']}:**")
+ st.markdown(f"
{item['doc1_text']}
", unsafe_allow_html=True)
+ with col2:
+ st.markdown(f"**{report_data['title2']}:**")
+ st.markdown(f"
{item['doc2_text']}
", unsafe_allow_html=True)
+ else:
+ st.error("تعذر تحميل تقرير المقارنة")
+ else:
+ st.info("لا توجد تقارير مقارنة محفوظة")
+
+ # إضافة CSS للتنسيق
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+ def render(self):
+ """عرض واجهة المستخدم الرئيسية للتطبيق"""
+ self.render_advanced_comparison_tools()
\ No newline at end of file
diff --git a/modules/maps/README.md b/modules/maps/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4698548083da0a4ae7f3e011b2a6b26222364955
--- /dev/null
+++ b/modules/maps/README.md
@@ -0,0 +1,45 @@
+# وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد
+
+## نظرة عامة
+تتيح هذه الوحدة عرض مواقع المشاريع على خريطة تفاعلية مع إمكانية رؤية التضاريس بشكل ثلاثي الأبعاد، مما يساعد في تقييم طبيعة الموقع بشكل أفضل قبل البدء في العمل.
+
+## الميزات الرئيسية
+
+### الخريطة التفاعلية
+- عرض جميع مواقع المشاريع على خريطة تفاعلية
+- إمكانية البحث عن المواقع وتصفيتها
+- تجميع المواقع القريبة (Clustering)
+- عرض خرائط حرارية لتوزيع المشاريع
+- أدوات قياس المسافة والمساحة
+
+### عرض التضاريس ثلاثي الأبعاد
+- عرض تضاريس موقع المشروع بشكل ثلاثي الأبعاد
+- التحكم في نطاق العرض ومقياس الارتفاع
+- تحليل الارتفاعات وعرض المقطع الجانبي
+- إمكانية تدوير وتكبير العرض للرؤية من زوايا مختلفة
+
+### تحليل المواقع
+- عرض توزيع المشاريع حسب المدينة والحالة
+- تحليل المسافات بين المشاريع
+- عرض المشاريع القريبة من مشروع محدد
+- رسوم بيانية توضيحية للتوزيع الجغرافي
+
+### إدارة المواقع
+- إضافة مواقع جديدة
+- تحرير وحذف المواقع الموجودة
+- استيراد وتصدير بيانات المواقع بصيغ متعددة (CSV, JSON, GeoJSON)
+
+## المتطلبات الفنية
+- Streamlit
+- Folium
+- PyDeck
+- Pandas
+- NumPy
+- Plotly
+- streamlit-folium
+
+## المطورون
+فريق تطوير نظام WAHBI AI لتحليل العقود والمناقصات - شركة شبه الجزيرة للمقاولات
+
+## تاريخ الإصدار
+مارس 2025
\ No newline at end of file
diff --git a/modules/maps/__init__.py b/modules/maps/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..03d5e034693e76c23713942b30b72703bfdfc710
--- /dev/null
+++ b/modules/maps/__init__.py
@@ -0,0 +1 @@
+# ملف تهيئة وحدة الخرائط
\ No newline at end of file
diff --git a/modules/maps/interactive_map.py b/modules/maps/interactive_map.py
new file mode 100644
index 0000000000000000000000000000000000000000..653d7d6c110ba1b8b0f15dcfb0858161ff8a78bc
--- /dev/null
+++ b/modules/maps/interactive_map.py
@@ -0,0 +1,1671 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد
+تتيح هذه الوحدة عرض مواقع المشاريع على خريطة تفاعلية مع إمكانية رؤية التضاريس بشكل ثلاثي الأبعاد
+"""
+
+import os
+import sys
+import streamlit as st
+import pandas as pd
+import numpy as np
+import pydeck as pdk
+import folium
+from folium.plugins import MarkerCluster, HeatMap, MeasureControl
+from streamlit_folium import folium_static
+import requests
+import json
+import random
+from typing import List, Dict, Any, Tuple, Optional
+import tempfile
+import base64
+from PIL import Image
+from io import BytesIO
+
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
+
+# استيراد مكونات واجهة المستخدم
+from utils.components.header import render_header
+from utils.components.credits import render_credits
+from utils.helpers import format_number, format_currency, styled_button
+
+
+class InteractiveMap:
+ """فئة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد"""
+
+ def __init__(self):
+ """تهيئة وحدة الخريطة التفاعلية"""
+ # تهيئة مجلدات حفظ البيانات
+ self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/maps"))
+ os.makedirs(self.data_dir, exist_ok=True)
+
+ # مفاتيح API لخدمات الخرائط
+ self.mapbox_token = os.environ.get("MAPBOX_TOKEN", "")
+ self.opentopodata_api = "https://api.opentopodata.org/v1/srtm30m"
+
+ # تهيئة حالة الجلسة
+ if 'project_locations' not in st.session_state:
+ st.session_state.project_locations = []
+
+ if 'selected_location' not in st.session_state:
+ st.session_state.selected_location = None
+
+ if 'terrain_data' not in st.session_state:
+ st.session_state.terrain_data = None
+
+ # بيانات اختبارية للمشاريع (سيتم استبدالها بالبيانات الفعلية من قاعدة البيانات)
+ self._initialize_sample_projects()
+
+ def render(self):
+ """عرض واجهة وحدة الخريطة التفاعلية"""
+ # عرض الشعار والعنوان الرئيسي
+ render_header("خريطة مواقع المشاريع التفاعلية")
+
+ # تبويبات الوحدة
+ tabs = st.tabs([
+ "الخريطة التفاعلية",
+ "عرض التضاريس ثلاثي الأبعاد",
+ "تحليل المواقع",
+ "إدارة المواقع"
+ ])
+
+ # تبويب الخريطة التفاعلية
+ with tabs[0]:
+ self._render_interactive_map()
+
+ # تبويب عرض التضاريس ثلاثي الأبعاد
+ with tabs[1]:
+ self._render_3d_terrain()
+
+ # تبويب تحليل المواقع
+ with tabs[2]:
+ self._render_location_analysis()
+
+ # تبويب إدارة المواقع
+ with tabs[3]:
+ self._render_location_management()
+
+ # عرض حقوق النشر
+ render_credits()
+
+ def _render_interactive_map(self):
+ """عرض الخريطة التفاعلية"""
+ st.markdown("""
+
+
🗺️ الخريطة التفاعلية لمواقع المشاريع
+
خريطة تفاعلية تعرض مواقع جميع المشاريع مع إمكانية التكبير والتصغير والتحرك.
+
يمكنك النقر على أي موقع للحصول على تفاصيل المشروع.
- """
-
- # إضافة علامة للمشروع
- folium.Marker(
- location=project["coordinates"],
- popup=folium.Popup(popup_text, max_width=300),
- tooltip=project["name"],
- icon=folium.Icon(color=color, icon="info-sign")
- ).add_to(m)
-
- # عرض الخريطة
- folium_static(m, width=1000, height=500)
-
- # عرض إحصائيات المشاريع
- st.markdown("### إحصائيات المشاريع")
-
- col1, col2, col3, col4 = st.columns(4)
+ def render(self):
+ """عرض واجهة وحدة تطبيق الخريطة التفاعلية"""
+ st.markdown("
وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد
", unsafe_allow_html=True)
- with col1:
- self.ui.create_metric_card(
- "إجمالي المشاريع",
- str(len(filtered_projects)),
- None,
- self.ui.COLORS['primary']
- )
-
- with col2:
- projects_in_progress = len([p for p in filtered_projects if p["status"] == "جاري التنفيذ"])
- self.ui.create_metric_card(
- "مشاريع جارية",
- str(projects_in_progress),
- None,
- self.ui.COLORS['secondary']
- )
-
- with col3:
- total_budget = sum([p["budget"] for p in filtered_projects])
- self.ui.create_metric_card(
- "إجمالي الميزانية",
- f"{total_budget/1000000:.1f} مليون ريال",
- None,
- self.ui.COLORS['accent']
- )
-
- with col4:
- avg_completion = np.mean([p["completion"] for p in filtered_projects])
- self.ui.create_metric_card(
- "متوسط نسبة الإنجاز",
- f"{avg_completion:.1f}%",
- None,
- self.ui.COLORS['success']
- )
-
- def show_location_details(self):
- """عرض تفاصيل المواقع"""
- st.markdown("### تفاصيل مواقع المشاريع")
-
- # إنشاء جدول بيانات المشاريع
- projects_df = pd.DataFrame(self.projects_data)
- projects_df = projects_df.rename(columns={
- "id": "رقم المشروع",
- "name": "اسم المشروع",
- "location": "الموقع",
- "status": "الحالة",
- "budget": "الميزانية (ريال)",
- "completion": "نسبة الإنجاز (%)",
- "client": "العميل",
- "start_date": "تاريخ البدء",
- "end_date": "تاريخ الانتهاء"
- })
-
- # حذف عمود الإحداثيات من العرض
- projects_df = projects_df.drop(columns=["coordinates"])
-
- # عرض الجدول
- st.dataframe(
- projects_df,
- use_container_width=True,
- hide_index=True
- )
-
- # إضافة خيار تصدير البيانات
- col1, col2 = st.columns([1, 5])
- with col1:
- self.ui.create_button("تصدير البيانات", "primary")
-
- # عرض تفاصيل مشروع محدد
- st.markdown("### تفاصيل مشروع محدد")
-
- selected_project = st.selectbox(
- "اختر مشروعاً لعرض التفاصيل",
- options=[p["name"] for p in self.projects_data]
- )
-
- # العثور على المشروع المحدد
- project = next((p for p in self.projects_data if p["name"] == selected_project), None)
-
- if project:
- col1, col2 = st.columns([2, 1])
-
- with col1:
- # عرض تفاصيل المشروع
- st.markdown(f"#### {project['name']}")
- st.markdown(f"**الموقع:** {project['location']}")
- st.markdown(f"**الحالة:** {project['status']}")
- st.markdown(f"**الميزانية:** {project['budget']:,} ريال")
- st.markdown(f"**نسبة الإنجاز:** {project['completion']}%")
- st.markdown(f"**العميل:** {project['client']}")
- st.markdown(f"**تاريخ البدء:** {project['start_date']}")
- st.markdown(f"**تاريخ الانتهاء:** {project['end_date']}")
-
- # أزرار الإجراءات
- col1, col2, col3 = st.columns(3)
- with col1:
- self.ui.create_button("تعديل البيانات", "primary")
- with col2:
- self.ui.create_button("عرض المستندات", "secondary")
- with col3:
- self.ui.create_button("تقرير الموقع", "accent")
-
- with col2:
- # عرض خريطة مصغرة للمشروع
- m = folium.Map(location=project["coordinates"], zoom_start=12)
- folium.Marker(
- location=project["coordinates"],
- tooltip=project["name"],
- icon=folium.Icon(color="red", icon="info-sign")
- ).add_to(m)
- folium_static(m, width=300, height=300)
-
- def add_new_location(self):
- """إضافة موقع جديد"""
- st.markdown("### إضافة موقع مشروع جديد")
-
- # نموذج إضافة موقع جديد
- with st.form("new_location_form"):
- col1, col2 = st.columns(2)
-
- with col1:
- project_id = st.text_input("رقم المشروع", value="P00" + str(len(self.projects_data) + 1))
- project_name = st.text_input("اسم المشروع")
- location = st.text_input("الموقع")
- status = st.selectbox(
- "الحالة",
- options=["جاري التنفيذ", "قيد الدراسة", "مكتمل"]
- )
- budget = st.number_input("الميزانية (ريال)", min_value=0, step=100000)
-
- with col2:
- completion = st.slider("نسبة الإنجاز (%)", 0, 100, 0)
- client = st.text_input("العميل")
- start_date = st.date_input("تاريخ البدء")
- end_date = st.date_input("تاريخ الانتهاء")
-
- st.markdown("### تحديد الموقع على الخريطة")
- st.markdown("انقر على الخريطة لتحديد موقع المشروع أو أدخل الإحداثيات يدوياً")
-
- col1, col2 = st.columns(2)
-
- with col1:
- latitude = st.number_input("خط العرض", value=24.0, format="%.4f")
-
- with col2:
- longitude = st.number_input("خط الطول", value=45.0, format="%.4f")
-
- # عرض الخريطة لتحديد الموقع
- m = folium.Map(location=[latitude, longitude], zoom_start=5)
- folium.Marker(
- location=[latitude, longitude],
- tooltip="موقع المشروع الجديد",
- icon=folium.Icon(color="red", icon="info-sign")
- ).add_to(m)
- folium_static(m, width=700, height=300)
-
- # زر الإرسال
- submit_button = st.form_submit_button("إضافة المشروع")
-
- if submit_button:
- # إضافة المشروع الجديد (في تطبيق حقيقي، سيتم حفظ البيانات في قاعدة البيانات)
- st.success("تم إضافة المشروع بنجاح!")
-
- # إعادة تعيين النموذج
- st.experimental_rerun()
-
- def analyze_regions(self):
- """تحليل المناطق"""
- st.markdown("### تحليل المناطق")
-
- # إنشاء بيانات المناطق (نموذجية)
- regions_data = {
- "المنطقة": ["الرياض", "مكة المكرمة", "المدينة المنورة", "القصيم", "المنطقة الشرقية", "عسير", "تبوك", "حائل", "الحدود الشمالية", "جازان", "نجران", "الباحة", "الجوف"],
- "عدد المشاريع": [15, 12, 8, 5, 18, 7, 4, 3, 2, 6, 3, 2, 3],
- "إجمالي الميزانية (مليون ريال)": [120, 95, 45, 30, 150, 40, 25, 18, 12, 35, 20, 15, 22],
- "متوسط مدة المشروع (شهر)": [18, 16, 14, 12, 20, 15, 12, 10, 9, 14, 12, 10, 11]
- }
-
- regions_df = pd.DataFrame(regions_data)
-
- # عرض خريطة حرارية للمناطق
- st.markdown("#### توزيع المشاريع حسب المناطق")
-
- # في تطبيق حقيقي، يمكن استخدام خريطة حرارية حقيقية للمملكة
- st.image("https://via.placeholder.com/800x400?text=خريطة+حرارية+للمشاريع+حسب+المناطق", use_column_width=True)
-
- # عرض إحصائيات المناطق
- st.markdown("#### إحصائيات المناطق")
-
- # عرض الجدول
- st.dataframe(
- regions_df,
- use_container_width=True,
- hide_index=True
- )
-
- # عرض رسوم بيانية للمقارنة
- st.markdown("#### مقارنة المناطق")
-
- chart_type = st.radio(
- "نوع الرسم البياني",
- options=["عدد المشاريع", "إجمالي الميزانية", "متوسط مدة المشروع"],
- horizontal=True
- )
-
- if chart_type == "عدد المشاريع":
- chart_data = regions_df[["المنطقة", "عدد المشاريع"]].sort_values(by="عدد المشاريع", ascending=False)
- st.bar_chart(chart_data.set_index("المنطقة"))
- elif chart_type == "إجمالي الميزانية":
- chart_data = regions_df[["المنطقة", "إجمالي الميزانية (مليون ريال)"]].sort_values(by="إجمالي الميزانية (مليون ريال)", ascending=False)
- st.bar_chart(chart_data.set_index("المنطقة"))
- else:
- chart_data = regions_df[["المنطقة", "متوسط مدة المشروع (شهر)"]].sort_values(by="متوسط مدة المشروع (شهر)", ascending=False)
- st.bar_chart(chart_data.set_index("المنطقة"))
-
- # تحليل الكثافة
- st.markdown("#### تحليل كثافة المشاريع")
st.markdown("""
- يوضح هذا التحليل توزيع المشاريع حسب المناطق الجغرافية، مما يساعد في:
- - تحديد المناطق ذات النشاط العالي
- - تحديد فرص النمو في المناطق الأقل نشاطاً
- - تخطيط الموارد بناءً على التوزيع الجغرافي
- """)
-
- # في تطبيق حقيقي، يمكن إضافة تحليلات أكثر تفصيلاً
+
+ تمكنك هذه الوحدة من عرض وإدارة مواقع المشاريع على خريطة تفاعلية، مع إمكانية عرض التضاريس بشكل ثلاثي الأبعاد.
+ يمكنك إضافة وتحرير مواقع المشاريع، وتحليل توزيعها الجغرافي، وعرض المعلومات الطبوغرافية للمواقع.
+
+ """, unsafe_allow_html=True)
+
+ # عرض وحدة الخريطة التفاعلية
+ self.interactive_map.render()
+
-# تشغيل التطبيق
+# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
if __name__ == "__main__":
- maps_app = MapsApp()
- maps_app.run()
+ st.set_page_config(
+ page_title="الخريطة التفاعلية | WAHBi AI",
+ page_icon="🗺️",
+ layout="wide",
+ initial_sidebar_state="expanded"
+ )
+
+ app = MapsApp()
+ app.render()
\ No newline at end of file
diff --git a/modules/notifications/__init__.py b/modules/notifications/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a177748231ffad32b4284b13461159d77057db3d
--- /dev/null
+++ b/modules/notifications/__init__.py
@@ -0,0 +1 @@
+# ملف تهيئة وحدة الإشعارات الذكية
\ No newline at end of file
diff --git a/modules/notifications/notifications_app.py b/modules/notifications/notifications_app.py
index 8052ca90da563e7dd99aae37022185dd9de0e847..16aae363ddc6a62d64c8d66ecb40d0999c009f57 100644
--- a/modules/notifications/notifications_app.py
+++ b/modules/notifications/notifications_app.py
@@ -1,672 +1,53 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
"""
-وحدة الإشعارات الذكية - نظام تحليل المناقصات
+وحدة تطبيق نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات
"""
-import streamlit as st
-import pandas as pd
-import datetime
-import json
import os
import sys
-from pathlib import Path
+import streamlit as st
+import pandas as pd
+import numpy as np
-# إضافة مسار المشروع للنظام
-sys.path.append(str(Path(__file__).parent.parent))
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
+
+# استيراد مكونات الإشعارات الذكية
+from modules.notifications.smart_notifications import SmartNotificationSystem
-# استيراد محسن واجهة المستخدم
-from styling.enhanced_ui import UIEnhancer
class NotificationsApp:
- """تطبيق الإشعارات الذكية"""
+ """وحدة تطبيق نظام الإشعارات الذكي"""
def __init__(self):
- """تهيئة تطبيق الإشعارات الذكية"""
- self.ui = UIEnhancer(page_title="الإشعارات الذكية - نظام تحليل المناقصات", page_icon="🔔")
- self.ui.apply_theme_colors()
-
- # بيانات الإشعارات (نموذجية)
- self.notifications_data = [
- {
- "id": "N001",
- "title": "موعد تسليم مناقصة",
- "message": "موعد تسليم مناقصة T-2025-001 (إنشاء مبنى إداري) بعد 5 أيام",
- "type": "deadline",
- "priority": "high",
- "related_entity": "T-2025-001",
- "created_at": "2025-03-25T10:30:00",
- "is_read": False
- },
- {
- "id": "N002",
- "title": "ترسية مناقصة",
- "message": "تم ترسية مناقصة T-2025-003 (توريد معدات) بنجاح",
- "type": "award",
- "priority": "medium",
- "related_entity": "T-2025-003",
- "created_at": "2025-03-28T14:15:00",
- "is_read": True
- },
- {
- "id": "N003",
- "title": "تحديث مستندات",
- "message": "تم تحديث مستندات مناقصة T-2025-002 (صيانة طرق)",
- "type": "document",
- "priority": "medium",
- "related_entity": "T-2025-002",
- "created_at": "2025-03-29T09:45:00",
- "is_read": False
- },
- {
- "id": "N004",
- "title": "تغيير في المواصفات",
- "message": "تم تغيير المواصفات الفنية لمناقصة T-2025-001 (إنشاء مبنى إداري)",
- "type": "change",
- "priority": "high",
- "related_entity": "T-2025-001",
- "created_at": "2025-03-27T11:20:00",
- "is_read": False
- },
- {
- "id": "N005",
- "title": "تأخير في المشروع",
- "message": "تأخير في تنفيذ مشروع P002 (تطوير طريق الملك فهد - جدة)",
- "type": "delay",
- "priority": "high",
- "related_entity": "P002",
- "created_at": "2025-03-26T16:10:00",
- "is_read": True
- },
- {
- "id": "N006",
- "title": "اكتمال مرحلة",
- "message": "اكتمال مرحلة الأساسات في مشروع P001 (إنشاء مبنى إداري - الرياض)",
- "type": "milestone",
- "priority": "low",
- "related_entity": "P001",
- "created_at": "2025-03-24T13:30:00",
- "is_read": True
- },
- {
- "id": "N007",
- "title": "طلب معلومات إضافية",
- "message": "طلب معلومات إضافية لمناقصة T-2025-004 (تجهيز مختبرات)",
- "type": "request",
- "priority": "medium",
- "related_entity": "T-2025-004",
- "created_at": "2025-03-30T08:15:00",
- "is_read": False
- },
- {
- "id": "N008",
- "title": "تحديث أسعار المواد",
- "message": "تم تحديث أسعار مواد البناء في قاعدة البيانات",
- "type": "update",
- "priority": "low",
- "related_entity": "DB-MATERIALS",
- "created_at": "2025-03-29T15:40:00",
- "is_read": False
- },
- {
- "id": "N009",
- "title": "اجتماع فريق العمل",
- "message": "اجتماع فريق العمل لمناقشة مناقصة T-2025-001 غداً الساعة 10:00 صباحاً",
- "type": "meeting",
- "priority": "medium",
- "related_entity": "T-2025-001",
- "created_at": "2025-03-28T16:20:00",
- "is_read": True
- },
- {
- "id": "N010",
- "title": "تغيير في الميزانية",
- "message": "تم تغيير الميزانية المخصصة لمشروع P004 (بناء مدرسة - أبها)",
- "type": "budget",
- "priority": "high",
- "related_entity": "P004",
- "created_at": "2025-03-25T14:50:00",
- "is_read": False
- }
- ]
-
- # إعدادات الإشعارات (نموذجية)
- self.notification_settings = {
- "deadline": True,
- "award": True,
- "document": True,
- "change": True,
- "delay": True,
- "milestone": True,
- "request": True,
- "update": True,
- "meeting": True,
- "budget": True,
- "email_notifications": True,
- "sms_notifications": False,
- "push_notifications": True,
- "notification_frequency": "realtime"
- }
-
- 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.show_current_notifications()
-
- # علامة تبويب إعدادات الإشعارات
- with tabs[1]:
- self.show_notification_settings()
-
- # علامة تبويب إنشاء إشعار
- with tabs[2]:
- self.create_notification()
-
- # علامة تبويب سجل الإشعارات
- with tabs[3]:
- self.show_notification_history()
-
- def show_current_notifications(self):
- """عرض الإشعارات الحالية"""
- st.markdown("### الإشعارات الحالية")
-
- # إنشاء فلاتر للإشعارات
- col1, col2, col3 = st.columns(3)
-
- with col1:
- type_filter = st.multiselect(
- "نوع الإشعار",
- options=["الكل", "موعد نهائي", "ترسية", "مستند", "تغيير", "تأخير", "مرحلة", "طلب", "تحديث", "اجتماع", "ميزانية"],
- default=["الكل"]
- )
-
- with col2:
- priority_filter = st.multiselect(
- "الأولوية",
- options=["الكل", "عالية", "متوسطة", "منخفضة"],
- default=["الكل"]
- )
-
- with col3:
- read_filter = st.radio(
- "الحالة",
- options=["الكل", "غير مقروءة", "مقروءة"],
- horizontal=True
- )
-
- # تطبيق الفلاتر
- filtered_notifications = self.notifications_data
-
- # تحويل أنواع الإشعارات من الإنجليزية إلى العربية للفلترة
- type_mapping = {
- "موعد نهائي": "deadline",
- "ترسية": "award",
- "مستند": "document",
- "تغيير": "change",
- "تأخير": "delay",
- "مرحلة": "milestone",
- "طلب": "request",
- "تحديث": "update",
- "اجتماع": "meeting",
- "ميزانية": "budget"
- }
-
- # تحويل الأولويات من العربية إلى الإنجليزية للفلترة
- priority_mapping = {
- "عالية": "high",
- "متوسطة": "medium",
- "منخفضة": "low"
- }
-
- if "الكل" not in type_filter and type_filter:
- filtered_types = [type_mapping[t] for t in type_filter if t in type_mapping]
- filtered_notifications = [n for n in filtered_notifications if n["type"] in filtered_types]
-
- if "الكل" not in priority_filter and priority_filter:
- filtered_priorities = [priority_mapping[p] for p in priority_filter if p in priority_mapping]
- filtered_notifications = [n for n in filtered_notifications if n["priority"] in filtered_priorities]
-
- if read_filter == "غير مقروءة":
- filtered_notifications = [n for n in filtered_notifications if not n["is_read"]]
- elif read_filter == "مقروءة":
- filtered_notifications = [n for n in filtered_notifications if n["is_read"]]
-
- # عرض عدد الإشعارات غير المقروءة
- unread_count = len([n for n in filtered_notifications if not n["is_read"]])
-
- st.markdown(f"**عدد الإشعارات غير المقروءة:** {unread_count}")
-
- # زر تحديث وتعليم الكل كمقروء
- col1, col2 = st.columns([1, 1])
- with col1:
- if st.button("تحديث الإشعارات", use_container_width=True):
- st.success("تم تحديث الإشعارات بنجاح")
-
- with col2:
- if st.button("تعليم الكل كمقروء", use_container_width=True):
- st.success("تم تعليم جميع الإشعارات كمقروءة")
-
- # عرض الإشعارات
- if not filtered_notifications:
- st.info("لا توجد إشعارات تطابق الفلاتر المحددة")
- else:
- for notification in filtered_notifications:
- self.display_notification(notification)
-
- def display_notification(self, notification):
- """عرض إشعار واحد"""
- # تحديد لون الإشعار بناءً على الأولوية
- if notification["priority"] == "high":
- color = self.ui.COLORS['danger']
- priority_text = "عالية"
- elif notification["priority"] == "medium":
- color = self.ui.COLORS['warning']
- priority_text = "متوسطة"
- else:
- color = self.ui.COLORS['secondary']
- priority_text = "منخفضة"
-
- # تحويل نوع الإشعار إلى العربية
- type_mapping = {
- "deadline": "موعد نهائي",
- "award": "ترسية",
- "document": "مستند",
- "change": "تغيير",
- "delay": "تأخير",
- "milestone": "مرحلة",
- "request": "طلب",
- "update": "تحديث",
- "meeting": "اجتماع",
- "budget": "ميزانية"
- }
-
- notification_type = type_mapping.get(notification["type"], notification["type"])
-
- # تحويل التاريخ إلى تنسيق مناسب
- created_at = datetime.datetime.fromisoformat(notification["created_at"])
- formatted_date = created_at.strftime("%Y-%m-%d %H:%M")
-
- # تحديد أيقونة الإشعار
- icon_mapping = {
- "deadline": "⏰",
- "award": "🏆",
- "document": "📄",
- "change": "🔄",
- "delay": "⚠️",
- "milestone": "🏁",
- "request": "❓",
- "update": "🔄",
- "meeting": "👥",
- "budget": "💰"
- }
-
- icon = icon_mapping.get(notification["type"], "📌")
-
- # إنشاء بطاقة الإشعار
- st.markdown(
- f"""
-
", unsafe_allow_html=True)
+
+ # التأكد من وجود المتغيرات المطلوبة في حالة الجلسة وضمان أن لديهم قيم صحيحة
+ required_fields = {
+ 'materials_cost': 0.0,
+ 'equipment_cost': 0.0,
+ 'labor_cost': 0.0,
+ 'admin_cost': 0.0,
+ 'profit_margin': 15.0,
+ 'materials': [],
+ 'equipment': [],
+ 'labor': [],
+ 'admin_expenses': []
+ }
+
+ # مرور على كافة الحقول المطلوبة للتأكد من وجودها
+ for field, default_value in required_fields.items():
+ if field not in st.session_state:
+ st.session_state[field] = default_value
+
+ # التحقق من أن القيم العددية صالحة (غير None وليست NaN)
+ if field in ['materials_cost', 'equipment_cost', 'labor_cost', 'admin_cost', 'profit_margin']:
+ # إذا كانت القيمة None أو NaN، استخدم القيمة الافتراضية
+ if st.session_state[field] is None or pd.isna(st.session_state[field]):
+ st.session_state[field] = default_value
+
+ # حساب التكاليف المباشرة والإجمالية
+ direct_costs = st.session_state.materials_cost + st.session_state.equipment_cost + st.session_state.labor_cost
+ total_costs = direct_costs + st.session_state.admin_cost
+ profit_amount = total_costs * (st.session_state.profit_margin / 100)
+ total_price = total_costs + profit_amount
+
+ # معلومات المشروع
+ st.markdown("
', unsafe_allow_html=True)
+
+ # استخدام السعر المحسوب
+ use_calculated_price = st.checkbox("استخدام السعر المحسوب من التحليل", value=True, key="use_calc_edit")
+
+ # تحديد سعر الوحدة النهائي
+ if use_calculated_price and edited_qty > 0:
+ edited_price = unit_price_from_analysis
+ else:
+ edited_price = st.number_input(
+ "سعر الوحدة (تعديل)",
+ value=unit_price_from_analysis if edited_qty > 0 and unit_price_from_analysis > 0 else float(row['سعر الوحدة']),
+ min_value=0.0,
+ format="%.2f",
+ key="edit_price"
+ )
+
+ # حساب الإجمالي
+ edited_total = edited_qty * edited_price
+ st.info(f"إجمالي البند بعد التعديل: {edited_total:,.2f} ريال")
+
+ # مقارنة السعر المدخل مع السعر المحسوب
+ if not use_calculated_price and edited_qty > 0 and unit_price_from_analysis > 0:
+ price_diff = edited_price - unit_price_from_analysis
+ diff_percentage = (price_diff / unit_price_from_analysis) * 100
+
+ if abs(diff_percentage) > 5: # إذا كان الفرق أكبر من 5%
+ if diff_percentage > 0:
+ st.warning(f"السعر المدخل أعلى من السعر المحسوب بنسبة {diff_percentage:.2f}%")
+ else:
+ st.warning(f"السعر المدخل أقل من السعر المحسوب بنسبة {abs(diff_percentage):.2f}%")
+
+ # أزرار الإجراءات
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ if st.button("حفظ التعديلات", use_container_width=True):
+ # التحقق من صحة البيانات
+ if edited_id and edited_desc and edited_qty > 0:
+ # التحقق من تغيير رقم البند
+ if edited_id != edit_item_id:
+ # نقل تحليل السعر إلى الرقم الجديد
+ st.session_state.items_price_analysis[edited_id] = st.session_state.items_price_analysis.pop(edit_item_id)
+
+ # تحديث البند
+ st.session_state.manual_items.at[idx, 'رقم البند'] = edited_id
+ st.session_state.manual_items.at[idx, 'وصف البند'] = edited_desc
+ st.session_state.manual_items.at[idx, 'الوحدة'] = edited_unit
+ st.session_state.manual_items.at[idx, 'الكمية'] = edited_qty
+ st.session_state.manual_items.at[idx, 'سعر الوحدة'] = edited_price
+ st.session_state.manual_items.at[idx, 'الإجمالي'] = edited_total
+
+ st.success("تم تحديث البند وتحليل السعر بنجاح!")
+ time.sleep(0.5)
+ st.rerun()
+ else:
+ st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.")
+
+ with col2:
+ if st.button("استعادة القيم الأصلية", use_container_width=True):
+ # إعادة تحميل الصفحة لاستعادة القيم الأصلية
+ st.rerun()
+
+ with col3:
+ if st.button("حذف هذا البند", use_container_width=True):
+ # حذف تحليل السعر للبند
+ if edit_item_id in st.session_state.items_price_analysis:
+ del st.session_state.items_price_analysis[edit_item_id]
+
+ # حذف البند
+ st.session_state.manual_items = st.session_state.manual_items.drop(idx).reset_index(drop=True)
+
+ st.warning("تم حذف البند وتحليل السعر!")
+ time.sleep(0.5)
+ st.rerun()
diff --git a/modules/pricing/pricing_app.py b/modules/pricing/pricing_app.py
index ffe12ba5e30531ec4c4ebf28baf2089167aafaeb..b2d598c7d72429103bfbd7a5906634951498a2ab 100644
--- a/modules/pricing/pricing_app.py
+++ b/modules/pricing/pricing_app.py
@@ -1,5 +1,5 @@
"""
-وحدة التسعير - التطبيق الرئيسي
+تطبيق وحدة التسعير المتكاملة
"""
import streamlit as st
@@ -9,1752 +9,4350 @@ import matplotlib.pyplot as plt
import plotly.express as px
import plotly.graph_objects as go
from datetime import datetime
+import random
+import os
import time
import io
-import os
-import json
-import base64
-from pathlib import Path
+
+# ملاحظة: نحن لا نستخدم st.set_page_config هنا لأنه يجب أن يكون في ملف app.py الرئيسي فقط
+
+# تحسين المظهر العام باستخدام CSS
+st.markdown("""
+
+""", unsafe_allow_html=True)
+
+# تحسين شكل الأزرار بشكل متقدم
+st.markdown("""
+
+""", unsafe_allow_html=True)
+
+# وظيفة مساعدة لإنشاء أزرار بتنسيقات مختلفة
+def styled_button(label, key, type="primary", on_click=None, args=None, full_width=False, icon=None):
+ """
+ إنشاء زر بتنسيق معين
+ :param label: نص الزر
+ :param key: مفتاح الزر الفريد
+ :param type: نوع التنسيق ('primary', 'secondary', 'success', 'warning', 'danger', 'info', 'glass', 'flat')
+ :param on_click: الدالة التي سيتم تنفيذها عند النقر
+ :param args: معاملات الدالة
+ :param full_width: هل يأخذ الزر العرض كاملاً
+ :param icon: أيقونة لعرضها قبل النص (emoji)
+ :return: زر مُنسّق
+ """
+ # استخدام مكونات Streamlit فقط بدون HTML
+ with st.container():
+ # إنشاء مساحة تعرض الزر فقط
+ col1 = st.columns([1])
+
+ # إضافة الأيقونة للنص إذا تم تزويدها
+ display_label = f"{icon} {label}" if icon else label
+
+ # إنشاء الزر مباشرة باستخدام Streamlit
+ clicked = col1[0].button(
+ display_label,
+ key=key,
+ on_click=on_click,
+ args=args,
+ use_container_width=full_width
+ )
+
+ return clicked
+
+# وظيفة لإنشاء أزرار أيقونات صغيرة
+def icon_button(icon, key, type="primary", on_click=None, args=None, tooltip=""):
+ """
+ إنشاء زر أيقونة صغير
+ :param icon: الأيقونة (emoji)
+ :param key: مفتاح الزر الفريد
+ :param type: نوع التنسيق
+ :param on_click: الدالة التي سيتم تنفيذها عند النقر
+ :param args: معاملات الدالة
+ :param tooltip: تلميح عند تمرير المؤشر فوق الزر
+ :return: زر أيقونة
+ """
+ # استخدام مكونات Streamlit فقط
+ with st.container():
+ # إضافة تلميح باستخدام Streamlit
+ if tooltip:
+ st.caption(tooltip)
+
+ # إنشاء زر الأيقونة
+ clicked = st.button(
+ icon,
+ key=key,
+ on_click=on_click,
+ args=args,
+ help=tooltip # استخدام خاصية help لعرض التلميح
+ )
+
+ return clicked
+
+from modules.pricing.services.standard_pricing import StandardPricing
+from modules.pricing.services.unbalanced_pricing import UnbalancedPricing
+from modules.pricing.services.local_content_calculator import LocalContentCalculator
+from modules.pricing.services.price_prediction import PricePrediction
+from modules.pricing.services.construction_cost_calculator import ConstructionCostCalculator
+from modules.pricing.services.construction_templates import ConstructionTemplates
+from modules.pricing.services.templates_catalog.templates_catalog import TemplatesCatalog
+from utils.excel_handler import export_to_excel
+from utils.pdf_handler import export_pricing_to_pdf, export_pricing_with_analysis_to_pdf
+from utils.helpers import format_number, format_currency, create_directory_if_not_exists
+
class PricingApp:
- """وحدة التسعير"""
+ """وحدة التسعير المتكاملة"""
def __init__(self):
- """تهيئة وحدة التسعير"""
-
- # تهيئة حالة الجلسة
- if 'bill_of_quantities' not in st.session_state:
- st.session_state.bill_of_quantities = [
- {
- 'id': 1,
- 'code': 'A-001',
- 'description': 'أعمال الحفر والردم',
- 'unit': 'م3',
- 'quantity': 1500,
- 'unit_price': 45,
- 'total_price': 67500,
- 'category': 'أعمال ترابية'
- },
- {
- 'id': 2,
- 'code': 'A-002',
- 'description': 'توريد وصب خرسانة عادية',
- 'unit': 'م3',
- 'quantity': 250,
- 'unit_price': 350,
- 'total_price': 87500,
- 'category': 'أعمال خرسانية'
- },
- {
- 'id': 3,
- 'code': 'A-003',
- 'description': 'توريد وصب خرسانة مسلحة للأساسات',
- 'unit': 'م3',
- 'quantity': 180,
- 'unit_price': 450,
- 'total_price': 81000,
- 'category': 'أعمال خرسانية'
- },
- {
- 'id': 4,
- 'code': 'B-001',
- 'description': 'توريد وتركيب حديد تسليح',
- 'unit': 'طن',
- 'quantity': 15,
- 'unit_price': 3500,
- 'total_price': 52500,
- 'category': 'أعمال حديد'
- },
- {
- 'id': 5,
- 'code': 'C-001',
- 'description': 'توريد وبناء طابوق',
- 'unit': 'م2',
- 'quantity': 450,
- 'unit_price': 120,
- 'total_price': 54000,
- 'category': 'أعمال بناء'
- }
- ]
-
- if 'cost_analysis' not in st.session_state:
- st.session_state.cost_analysis = [
- {
- 'id': 1,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'مواد',
- 'description': 'خرسانة',
- 'amount': 168500,
- 'percentage': 25.2
- },
- {
- 'id': 2,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'مواد',
- 'description': 'حديد تسليح',
- 'amount': 52500,
- 'percentage': 7.8
- },
- {
- 'id': 3,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'مواد',
- 'description': 'طابوق',
- 'amount': 54000,
- 'percentage': 8.1
- },
- {
- 'id': 4,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'عمالة',
- 'description': 'عمالة تنفيذ',
- 'amount': 120000,
- 'percentage': 17.9
- },
- {
- 'id': 5,
- 'category': 'تكاليف مباشرة',
- 'subcategory': 'معدات',
- 'description': 'معدات إنشائية',
- 'amount': 85000,
- 'percentage': 12.7
- },
- {
- 'id': 6,
- 'category': 'تكاليف غير مباشرة',
- 'subcategory': 'إدارة',
- 'description': 'إدارة المشروع',
- 'amount': 45000,
- 'percentage': 6.7
- },
- {
- 'id': 7,
- 'category': 'تكاليف غير مباشرة',
- 'subcategory': 'إدارة',
- 'description': 'إشراف هندسي',
- 'amount': 35000,
- 'percentage': 5.2
- },
- {
- 'id': 8,
- 'category': 'تكاليف غير مباشرة',
- 'subcategory': 'عامة',
- 'description': 'تأمينات وضمانات',
- 'amount': 25000,
- 'percentage': 3.7
- },
- {
- 'id': 9,
- 'category': 'تكاليف غير مباشرة',
- 'subcategory': 'عامة',
- 'description': 'مصاريف إدارية',
- 'amount': 30000,
- 'percentage': 4.5
- },
- {
- 'id': 10,
- 'category': 'أرباح',
- 'subcategory': 'أرباح',
- 'description': 'هامش الربح',
- 'amount': 55000,
- 'percentage': 8.2
- }
- ]
+ """تهيئة وحدة التسعير المتكاملة"""
+ self.pricing_methods = [
+ "التسعير القياسي",
+ "التسعير غير المتزن",
+ "التسعير التنافسي",
+ "التسعير الموجه بالربحية"
+ ]
- if 'price_scenarios' not in st.session_state:
- st.session_state.price_scenarios = [
- {
- 'id': 1,
- 'name': 'السيناريو الأساسي',
- 'description': 'التسعير الأساسي مع هامش ربح 8%',
- 'total_cost': 615000,
- 'profit_margin': 8.2,
- 'total_price': 670000,
- 'is_active': True
- },
- {
- 'id': 2,
- 'name': 'سيناريو تنافسي',
- 'description': 'تخفيض هامش الربح للمنافسة',
- 'total_cost': 615000,
- 'profit_margin': 5.0,
- 'total_price': 650000,
- 'is_active': False
- },
- {
- 'id': 3,
- 'name': 'سيناريو مرتفع',
- 'description': 'زيادة هامش الربح للمشاريع ذات المخاطر العالية',
- 'total_cost': 615000,
- 'profit_margin': 12.0,
- 'total_price': 700000,
- 'is_active': False
- }
- ]
+ # تهيئة خدمات التسعير
+ self.standard_pricing = StandardPricing()
+ self.unbalanced_pricing = UnbalancedPricing()
+ self.local_content = LocalContentCalculator()
+ self.price_prediction = PricePrediction()
+ self.construction_calculator = ConstructionCostCalculator()
+ self.construction_templates = ConstructionTemplates()
+ self.templates_catalog = TemplatesCatalog(self.construction_templates)
def render(self):
"""عرض واجهة وحدة التسعير"""
- st.markdown("
وحدة التسعير
", unsafe_allow_html=True)
+ # استخدام مكونات Streamlit مباشرة بدلاً من HTML
+ st.title("وحدة التسعير المتكاملة")
tabs = st.tabs([
- "لوحة التحكم",
- "جدول الكميات",
- "تحليل التكاليف",
- "سيناريوهات التسعير",
- "المقارنة التنافسية",
- "التقارير"
- ])
+ "إنشاء تسعير جديد",
+ "تحليل سعر البند",
+ "نموذج التسعير الشامل",
+ "التسعير غير المتزن",
+ "المحتوى المحلي",
+ "حاسبة تكاليف البناء",
+ "كتالوج البنود النموذجية",
+ "الأدوات المساعدة"
+ ])
with tabs[0]:
- self._render_dashboard_tab()
+ self._render_new_pricing_tab()
with tabs[1]:
- self._render_bill_of_quantities_tab()
+ self._render_item_analysis_tab()
with tabs[2]:
- self._render_cost_analysis_tab()
+ self._render_comprehensive_pricing_tab()
with tabs[3]:
- self._render_pricing_scenarios_tab()
+ self._render_unbalanced_pricing_tab()
with tabs[4]:
- self._render_competitive_analysis_tab()
-
+ self._render_local_content_tab()
+
with tabs[5]:
- self._render_reports_tab()
+ self._render_construction_calculator_tab()
+
+ with tabs[6]:
+ self._render_templates_catalog_tab()
+
+ with tabs[7]:
+ self._render_utilities_tab()
+
+ def _render_templates_catalog_tab(self):
+ """عرض تبويب كتالوج البنود النموذجية"""
+
+ st.markdown("### كتالوج البنود النموذجية")
+
+ # شرح كتالوج البنود النموذجية
+ with st.expander("دليل استخدام كتالوج البنود النموذجية", expanded=False):
+ st.markdown("""
+ **كتالوج البنود النموذجية** هو مكتبة شاملة من البنود الجاهزة لمختلف أنواع الأعمال الإنشائية (خرسانة، حديد، عزل، تشطيبات، إلخ).
+
+ ### مميزات الكتالوج:
+ - تفاصيل دقيقة للمواد والعمالة والمعدات المطلوبة لكل بند.
+ - تحليل تكلفة تفصيلي يمكن استخدامه مباشرة في عروض الأسعار.
+ - ربط مباشر مع حاسبة تكاليف البناء وحاسبة الأسعار.
+
+ ### كيفية الاستخدام:
+ - استخدام البنود النموذجية مباشرة في مشاريعك.
+ - تعديل البنود النموذجية لتناسب متطلبات المشروع.
+ - إضافة بنود جديدة إلى الكتالوج للاستخدام المستقبلي.
+ """)
+
+ # عرض الكتالوج باستخدام مكون TemplatesCatalog
+ self.templates_catalog.render()
- def _render_dashboard_tab(self):
- """عرض تبويب لوحة التحكم"""
+ def _render_item_analysis_tab(self):
+ """عرض تبويب تحليل سعر البند"""
- st.markdown("### لوحة تحكم التسعير")
+ st.markdown("### تحليل سعر البند")
+
+ # التحقق من وجود تسعير حالي
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
+ return
+
+ # اختيار البند للتحليل
+ if 'current_pricing' in st.session_state and st.session_state.current_pricing is not None:
+ items = st.session_state.current_pricing['items']
+ item_options = items['رقم البند'].tolist()
+ selected_item = st.selectbox("اختر البند للتحليل", item_options, key="item_analysis_selector")
+
+ if selected_item:
+ item_data = items[items['رقم البند'] == selected_item].iloc[0]
+
+ st.markdown(f"### تحليل البند: {selected_item}")
+ st.markdown(f"**وصف البند**: {item_data['وصف البند']}")
+ st.markdown(f"**الوحدة**: {item_data['الوحدة']}")
+ st.markdown(f"**الكمية**: {item_data['الكمية']}")
+ st.markdown(f"**سعر الوحدة**: {item_data['سعر الوحدة']:,.2f} ريال")
+
+ # تحليل مكونات السعر
+ st.markdown("### تحليل مكونات السعر")
+
+ # عناصر التكلفة الافتراضية
+ cost_components = {
+ 'المواد': 0.6, # 60% من التكلفة
+ 'العمالة': 0.25, # 25% من التكلفة
+ 'المعدات': 0.1, # 10% من التكلفة
+ 'نفقات عامة': 0.05 # 5% من التكلفة
+ }
+
+ # حساب تكلفة كل عنصر
+ unit_price = item_data['سعر الوحدة']
+ component_values = {k: v * unit_price for k, v in cost_components.items()}
+
+ # عرض مكونات التكلفة في جدول
+ components_df = pd.DataFrame({
+ 'العنصر': component_values.keys(),
+ 'نسبة من التكلفة': [f"{v*100:.1f}%" for v in cost_components.values()],
+ 'القيمة (ريال)': [f"{v:,.2f}" for v in component_values.values()]
+ })
+
+ st.table(components_df)
+
+ # رسم بياني لمكونات التكلفة
+ fig = px.pie(
+ names=list(component_values.keys()),
+ values=list(component_values.values()),
+ title='توزيع مكونات التكلفة'
+ )
+
+ st.plotly_chart(fig)
+
+ # تحليل تاريخي للأسعار
+ st.markdown("### تحليل تاريخي للأسعار")
+
+ # بيانات تاريخية افتراضية
+ historical_data = {
+ 'التاريخ': ['2020-01', '2020-07', '2021-01', '2021-07', '2022-01', '2022-07', '2023-01', '2023-07'],
+ 'السعر': [
+ unit_price * 0.7,
+ unit_price * 0.75,
+ unit_price * 0.8,
+ unit_price * 0.85,
+ unit_price * 0.9,
+ unit_price * 0.95,
+ unit_price,
+ unit_price * 1.05
+ ]
+ }
+
+ hist_df = pd.DataFrame(historical_data)
+
+ # رسم بياني للتحليل التاريخي
+ fig = px.line(
+ hist_df,
+ x='التاريخ',
+ y='السعر',
+ title='تطور سعر الوحدة عبر الزمن',
+ markers=True
+ )
+
+ st.plotly_chart(fig)
+
+ # المقارنة مع الأسعار المرجعية
+ st.markdown("### المقارنة مع الأسعار المرجعية")
+
+ # بيانات مرجعية افتراضية
+ reference_data = {
+ 'المصدر': ['قاعدة البيانات الداخلية', 'دليل الأسعار الاسترشادي', 'متوسط أسعار السوق', 'أسعار المشاريع المماثلة'],
+ 'السعر المرجعي': [
+ unit_price * 0.95,
+ unit_price * 1.05,
+ unit_price * 1.1,
+ unit_price * 0.9
+ ]
+ }
+
+ ref_df = pd.DataFrame(reference_data)
+ ref_df['الفرق عن السعر الحالي'] = ref_df['السعر المرجعي'] - unit_price
+ ref_df['نسبة الفرق'] = (ref_df['الفرق عن السعر الحالي'] / unit_price * 100).round(2).astype(str) + '%'
+
+ st.table(ref_df)
+
+ def _render_new_pricing_tab(self):
+ """عرض تبويب إنشاء تسعير جديد"""
- # عرض ملخص التسعير
- col1, col2, col3, col4 = st.columns(4)
+ st.markdown("### إنشاء تسعير جديد")
- # حساب إجمالي التكاليف
- total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
- total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
- total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح')
- total_cost = total_direct_cost + total_indirect_cost
- total_price = total_cost + total_profit
+ col1, col2 = st.columns(2)
with col1:
- st.metric("إجمالي التكاليف المباشرة", f"{total_direct_cost:,.0f} ريال")
+ tender_name = st.text_input("اسم المناقصة", key="tender_name_input")
+ client = st.text_input("الجهة المالكة", key="client_input")
+ pricing_method = st.selectbox("طريقة التسعير", self.pricing_methods, key="pricing_method_selector")
with col2:
- st.metric("إجمالي التكاليف غير المباشرة", f"{total_indirect_cost:,.0f} ريال")
+ tender_number = st.text_input("رقم المناقصة", key="tender_number_input")
+ location = st.text_input("الموقع", key="location_input")
+ submission_date = st.date_input("تاريخ التقديم", key="submission_date_input")
- with col3:
- st.metric("إجمالي التكاليف", f"{total_cost:,.0f} ريال")
-
- with col4:
- st.metric("السعر الإجمالي", f"{total_price:,.0f} ريال")
+ # خيارات بيانات البنود
+ st.markdown("### بيانات البنود")
- # عرض توزيع التكاليف
- st.markdown("### توزيع التكاليف")
+ data_source = st.radio(
+ "مصدر بيانات البنود",
+ ["إدخال يدوي", "استيراد من Excel", "استيراد من وحدة تحليل المستندات", "استيراد من وحدة المشاريع"],
+ key="data_source_radio"
+ )
- # تجميع البيانات حسب الفئة
- cost_categories = {}
+ if data_source == "إدخال يدوي":
+ # ضبط CSS لتحسين ظهور الواجهة العربية
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+ # تهيئة قائمة الوحدات المتاحة
+ unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"]
+
+ # إنشاء بيانات افتراضية إذا لم تكن موجودة
+ if 'manual_items' not in st.session_state:
+ # إنشاء DataFrame فارغ
+ manual_items = pd.DataFrame(columns=[
+ 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي'
+ ])
+
+ # إضافة بضعة صفوف افتراضية
+ default_items = pd.DataFrame({
+ 'رقم البند': ["A1", "A2", "A3", "A4", "A5"],
+ 'وصف البند': [
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
+ "توريد وتركيب حديد التسليح للأساسات",
+ "أعمال العزل المائي للأساسات",
+ "أعمال الردم والدك للأساسات",
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة"
+ ],
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3"],
+ 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0],
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0],
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0]
+ })
+
+ manual_items = pd.concat([manual_items, default_items])
+ st.session_state.manual_items = manual_items
+
+ # عرض واجهة إدخال البنود
+ st.markdown("### إدخال تفاصيل البنود")
+
+ # التحقق من استخدام طريقة الإدخال البسيطة
+ use_simple_input = st.checkbox("استخدام طريقة الإدخال البسيطة", value=True, key="use_simple_input_checkbox")
+
+ if use_simple_input:
+ # عرض البنود الحالية كجدول للعرض فقط
+ st.markdown("### جدول البنود الحالية")
+ st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True)
+
+ # إضافة بند جديد
+ st.markdown("### إضافة بند جديد")
+ col1, col2 = st.columns(2)
+
+ with col1:
+ new_id = st.text_input("رقم البند", value=f"A{len(st.session_state.manual_items)+1}", key="new_item_id")
+ new_desc = st.text_area("وصف البند", value="", key="new_item_description")
+
+ with col2:
+ new_unit = st.selectbox("الوحدة", options=unit_options, key="new_item_unit")
+ new_qty = st.number_input("الكمية", value=0.0, min_value=0.0, format="%.2f", key="new_item_qty")
+ new_price = st.number_input("سعر الوحدة", value=0.0, min_value=0.0, format="%.2f", key="new_item_price")
+
+ new_total = new_qty * new_price
+ st.info(f"إجمالي البند الجديد: {new_total:,.2f} ريال")
+
+ if st.button("إضافة البند", key="add_item_button"):
+ # التحقق من صحة البيانات
+ if new_id and new_desc and new_qty > 0:
+ # إنشاء صف جديد
+ new_row = pd.DataFrame({
+ 'رقم البند': [new_id],
+ 'وصف البند': [new_desc],
+ 'الوحدة': [new_unit],
+ 'الكمية': [float(new_qty)],
+ 'سعر الوحدة': [float(new_price)],
+ 'الإجمالي': [float(new_total)]
+ })
+
+ # إضافة الصف إلى DataFrame
+ st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True)
+ st.success("تم إضافة البند بنجاح!")
+ st.rerun()
+ else:
+ st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.")
+
+ # تعديل البنود الحالية
+ st.markdown("### تعديل البنود الحالية")
+
+ # تحديد البند المراد تعديله
+ item_to_edit = st.selectbox(
+ "اختر البند للتعديل",
+ options=st.session_state.manual_items['رقم البند'].tolist(),
+ format_func=lambda x: f"{x}: {st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == x]['وصف البند'].values[0][:30]}...",
+ key="item_to_edit_selector"
+ )
+
+ if item_to_edit:
+ # الحصول على مؤشر الصف للبند المحدد
+ idx = st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == item_to_edit].index[0]
+ row = st.session_state.manual_items.loc[idx]
+
+ # إنشاء نموذج تعديل
+ col1, col2 = st.columns(2)
+
+ with col1:
+ edited_id = st.text_input("رقم البند (تعديل)", value=row['رقم البند'], key="edit_id")
+ edited_desc = st.text_area("وصف البند (تعديل)", value=row['وصف البند'], key="edit_desc")
+
+ with col2:
+ edited_unit = st.selectbox(
+ "الوحدة (تعديل)",
+ options=unit_options,
+ index=unit_options.index(row['الوحدة']) if row['الوحدة'] in unit_options else 0,
+ key="edit_unit"
+ )
+ edited_qty = st.number_input("الكمية (تعديل)", value=float(row['الكمية']), min_value=0.0, format="%.2f", key="edit_qty")
+ edited_price = st.number_input("سعر الوحدة (تعديل)", value=float(row['سعر الوحدة']), min_value=0.0, format="%.2f", key="edit_price")
+
+ edited_total = edited_qty * edited_price
+ st.info(f"إجمالي البند بعد التعديل: {edited_total:,.2f} ريال")
+
+ col1, col2 = st.columns(2)
+ with col1:
+ if st.button("حفظ التعديلات", key="save_edit_button"):
+ # تحديث البند
+ st.session_state.manual_items.at[idx, 'رقم البند'] = edited_id
+ st.session_state.manual_items.at[idx, 'وصف البند'] = edited_desc
+ st.session_state.manual_items.at[idx, 'الوحدة'] = edited_unit
+ st.session_state.manual_items.at[idx, 'الكمية'] = edited_qty
+ st.session_state.manual_items.at[idx, 'سعر الوحدة'] = edited_price
+ st.session_state.manual_items.at[idx, 'الإجمالي'] = edited_total
+
+ st.success("تم تحديث البند بنجاح!")
+ st.rerun()
+
+ with col2:
+ if st.button("حذف هذا البند", key="delete_item_button"):
+ st.session_state.manual_items = st.session_state.manual_items.drop(idx).reset_index(drop=True)
+ st.warning("تم حذف البند!")
+ st.rerun()
+
+ # المجموع الكلي
+ total = st.session_state.manual_items['الإجمالي'].sum()
+ st.metric("المجموع الكلي", f"{total:,.2f} ريال")
+
+ # جعل هذه البيانات متاحة للاستخدام في الخطوات التالية
+ edited_items = st.session_state.manual_items.copy()
+
+ else:
+ # عرض رسالة توضح أن طريقة الإدخال البسيطة هي الأفضل
+ st.warning("لتجنب مشاكل عدم التوافق في أنواع البيانات، يُفضل استخدام طريقة الإدخال البسيطة.")
+
+ # محاولة استخدام المحرر القياسي مع معالجة الأخطاء
+ try:
+ # تحويل البيانات إلى الأنواع المناسبة
+ for col in st.session_state.manual_items.columns:
+ if col in ['رقم البند', 'وصف البند', 'الوحدة']:
+ st.session_state.manual_items[col] = st.session_state.manual_items[col].astype(str)
+
+ # عرض المحرر (للقراءة فقط)
+ st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True)
+
+ # إنشاء نظام تعديل منفصل
+ st.markdown("### تعديل أسعار الوحدات")
+
+ for idx, row in st.session_state.manual_items.iterrows():
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ st.text(f"{row['رقم البند']}: {row['وصف البند'][:50]}")
+
+ with col2:
+ price = st.number_input(
+ f"سعر الوحدة ({row['الوحدة']})",
+ value=float(row['سعر الوحدة']),
+ min_value=0.0,
+ key=f"price_{idx}"
+ )
+
+ # تحديث السعر والإجمالي
+ st.session_state.manual_items.at[idx, 'سعر الوحدة'] = price
+ st.session_state.manual_items.at[idx, 'الإجمالي'] = price * row['الكمية']
+
+ # المجموع الكلي
+ total = st.session_state.manual_items['الإجمالي'].sum()
+ st.metric("المجموع الكلي", f"{total:,.2f} ريال")
+
+ # جعل هذه البيانات متاحة للاستخدام في الخطوات التالية
+ edited_items = st.session_state.manual_items.copy()
+
+ except Exception as e:
+ st.error(f"حدث خطأ: {str(e)}")
+ st.info("يرجى استخدام طريقة الإدخال البسيطة لتجنب هذه المشكلة.")
- for item in st.session_state.cost_analysis:
- category = item['category']
- if category in cost_categories:
- cost_categories[category] += item['amount']
+ elif data_source == "استيراد من Excel":
+ uploaded_file = st.file_uploader("رفع ملف Excel", type=["xlsx", "xls"])
+
+ if uploaded_file is not None:
+ st.success("تم رفع الملف بنجاح")
+ # محاكاة قراءة الملف
+ st.markdown("### معاينة البيانات المستوردة")
+
+ # إنشاء بيانات افتراضية
+ import_items = pd.DataFrame({
+ 'رقم البند': ["A1", "A2", "A3", "A4", "A5", "A6", "A7"],
+ 'وصف البند': [
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
+ "توريد وتركيب حديد التسليح للأساسات",
+ "أعمال العزل المائي للأساسات",
+ "أعمال الردم والدك للأساسات",
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة",
+ "توريد وتركيب حديد التسليح للأعمدة",
+ "أعمال البلوك للجدران"
+ ],
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"],
+ 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0, 10.0, 400.0],
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+ })
+
+ st.dataframe(import_items)
+
+ if st.button("استيراد البيانات", key="import_excel_button"):
+ st.session_state.manual_items = import_items.copy()
+ st.session_state.manual_items_modified = True
+ st.success("تم استيراد البيانات بنجاح!")
+ st.rerun()
+
+ elif data_source == "استيراد من وحدة تحليل المستندات":
+ available_documents = [
+ "كراسة شروط مشروع توسعة مستشفى الملك فهد",
+ "جدول كميات صيانة محطات المياه",
+ "مخططات إنشاء مدرسة ثانوية"
+ ]
+
+ selected_doc = st.selectbox("اختر المستند", available_documents, key="document_selector")
+
+ if styled_button("استيراد البيانات من تحليل المستند", key="import_doc_analysis_button", type="info", icon="📄", full_width=True):
+ # محاكاة استيراد البيانات
+ with st.spinner("جاري استيراد البيانات..."):
+ time.sleep(2)
+
+ # إنشاء بيانات افتراضية
+ doc_items = pd.DataFrame({
+ 'رقم البند': ["A1", "A2", "A3", "A4", "A5", "A6", "A7"],
+ 'وصف البند': [
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
+ "توريد وتركيب حديد التسليح للأساسات",
+ "أعمال العزل المائي للأساسات",
+ "أعمال الردم والدك للأساسات",
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة",
+ "توريد وتركيب حديد التسليح للأعمدة",
+ "أعمال البلوك للجدران"
+ ],
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"],
+ 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0, 10.0, 400.0],
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+ })
+
+ st.session_state.manual_items = doc_items.copy()
+ st.success("تم استيراد البيانات من تحليل المستند بنجاح!")
+ st.dataframe(doc_items)
+
+ elif data_source == "استيراد من وحدة المشاريع":
+ # قائمة المشاريع المتاحة للاستيراد منها
+ available_projects = [
+ "مشروع تطوير طريق الملك عبدالعزيز",
+ "مشروع إنشاء محطة تحلية المياه بالجبيل",
+ "مشروع توسعة مستشفى الملك فهد",
+ "مشروع إنشاء مجمع سكني بالرياض"
+ ]
+
+ selected_project = st.selectbox("اختر المشروع", available_projects, key="project_selector")
+
+ if styled_button("استيراد البيانات من المشروع", key="import_project_data_button", type="primary", icon="🏗️", full_width=True):
+ # محاكاة استيراد البيانات من المشروع
+ with st.spinner("جاري استيراد بيانات المشروع..."):
+ time.sleep(1.5)
+
+ # إنشاء بيانات مشروع افتراضية بناءً على المشروع المختار
+ if selected_project == "مشروع تطوير طريق الملك عبدالعزيز":
+ project_items = pd.DataFrame({
+ 'رقم البند': ["R1", "R2", "R3", "R4", "R5"],
+ 'وصف البند': [
+ "أعمال الحفر والردم للطريق",
+ "توريد وتنفيذ طبقة الأساس",
+ "طبقة الأسفلت الأولى",
+ "طبقة الأسفلت النهائية",
+ "أعمال الدهانات والعلامات"
+ ],
+ 'الوحدة': ["م3", "م2", "م2", "م2", "م.ط"],
+ 'الكمية': [5000.0, 8000.0, 8000.0, 8000.0, 4000.0],
+ 'سعر الوحدة': [45.0, 120.0, 85.0, 95.0, 25.0],
+ 'الإجمالي': [225000.0, 960000.0, 680000.0, 760000.0, 100000.0]
+ })
+ elif selected_project == "مشروع إنشاء محطة تحلية المياه بالجبيل":
+ project_items = pd.DataFrame({
+ 'رقم البند': ["W1", "W2", "W3", "W4", "W5"],
+ 'وصف البند': [
+ "أعمال الخرسانة المسلحة للخزانات",
+ "توريد وتركيب معدات التحلية",
+ "أعمال التمديدات والأنابيب",
+ "تشطيبات المباني الإدارية",
+ "أعمال الكهرباء والتحكم"
+ ],
+ 'الوحدة': ["م3", "قطعة", "م.ط", "م2", "مقطوعية"],
+ 'الكمية': [1800.0, 12.0, 5000.0, 1200.0, 1.0],
+ 'سعر الوحدة': [1200.0, 250000.0, 850.0, 750.0, 1500000.0],
+ 'الإجمالي': [2160000.0, 3000000.0, 4250000.0, 900000.0, 1500000.0]
+ })
+ elif selected_project == "مشروع توسعة مستشفى الملك فهد":
+ project_items = pd.DataFrame({
+ 'رقم البند': ["H1", "H2", "H3", "H4", "H5", "H6"],
+ 'وصف البند': [
+ "أعمال الهيكل الخرساني",
+ "أعمال البناء والجدران",
+ "التشطيبات الداخلية",
+ "الأنظمة الكهربائية والميكانيكية",
+ "تجهيزات طبية",
+ "أعمال الموقع العام"
+ ],
+ 'الوحدة': ["م3", "م2", "م2", "غرفة", "قطعة", "م2"],
+ 'الكمية': [2800.0, 4500.0, 7500.0, 120.0, 45.0, 3000.0],
+ 'سعر الوحدة': [1500.0, 650.0, 1200.0, 35000.0, 80000.0, 350.0],
+ 'الإجمالي': [4200000.0, 2925000.0, 9000000.0, 4200000.0, 3600000.0, 1050000.0]
+ })
+ else: # مشروع إنشاء مجمع سكني بالرياض
+ project_items = pd.DataFrame({
+ 'رقم البند': ["B1", "B2", "B3", "B4", "B5", "B6", "B7"],
+ 'وصف البند': [
+ "الهياكل الخرسانية للفلل",
+ "الجدران والقواطع الداخلية",
+ "التشطيبات الخارجية",
+ "التشطيبات الداخلية",
+ "أعمال الكهرباء والسباكة",
+ "الملحقات والحدائق",
+ "البنية التحتية للموقع"
+ ],
+ 'الوحدة': ["م3", "م2", "م2", "م2", "فيلا", "م2", "مقطوعية"],
+ 'الكمية': [3500.0, 12000.0, 8000.0, 18000.0, 25.0, 5000.0, 1.0],
+ 'سعر الوحدة': [1350.0, 450.0, 380.0, 750.0, 85000.0, 280.0, 2500000.0],
+ 'الإجمالي': [4725000.0, 5400000.0, 3040000.0, 13500000.0, 2125000.0, 1400000.0, 2500000.0]
+ })
+
+ st.session_state.manual_items = project_items.copy()
+ st.success(f"تم استيراد بيانات المشروع '{selected_project}' بنجاح!")
+ st.dataframe(project_items)
+
+ # زر بدء التسعير
+ if styled_button("بدء التسعير", key="start_pricing_button", type="success", icon="✅", full_width=True):
+ # تحقق من صحة البيانات
+ if 'manual_items' in st.session_state and not st.session_state.manual_items.empty:
+ # التأكد من حساب الإجمالي قبل الحفظ
+ st.session_state.manual_items['الإجمالي'] = st.session_state.manual_items['الكمية'] * st.session_state.manual_items['سعر الوحدة']
+
+ # حفظ بيانات التسعير الحالي
+ st.session_state.current_pricing = {
+ 'name': tender_name,
+ 'number': tender_number,
+ 'client': client,
+ 'location': location,
+ 'method': pricing_method,
+ 'submission_date': submission_date,
+ 'items': st.session_state.manual_items.copy(),
+ 'status': 'جديد',
+ 'created_at': datetime.now()
+ }
+
+ # الانتقال إلى تبويب نموذج التسعير الشامل
+ st.success("تم إنشاء التسعير بنجاح! يمكنك الانتقال إلى نموذج التسعير الشامل.")
else:
- cost_categories[category] = item['amount']
+ st.error("يرجى إدخال بيانات البنود أولاً.")
+
+ def _render_comprehensive_pricing_tab(self):
+ """عرض تبويب نموذج التسعير الشامل"""
- # إنشاء DataFrame للرسم البياني
- cost_df = pd.DataFrame({
- 'الفئة': list(cost_categories.keys()),
- 'المبلغ': list(cost_categories.values())
- })
+ st.markdown("### نموذج التسعير الشامل")
- # إنشاء رسم بياني دائري
- fig = px.pie(
- cost_df,
- values='المبلغ',
- names='الفئة',
- title='توزيع التكاليف حسب الفئة',
- color_discrete_sequence=px.colors.qualitative.Set3
- )
+ # التحقق من وجود تسعير حالي
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
+ return
- st.plotly_chart(fig, use_container_width=True)
+ # عرض معلومات التسعير الحالي
+ pricing = st.session_state.current_pricing
- # عرض توزيع التكاليف المباشرة
- st.markdown("### توزيع التكاليف المباشرة")
+ col1, col2, col3 = st.columns(3)
- # تجميع البيانات حسب الفئة الفرعية للتكاليف المباشرة
- direct_cost_subcategories = {}
+ with col1:
+ st.metric("اسم المناقصة", pricing['name'])
+ st.metric("الجهة المالكة", pricing['client'])
- for item in st.session_state.cost_analysis:
- if item['category'] == 'تكاليف مباشرة':
- subcategory = item['subcategory']
- if subcategory in direct_cost_subcategories:
- direct_cost_subcategories[subcategory] += item['amount']
- else:
- direct_cost_subcategories[subcategory] = item['amount']
+ with col2:
+ st.metric("رقم المناقصة", pricing['number'])
+ st.metric("تاريخ التقديم", pricing['submission_date'].strftime("%Y-%m-%d"))
- # إنشاء DataFrame للرسم البياني
- direct_cost_df = pd.DataFrame({
- 'الفئة الفرعية': list(direct_cost_subcategories.keys()),
- 'المبلغ': list(direct_cost_subcategories.values())
- })
+ with col3:
+ st.metric("طريقة التسعير", pricing['method'])
+ st.metric("الموقع", pricing['location'])
+
+ # عرض البنود والتسعير
+ st.markdown("### بنود التسعير")
+
+ items = pricing['items'].copy()
+
+ # إضافة أسعار الوحدة للمحاكاة
+ if 'سعر الوحدة' in items.columns and (items['سعر الوحدة'] == 0).all():
+ items['سعر الوحدة'] = [
+ round(random.uniform(1000, 3000), 2), # الخرسانة
+ round(random.uniform(5000, 7000), 2), # الحديد
+ round(random.uniform(100, 200), 2), # العزل
+ round(random.uniform(50, 100), 2), # الردم
+ round(random.uniform(1200, 3500), 2), # الخرسانة للأعمدة
+ ]
+
+ if len(items) > 5:
+ for i in range(5, len(items)):
+ items.at[i, 'سعر الوحدة'] = round(random.uniform(500, 5000), 2)
- # إنشاء رسم بياني شريطي
- fig = px.bar(
- direct_cost_df,
- x='الفئة الفرعية',
- y='المبلغ',
- title='توزيع التكاليف المباشرة',
- color='الفئة الفرعية',
- text_auto='.2s'
- )
+ # حساب الإجمالي
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
- st.plotly_chart(fig, use_container_width=True)
+ # عرض البنود
+ st.dataframe(items, use_container_width=True, hide_index=True)
- # عرض مقارنة سيناريوهات التسعير
- st.markdown("### مقارنة سيناريوهات التسعير")
- # إنشاء DataFrame للرسم البياني
- scenarios_df = pd.DataFrame({
- 'السيناريو': [item['name'] for item in st.session_state.price_scenarios],
- 'التكلفة الإجمالية': [item['total_cost'] for item in st.session_state.price_scenarios],
- 'هامش الربح (%)': [item['profit_margin'] for item in st.session_state.price_scenarios],
- 'السعر الإجمالي': [item['total_price'] for item in st.session_state.price_scenarios]
- })
+ # ✅ التوصية الذكية باستخدام OpenAI
+ with st.expander("🔍 توليد توصية ذكية باستخدام AI"):
+ if styled_button("توليد توصية ذكية باستخدام AI", key="gen_ai_recommendation_btn", type="secondary", icon="🤖", full_width=True):
+ import openai
+ import os
+
+ # تهيئة عميل OpenAI - استخدام واجهة الإصدار الجديد فقط (1.0.0 وما فوق)
+ api_key = os.environ.get("ai")
+ client = openai.OpenAI(api_key=api_key)
+
+ items_df = items.copy()
+ prompt = f"""قم بتحليل الجدول التالي للبنود في مشروع إنشاء، وقدم توصية ذكية لتحسين التسعير وضمان التوازن المالي. الجدول يحتوي على البنود، الكميات، الأسعار، والإجماليات:\n\n{items_df.to_string(index=False)}\n\nالتوصية:\n"""
+
+ try:
+ with st.spinner("جاري توليد التوصية..."):
+ # استخدام واجهة OpenAI الجديدة
+ response = client.chat.completions.create(
+ model="gpt-4o", # استخدام أحدث نموذج من OpenAI GPT-4o
+ messages=[
+ {"role": "system", "content": "أنت خبير في تسعير مشاريع البناء والبنية التحتية."},
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.4,
+ max_tokens=500
+ )
+
+ # استخراج محتوى الرسالة من واجهة OpenAI الجديدة
+ recommendation = response.choices[0].message.content
+
+ st.success("تم توليد التوصية بنجاح!")
+ st.markdown("#### التوصية الذكية:")
+ st.info(recommendation)
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء الاتصال بنموذج OpenAI: {e}")
+ st.info("يجب التأكد من تثبيت أحدث إصدار من مكتبة OpenAI: `pip install openai --upgrade`")
+
+ # واجهة تعديل أسعار الوحدات
+ st.markdown("### تعديل أسعار الوحدات")
+
+ # تقسيم البنود إلى مجموعتين للعرض
+ col1, col2 = st.columns(2)
+ half = len(items) // 2 + len(items) % 2
- # إنشاء رسم بياني شريطي مزدوج
- fig = go.Figure()
-
- # إضافة شريط للتكلفة الإجمالية
- fig.add_trace(go.Bar(
- x=scenarios_df['السيناريو'],
- y=scenarios_df['التكلفة الإجمالية'],
- name='التكلفة الإجمالية',
- marker_color='indianred'
- ))
-
- # إضافة شريط للسعر الإجمالي
- fig.add_trace(go.Bar(
- x=scenarios_df['السيناريو'],
- y=scenarios_df['السعر الإجمالي'],
- name='السعر الإجمالي',
- marker_color='lightsalmon'
- ))
-
- # إضافة خط لهامش الربح
- fig.add_trace(go.Scatter(
- x=scenarios_df['السيناريو'],
- y=scenarios_df['هامش الربح (%)'] * 10000, # تكبير القيم لتظهر على الرسم البياني
- name='هامش الربح (%)',
- yaxis='y2',
- line=dict(color='royalblue', width=4)
- ))
-
- # تعديل تخطيط الرسم البياني
- fig.update_layout(
- title='مقارنة سيناريوهات التسعير',
- xaxis_title='السيناريو',
- yaxis_title='المبلغ (ريال)',
- yaxis2=dict(
- title='هامش الربح (%)',
- titlefont=dict(color='royalblue'),
- tickfont=dict(color='royalblue'),
- overlaying='y',
- side='right',
- range=[0, 20]
- ),
- barmode='group',
- legend=dict(
- x=0,
- y=1.2,
- orientation='h'
- )
- )
+ with col1:
+ for idx in range(half):
+ if idx < len(items):
+ row = items.iloc[idx]
+ price = st.number_input(
+ f"{row['رقم البند']}: {row['وصف البند'][:30]}... ({row['الوحدة']})",
+ value=float(row['سعر الوحدة']),
+ min_value=0.0,
+ key=f"price1_{idx}"
+ )
+ items.at[idx, 'سعر الوحدة'] = price
+ items.at[idx, 'الإجمالي'] = price * items.at[idx, 'الكمية']
- # تعديل النص على الأشرطة
- fig.update_traces(
- texttemplate='%{y:,.0f}',
- textposition='outside'
- )
+ with col2:
+ for idx in range(half, len(items)):
+ row = items.iloc[idx]
+ price = st.number_input(
+ f"{row['رقم البند']}: {row['وصف البند'][:30]}... ({row['الوحدة']})",
+ value=float(row['سعر الوحدة']),
+ min_value=0.0,
+ key=f"price2_{idx}"
+ )
+ items.at[idx, 'سعر الوحدة'] = price
+ items.at[idx, 'الإجمالي'] = price * items.at[idx, 'الكمية']
- st.plotly_chart(fig, use_container_width=True)
+ # حساب وعرض إجماليات التسعير
+ total_price = items['الإجمالي'].sum()
- # عرض مؤشرات الأداء الرئيسية
- st.markdown("### مؤشرات الأداء الرئيسية")
+ st.markdown("### إجماليات التسعير")
col1, col2, col3 = st.columns(3)
with col1:
- # حساب نسبة التكاليف المباشرة من إجمالي التكاليف
- direct_cost_percentage = (total_direct_cost / total_cost) * 100
- st.metric("نسبة التكاليف المباشرة", f"{direct_cost_percentage:.1f}%")
+ st.metric("إجمالي التكاليف المباشرة", f"{total_price:,.2f} ريال")
with col2:
- # حساب نسبة التكاليف غير المباشرة من إجمالي التكاليف
- indirect_cost_percentage = (total_indirect_cost / total_cost) * 100
- st.metric("نسبة التكاليف غير المباشرة", f"{indirect_cost_percentage:.1f}%")
+ overhead_percentage = st.slider("نسبة المصاريف العامة والأرباح (%)", 5, 30, 15)
+ overhead_value = total_price * overhead_percentage / 100
+ st.metric("المصاريف العامة والأرباح", f"{overhead_value:,.2f} ريال")
with col3:
- # حساب نسبة هامش الربح من السعر الإجمالي
- profit_margin = (total_profit / total_price) * 100
- st.metric("هامش الربح", f"{profit_margin:.1f}%")
-
- def _render_bill_of_quantities_tab(self):
- """عرض تبويب جدول الكميات"""
+ grand_total = total_price + overhead_value
+ st.metric("الإجمالي النهائي", f"{grand_total:,.2f} ريال")
- st.markdown("### جدول الكميات")
-
- # عرض جدول الكميات الحالي
- st.markdown("#### قائمة البنود")
+ # رسم بياني لتوزيع التكاليف
+ st.markdown("### تحليل التكاليف")
- # تحويل قائمة البنود إلى DataFrame
- boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
+ # حساب النسب المئوية لكل بند
+ pie_data = items.copy()
+ pie_data['نسبة من إجمالي التكاليف'] = pie_data['الإجمالي'] / total_price * 100
- # عرض البنود كجدول قابل للتعديل
- edited_df = st.data_editor(
- boq_df,
- column_config={
- "id": st.column_config.NumberColumn("الرقم", disabled=True),
- "code": st.column_config.TextColumn("الكود"),
- "description": st.column_config.TextColumn("الوصف"),
- "unit": st.column_config.SelectboxColumn(
- "الوحدة",
- options=["م3", "م2", "طن", "كجم", "عدد", "لتر", "متر"]
- ),
- "quantity": st.column_config.NumberColumn("الكمية", min_value=0),
- "unit_price": st.column_config.NumberColumn("سعر الوحدة (ريال)", min_value=0, format="%.2f"),
- "total_price": st.column_config.NumberColumn("السعر الإجمالي (ريال)", min_value=0, format="%.2f", disabled=True),
- "category": st.column_config.SelectboxColumn(
- "الفئة",
- options=["أعمال ترابية", "أعمال خرسانية", "أعمال حديد", "أعمال بناء", "أعمال تشطيب", "أعمال كهربائية", "أعمال ميكانيكية", "أعمال صحية", "أخرى"]
- )
- },
- use_container_width=True,
- hide_index=True,
- num_rows="dynamic"
+ fig = px.pie(
+ pie_data,
+ values='نسبة من إجمالي التكاليف',
+ names='وصف البند',
+ title='توزيع التكاليف حسب البنود',
+ hole=0.4
)
- # تحديث السعر الإجمالي لكل بند
- for i, row in edited_df.iterrows():
- edited_df.at[i, 'total_price'] = row['quantity'] * row['unit_price']
-
- # تحديث قائمة البنود
- if not edited_df.equals(boq_df):
- st.session_state.bill_of_quantities = edited_df.to_dict('records')
- st.success("تم تحديث جدول الكميات بنجاح!")
+ st.plotly_chart(fig, use_container_width=True)
- # عرض إجمالي جدول الكميات
- total_boq = sum(item['total_price'] for item in st.session_state.bill_of_quantities)
- st.metric("إجمالي جدول الكميات", f"{total_boq:,.2f} ريال")
+ # أزرار العمليات
+ col1, col2, col3 = st.columns(3)
- # إضافة بند جديد
- st.markdown("#### إضافة بند جديد")
+ with col1:
+ if styled_button("حفظ التسعير", key="save_comprehensive_pricing_button", type="primary", icon="💾"):
+ # تحديث بيانات التسعير الحالي
+ st.session_state.current_pricing['items'] = items.copy()
+ st.success("تم حفظ التسعير بنجاح!")
- with st.form(key="add_boq_item_form"):
- col1, col2 = st.columns(2)
-
- with col1:
- new_code = st.text_input("الكود", key="new_boq_code")
- new_description = st.text_area("الوصف", key="new_boq_description")
- new_category = st.selectbox(
- "الفئة",
- ["أعمال ترابية", "أعمال خرسانية", "أعمال حديد", "أعمال بناء", "أعمال تشطيب", "أعمال كهربائية", "أعمال ميكانيكية", "أعمال صحية", "أخرى"],
- key="new_boq_category"
- )
+ with col2:
+ if styled_button("تصدير إلى Excel", key="export_to_excel_button", type="info", icon="📊"):
+ try:
+ # إنشاء دليل الصادرات إذا لم يكن موجودًا
+ export_dir = "exports"
+ create_directory_if_not_exists(export_dir)
+
+ # تصدير البيانات إلى ملف Excel
+ file_path = os.path.join(export_dir, f"تسعير_{tender_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx")
+ export_to_excel(items, file_path, tender_name)
+
+ st.success(f"تم تصدير التسعير إلى Excel بنجاح! مسار الملف: {file_path}")
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء تصدير البيانات: {str(e)}")
- with col2:
- new_unit = st.selectbox(
- "الوحدة",
- ["م3", "م2", "طن", "كجم", "عدد", "لتر", "متر"],
- key="new_boq_unit"
- )
- new_quantity = st.number_input("الكمية", min_value=0.0, key="new_boq_quantity")
- new_unit_price = st.number_input("سعر الوحدة (ريال)", min_value=0.0, key="new_boq_unit_price")
-
- submit_button = st.form_submit_button("إضافة بند")
-
- if submit_button:
- if new_code and new_description:
- # إنشاء معرف جديد
- new_id = max([item['id'] for item in st.session_state.bill_of_quantities], default=0) + 1
-
- # حساب السعر الإجمالي
- new_total_price = new_quantity * new_unit_price
-
- # إضافة البند الجديد
- st.session_state.bill_of_quantities.append({
- 'id': new_id,
- 'code': new_code,
- 'description': new_description,
- 'unit': new_unit,
- 'quantity': new_quantity,
- 'unit_price': new_unit_price,
- 'total_price': new_total_price,
- 'category': new_category
- })
-
- st.success(f"تمت إضافة البند '{new_code}' بنجاح!")
- st.rerun()
- else:
- st.error("يرجى إدخال الكود والوصف.")
+ if styled_button("تصدير إلى PDF", key="export_to_pdf_button", type="info", icon="📄"):
+ try:
+ # إنشاء دليل الصادرات إذا لم يكن موجودًا
+ export_dir = "exports"
+ create_directory_if_not_exists(export_dir)
+
+ # تصدير البيانات إلى ملف PDF
+ file_path = os.path.join(export_dir, f"تسعير_{tender_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf")
+
+ # التحقق من وجود بيانات تحليل الأسعار
+ if 'items_price_analysis' in st.session_state and st.session_state.items_price_analysis:
+ # استخراج المعلومات الأساسية للمشروع
+ project_info = {
+ 'اسم_المشروع': tender_name,
+ 'وصف_المشروع': f"مناقصة رقم: {tender_number} - الجهة المالكة: {client} - الموقع: {location}"
+ }
+
+ # تصدير البيانات مع تحليل الأسعار
+ export_pricing_with_analysis_to_pdf(
+ items,
+ st.session_state.items_price_analysis,
+ file_path,
+ title=f"تسعير مناقصة: {tender_name}",
+ project_info=project_info
+ )
+ else:
+ # تصدير البيانات بدون تحليل الأسعار
+ export_pricing_to_pdf(
+ items,
+ file_path,
+ title=f"تسعير مناقصة: {tender_name}",
+ description=f"مناقصة رقم: {tender_number} - الجهة المالكة: {client} - الموقع: {location}"
+ )
+
+ st.success(f"تم تصدير التسعير إلى PDF بنجاح! مسار الملف: {file_path}")
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء تصدير البيانات: {str(e)}")
+
+ with col3:
+ if styled_button("تحليل المخاطر المالية", key="financial_risk_analysis_button", type="warning", icon="⚠️"):
+ st.success("تم إرسال الطلب إلى وحدة تحليل المخاطر!")
+
+ def _render_unbalanced_pricing_tab(self):
+ """عرض تبويب التسعير غير المتزن"""
- # تحليل جدول الكميات
- st.markdown("#### تحليل جدول الكميات")
+ st.markdown("### التسعير غير المتزن")
- col1, col2 = st.columns(2)
+ # التحقق من وجود تسعير حالي
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
+ return
- with col1:
- # توزيع البنود حسب الفئة
- category_totals = {}
+ # شرح التسعير غير المتزن
+ with st.expander("ما هو التسعير غير المتزن؟", expanded=False):
+ st.markdown("""
+ **التسعير غير المتزن** هو استراتيجية تسعير تقوم على توزيع التكاليف بين بنود المناقصة بشكل غير متساوٍ، مع الحفاظ على إجمالي قيمة العطاء.
- for item in st.session_state.bill_of_quantities:
- category = item['category']
- if category in category_totals:
- category_totals[category] += item['total_price']
- else:
- category_totals[category] = item['total_price']
+ ### استراتيجيات التسعير غير المتزن:
- category_df = pd.DataFrame({
- 'الفئة': list(category_totals.keys()),
- 'المبلغ': list(category_totals.values())
- })
+ 1. **التحميل الأمامي (Front Loading)**: زيادة أسعار البنود المبكرة في المشروع للحصول على تدفق نقدي أفضل في بداية المشروع.
+ 2. **التحميل الخلفي (Back Loading)**: زيادة أسعار البنود المتأخرة في المشروع.
+ 3. **تحميل البنود المؤكدة**: زيادة أسعار البنود التي من المؤكد تنفيذها بالكميات المحددة.
+ 4. **تخفيض أسعار البنود المحتملة**: تخفيض أسعار البنود التي قد تزيد كمياتها أثناء التنفيذ.
- fig = px.pie(
- category_df,
- values='المبلغ',
- names='الفئة',
- title='توزيع جدول الكميات حسب الفئة'
- )
+ ### مزايا التسعير غير المتزن:
- st.plotly_chart(fig, use_container_width=True)
+ - تحسين التدفق النقدي للمشروع.
+ - تعظيم الربحية في حالة التغييرات والأوامر التغييرية.
+ - زيادة فرص الفوز بالمناقصة.
+
+ ### مخاطر التسعير غير المتزن:
+
+ - قد يتم رفض العطاء إذا كان عدم التوازن واضحاً.
+ - قد تتأثر السمعة سلباً إذا تم استخدامه بشكل مفرط.
+ - قد يؤدي إلى خسائر إذا لم يتم تنفيذ البنود ذات الأسعار العالية.
+ """)
+
+ # عرض بنود التسعير الحالي
+ items = st.session_state.current_pricing['items'].copy()
+
+ # إضافة عمود إستراتيجية التسعير
+ if 'إستراتيجية التسعير' not in items.columns:
+ items['إستراتيجية التسعير'] = 'متوازن'
+
+ st.markdown("### إستراتيجية التسعير غير المتزن")
+
+ # اختيار الإستراتيجية
+ strategy = st.selectbox(
+ "اختر إستراتيجية التسعير",
+ [
+ "تحميل أمامي (Front Loading)",
+ "تحميل البنود المؤكدة",
+ "تخفيض البنود المحتمل زيادتها",
+ "إستراتيجية مخصصة"
+ ],
+ key="pricing_strategy_selector"
+ )
- with col2:
- # ترتيب البنود حسب القيمة
- top_items = sorted(st.session_state.bill_of_quantities, key=lambda x: x['total_price'], reverse=True)[:5]
+ # تطبيق الإستراتيجية المختارة
+ if strategy == "تحميل أمامي (Front Loading)":
+ # محاكاة تحميل أمامي
+ items_count = len(items)
+ early_items = items.iloc[:items_count//3].index
+ middle_items = items.iloc[items_count//3:2*items_count//3].index
+ late_items = items.iloc[2*items_count//3:].index
- top_items_df = pd.DataFrame({
- 'البند': [item['code'] + ' - ' + item['description'][:20] + '...' for item in top_items],
- 'القيمة': [item['total_price'] for item in top_items]
- })
+ # تطبيق الزيادة والنقصان
+ for idx in early_items:
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.3 # زيادة 30%
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
- fig = px.bar(
- top_items_df,
- x='البند',
- y='القيمة',
- title='أعلى 5 بنود من حيث القيمة',
- color='القيمة',
- text_auto='.2s'
- )
+ for idx in middle_items:
+ items.at[idx, 'إستراتيجية التسعير'] = 'متوازن'
- st.plotly_chart(fig, use_container_width=True)
+ for idx in late_items:
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30%
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
+
+ elif strategy == "تحميل البنود المؤكدة":
+ # محاكاة - اعتبار بعض البنود مؤكدة
+ confirmed_items = [0, 2, 4] # الأصفار-مستندة
+ variable_items = [idx for idx in range(len(items)) if idx not in confirmed_items]
+
+ # تطبيق الزيادة والنقصان
+ for idx in confirmed_items:
+ if idx < len(items):
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.25 # زيادة 25%
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
+
+ for idx in variable_items:
+ if idx < len(items):
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.85 # نقص 15%
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
+
+ elif strategy == "تخفيض البنود المحتمل زيادتها":
+ # محاكاة - اعتبار بعض البنود محتمل زيادتها
+ variable_items = [1, 3] # الأصفار-مستندة
+ other_items = [idx for idx in range(len(items)) if idx not in variable_items]
+
+ # تطبيق الزيادة والنقصان
+ for idx in variable_items:
+ if idx < len(items):
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30%
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
+
+ for idx in other_items:
+ if idx < len(items):
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.15 # زيادة 15%
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
+
+ else: # إستراتيجية مخصصة
+ st.markdown("### تعديل أسعار البنود يدوياً")
+ st.markdown("قم بتعديل أسعار البنود وإستراتيجية التسعير يدوياً من خلال النموذج أدناه.")
+
+ # إضافة واجهة لتعديل الأسعار يدوياً
+ if 'edit_items' not in st.session_state:
+ st.session_state.edit_items = items.copy()
+
+ # عرض نموذج التعديل لكل بند
+ for index, row in items.iterrows():
+ with st.container():
+ col1, col2, col3, col4 = st.columns([3, 1, 1, 1])
+
+ with col1:
+ st.markdown(f"**البند:** {row['وصف البند']}")
+
+ with col2:
+ original_price = st.session_state.current_pricing['items'].iloc[index]['سعر الوحدة']
+ new_price = st.number_input(
+ "سعر الوحدة الجديد",
+ min_value=0.01,
+ value=float(row['سعر الوحدة']),
+ key=f"price_{index}"
+ )
+ items.at[index, 'سعر الوحدة'] = new_price
+
+ with col3:
+ percent_change = ((new_price - original_price) / original_price) * 100
+ st.metric(
+ "نسبة التغيير",
+ f"{percent_change:.1f}%",
+ delta=f"{new_price - original_price:.2f}"
+ )
+
+ with col4:
+ strategy_options = ['متوازن', 'زيادة', 'نقص']
+ current_strategy = row['إستراتيجية التسعير']
+ strategy_index = strategy_options.index(current_strategy) if current_strategy in strategy_options else 0
+
+ new_strategy = st.selectbox(
+ "الإستراتيجية",
+ options=strategy_options,
+ index=strategy_index,
+ key=f"strategy_{index}"
+ )
+ items.at[index, 'إستراتيجية التسعير'] = new_strategy
+
+ st.markdown("---")
+
+ # حساب الإجمالي بعد التعديل
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
+
+ # تعيين ألوان للإستراتيجيات وتنسيق الجدول بشكل متقدم
+ def highlight_row(row):
+ strategy = row['إستراتيجية التسعير']
+ styles = [''] * len(row)
+
+ # تطبيق لون خلفية لكل صف حسب الإستراتيجية
+ if strategy == 'زيادة':
+ background = 'linear-gradient(90deg, rgba(168, 230, 207, 0.3), rgba(168, 230, 207, 0.1))'
+ text_color = '#1F7A8C'
+ elif strategy == 'نقص':
+ background = 'linear-gradient(90deg, rgba(255, 154, 162, 0.3), rgba(255, 154, 162, 0.1))'
+ text_color = '#9D2A45'
+ else:
+ background = 'linear-gradient(90deg, rgba(220, 237, 255, 0.3), rgba(220, 237, 255, 0.1))'
+ text_color = '#555555'
+
+ # تطبيق النمط على جميع الخلايا في الصف
+ for i in range(len(styles)):
+ styles[i] = f'background: {background}; color: {text_color}; border-bottom: 1px solid #ddd;'
+
+ # تطبيق نمط خاص على خلية الإستراتيجية
+ if strategy == 'زيادة':
+ styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #a8e6cf; color: #007263; font-weight: bold; border-radius: 5px; text-align: center;'
+ elif strategy == 'نقص':
+ styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #ff9aa2; color: #9D2A45; font-weight: bold; border-radius: 5px; text-align: center;'
+ else:
+ styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #dceeff; color: #555555; font-weight: bold; border-radius: 5px; text-align: center;'
+
+ # تنسيق عمود السعر
+ price_idx = list(row.index).index('سعر الوحدة')
+ styles[price_idx] = styles[price_idx] + 'font-weight: bold;'
+
+ # تنسيق عمود الإجمالي
+ total_idx = list(row.index).index('الإجمالي')
+ styles[total_idx] = styles[total_idx] + 'font-weight: bold;'
+
+ return styles
- # استيراد وتصدير جدول الكميات
- st.markdown("#### استيراد وتصدير جدول الكميات")
+ # عرض الجدول مع تنسيق متقدم
+ st.subheader("بنود التسعير غير المتزن")
- col1, col2 = st.columns(2)
-
- with col1:
- if st.button("تصدير جدول الكميات إلى Excel", key="export_boq_button"):
- # محاكاة تصدير البيانات
- st.success("تم تصدير جدول الكميات إلى Excel بنجاح!")
-
- with col2:
- uploaded_file = st.file_uploader("استيراد جدول الكميات من Excel", type=["xlsx"], key="import_boq_file")
-
- if uploaded_file is not None:
- if st.button("استيراد البيانات", key="import_boq_button"):
- # محاكاة استيراد البيانات
- st.success("تم استيراد جدول الكميات بنجاح!")
-
- def _render_cost_analysis_tab(self):
- """عرض تبويب تحليل التكاليف"""
+ # تطبيق التنسيق على الجدول
+ styled_items = items.style.apply(highlight_row, axis=1)
- st.markdown("### تحليل التكاليف")
-
- # عرض تحليل التكاليف الحالي
- st.markdown("#### قائمة التكاليف")
-
- # تحويل قائمة التكاليف إلى DataFrame
- cost_df = pd.DataFrame(st.session_state.cost_analysis)
-
- # عرض التكاليف كجدول قابل للتعديل
- edited_df = st.data_editor(
- cost_df,
- column_config={
- "id": st.column_config.NumberColumn("الرقم", disabled=True),
- "category": st.column_config.SelectboxColumn(
- "الفئة",
- options=["تكاليف مباشرة", "تكاليف غير مباشرة", "أرباح"]
- ),
- "subcategory": st.column_config.TextColumn("الفئة الفرعية"),
- "description": st.column_config.TextColumn("الوصف"),
- "amount": st.column_config.NumberColumn("المبلغ (ريال)", min_value=0, format="%.2f"),
- "percentage": st.column_config.NumberColumn("النسبة (%)", min_value=0, format="%.1f", disabled=True)
- },
- use_container_width=True,
- hide_index=True,
- num_rows="dynamic"
- )
-
- # حساب إجمالي التكاليف
- total_amount = sum(item['amount'] for item in st.session_state.cost_analysis)
+ # تنسيق تنسيق الأرقام
+ styled_items = styled_items.format({
+ 'الكمية': '{:,.2f}',
+ 'سعر الوحدة': '{:,.2f}',
+ 'الإجمالي': '{:,.2f}'
+ })
- # تحديث النسبة المئوية لكل بند
- for i, row in edited_df.iterrows():
- edited_df.at[i, 'percentage'] = (row['amount'] / total_amount) * 100
+ st.dataframe(styled_items, use_container_width=True, height=None)
+
+ # المقارنة بين التسعير المتوازن وغير المتوازن
+ st.subheader("مقارنة التسعير المتوازن وغير المتوازن")
+
+ original_items = st.session_state.current_pricing['items'].copy()
+ original_total = original_items['الإجمالي'].sum()
+ unbalanced_total = items['الإجمالي'].sum()
+
+ # عرض بطاقات المقارنة بتصميم متقدم
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
- # تحديث قائمة التكاليف
- if not edited_df.equals(cost_df):
- st.session_state.cost_analysis = edited_df.to_dict('records')
- st.success("تم تحديث تحليل التكاليف بنجاح!")
+ col1, col2, col3 = st.columns(3)
- # عرض إجمالي التكاليف
- st.metric("إجمالي التكاليف", f"{total_amount:,.2f} ريال")
+ with col1:
+ st.markdown("""
+
+
إجمالي التسعير المتوازن
+
{:,.2f} ريال
+
التسعير الأصلي
+
+ """.format(original_total), unsafe_allow_html=True)
- # إضافة تكلفة جديدة
- st.markdown("#### إضافة تكلفة جديدة")
+ with col2:
+ st.markdown("""
+
+
إجمالي التسعير غير المتوازن
+
{:,.2f} ريال
+
بعد إعادة توزيع الأسعار
+
+ """.format(
+ unbalanced_total,
+ "positive-delta" if unbalanced_total > original_total else "negative-delta" if unbalanced_total < original_total else "neutral-delta"
+ ), unsafe_allow_html=True)
- with st.form(key="add_cost_item_form"):
- col1, col2 = st.columns(2)
-
- with col1:
- new_category = st.selectbox(
- "الفئة",
- ["تكاليف مباشرة", "تكاليف غير مباشرة", "أرباح"],
- key="new_cost_category"
- )
- new_subcategory = st.text_input("الفئة الفرعية", key="new_cost_subcategory")
+ with col3:
+ diff = unbalanced_total - original_total
+ delta_percent = diff/original_total*100 if original_total > 0 else 0
- with col2:
- new_description = st.text_input("الوصف", key="new_cost_description")
- new_amount = st.number_input("المبلغ (ريال)", min_value=0.0, key="new_cost_amount")
-
- submit_button = st.form_submit_button("إضافة تكلفة")
-
- if submit_button:
- if new_description and new_subcategory:
- # إنشاء معرف جديد
- new_id = max([item['id'] for item in st.session_state.cost_analysis], default=0) + 1
-
- # حساب النسبة المئوية
- new_percentage = (new_amount / (total_amount + new_amount)) * 100
-
- # إضافة التكلفة الجديدة
- st.session_state.cost_analysis.append({
- 'id': new_id,
- 'category': new_category,
- 'subcategory': new_subcategory,
- 'description': new_description,
- 'amount': new_amount,
- 'percentage': new_percentage
- })
-
- # إعادة حساب النسب المئوية لجميع البنود
- new_total = total_amount + new_amount
- for item in st.session_state.cost_analysis:
- item['percentage'] = (item['amount'] / new_total) * 100
+ st.markdown("""
+
+
الفرق بين التسعيرين
+
{:,.2f} ريال
+
نسبة الفرق: {:+.1f}%
+
+ """.format(
+ diff,
+ "positive-delta" if diff > 0 else "negative-delta" if diff < 0 else "neutral-delta",
+ delta_percent
+ ), unsafe_allow_html=True)
+
+ # المعايرة للحفاظ على إجمالي التسعير
+ if abs(diff) > 1: # إذا كان هناك فرق كبير
+ if styled_button("معايرة الأسعار للحفاظ على إجمالي التسعير", key="calibrate_prices_button", type="primary", icon="⚖️", full_width=True):
+ # تعديل الأسعار للحفاظ على إجمالي التكلفة
+ adjustment_factor = original_total / unbalanced_total
+ items['سعر الوحدة'] = items['سعر الوحدة'] * adjustment_factor
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
- st.success(f"تمت إضافة التكلفة '{new_description}' بنجاح!")
- st.rerun()
- else:
- st.error("يرجى إدخال الفئة الفرعية والوصف.")
+ st.success(f"تم تعديل الأسعار للحفاظ على إجمالي التسعير الأصلي ({original_total:,.2f} ريال)")
+ st.dataframe(items, use_container_width=True)
- # تحليل التكاليف
- st.markdown("#### تحليل التكاليف")
-
- # تحليل التكاليف حسب الفئة
- st.markdown("##### توزيع التكاليف حسب الفئة")
-
- # تجميع البيانات حسب الفئة
- category_totals = {}
-
- for item in st.session_state.cost_analysis:
- category = item['category']
- if category in category_totals:
- category_totals[category] += item['amount']
- else:
- category_totals[category] = item['amount']
+ # رسم بياني للمقارنة
+ st.subheader("تحليل بصري للتسعير غير المتوازن")
- category_df = pd.DataFrame({
- 'الفئة': list(category_totals.keys()),
- 'المبلغ': list(category_totals.values())
+ # إعداد البيانات للرسم البياني
+ chart_data = pd.DataFrame({
+ 'وصف البند': original_items['وصف البند'],
+ 'التسعير المتوازن': original_items['الإجمالي'],
+ 'التسعير غير المتوازن': items['الإجمالي']
})
- fig = px.pie(
- category_df,
- values='المبلغ',
- names='الفئة',
- title='توزيع التكاليف حسب الفئة',
- color_discrete_sequence=px.colors.qualitative.Set3
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل التكاليف المباشرة
- st.markdown("##### تحليل التكاليف المباشرة")
-
- col1, col2 = st.columns(2)
-
- with col1:
- # تجميع البيانات حسب الفئة الفرعية للتكاليف المباشرة
- direct_subcategory_totals = {}
-
- for item in st.session_state.cost_analysis:
- if item['category'] == 'تكاليف مباشرة':
- subcategory = item['subcategory']
- if subcategory in direct_subcategory_totals:
- direct_subcategory_totals[subcategory] += item['amount']
- else:
- direct_subcategory_totals[subcategory] = item['amount']
+ # إضافة عمود للنسبة المئوية للتغيير
+ chart_data['نسبة التغيير'] = (chart_data['التسعير غير المتوازن'] - chart_data['التسعير المتوازن']) / chart_data['التسعير المتوازن'] * 100
+
+ # تحديد لون الأعمدة بناءً على نسبة التغيير
+ bar_colors = []
+ for change in chart_data['نسبة التغيير']:
+ if change > 5: # زيادة كبيرة
+ bar_colors.append('#1F7A8C') # أزرق مخضر
+ elif change > 0: # زيادة صغيرة
+ bar_colors.append('#81B29A') # أخضر فاتح
+ elif change > -5: # نقص صغير
+ bar_colors.append('#F2CC8F') # أصفر
+ else: # نقص كبير
+ bar_colors.append('#E07A5F') # أحمر
+
+ # التبويب بين مخططات مختلفة للمقارنة
+ chart_tabs = st.tabs(["مخطط شريطي", "مخطط مقارنة", "مخطط نسبة التغيير"])
+
+ with chart_tabs[0]: # رسم بياني شريطي
+ # رسم بياني شريطي للمقارنة
+ fig = go.Figure()
- direct_subcategory_df = pd.DataFrame({
- 'الفئة الفرعية': list(direct_subcategory_totals.keys()),
- 'المبلغ': list(direct_subcategory_totals.values())
- })
+ fig.add_trace(go.Bar(
+ x=chart_data['وصف البند'],
+ y=chart_data['التسعير المتوازن'],
+ name='التسعير المتوازن',
+ marker_color='rgba(55, 83, 109, 0.7)'
+ ))
+
+ fig.add_trace(go.Bar(
+ x=chart_data['وصف البند'],
+ y=chart_data['التسعير غير المتوازن'],
+ name='التسعير غير المتوازن',
+ marker_color=bar_colors
+ ))
- fig = px.pie(
- direct_subcategory_df,
- values='المبلغ',
- names='الفئة الفرعية',
- title='توزيع التكاليف المباشرة حسب الفئة الفرعية'
+ fig.update_layout(
+ title='مقارنة بين التسعير المتوازن وغير المتوازن',
+ xaxis_tickfont_size=14,
+ yaxis=dict(
+ title='الإجمالي (ريال)',
+ titlefont_size=16,
+ tickfont_size=14,
+ ),
+ legend=dict(
+ x=0.01,
+ y=0.99,
+ bgcolor='rgba(255, 255, 255, 0.8)',
+ bordercolor='rgba(0, 0, 0, 0.1)',
+ borderwidth=1
+ ),
+ barmode='group',
+ bargap=0.15,
+ bargroupgap=0.1,
+ plot_bgcolor='rgba(240, 249, 255, 0.5)',
+ margin=dict(t=50, b=50, l=20, r=20)
)
st.plotly_chart(fig, use_container_width=True)
- with col2:
- # تجميع البيانات حسب الوصف للتكاليف المباشرة
- direct_description_totals = {}
-
- for item in st.session_state.cost_analysis:
- if item['category'] == 'تكاليف مباشرة':
- description = item['description']
- if description in direct_description_totals:
- direct_description_totals[description] += item['amount']
- else:
- direct_description_totals[description] = item['amount']
+ with chart_tabs[1]: # رسم مقارنة
+ # رسم مقارنة بين التسعيرين
+ fig = go.Figure()
- direct_description_df = pd.DataFrame({
- 'الوصف': list(direct_description_totals.keys()),
- 'المبلغ': list(direct_description_totals.values())
- })
+ # إضافة خط للتسعير المتوازن
+ fig.add_trace(go.Scatter(
+ x=chart_data['وصف البند'],
+ y=chart_data['التسعير المتوازن'],
+ name='التسعير المتوازن',
+ mode='lines+markers',
+ line=dict(color='rgb(55, 83, 109)', width=3),
+ marker=dict(size=10, color='rgb(55, 83, 109)')
+ ))
- # ترتيب البيانات تنازلياً حسب المبلغ
- direct_description_df = direct_description_df.sort_values(by='المبلغ', ascending=False)
+ # إضافة نقاط للتسعير غير المتوازن
+ fig.add_trace(go.Scatter(
+ x=chart_data['وصف البند'],
+ y=chart_data['التسعير غير المتوازن'],
+ name='التسعير غير المتوازن',
+ mode='lines+markers',
+ line=dict(color='rgb(26, 118, 255)', width=3),
+ marker=dict(
+ size=12,
+ color=bar_colors,
+ line=dict(width=2, color='white')
+ )
+ ))
- fig = px.bar(
- direct_description_df,
- x='الوصف',
- y='المبلغ',
- title='توزيع التكاليف المباشرة حسب البند',
- color='المبلغ',
- text_auto='.2s'
+ # تحديثات التخطيط
+ fig.update_layout(
+ title='مقارنة مرئية بين استراتيجيات التسعير',
+ xaxis_tickfont_size=14,
+ yaxis=dict(
+ title='القيمة الإجمالية (ريال)',
+ titlefont_size=16,
+ tickfont_size=14,
+ gridcolor='rgba(200, 200, 200, 0.2)'
+ ),
+ legend=dict(
+ x=0.01,
+ y=0.99,
+ bgcolor='rgba(255, 255, 255, 0.8)',
+ bordercolor='rgba(0, 0, 0, 0.1)',
+ borderwidth=1
+ ),
+ plot_bgcolor='rgba(240, 249, 255, 0.5)',
+ margin=dict(t=50, b=50, l=20, r=20)
)
st.plotly_chart(fig, use_container_width=True)
- # تحليل التكاليف غير المباشرة
- st.markdown("##### تحليل التكاليف غير المباشرة")
-
- col1, col2 = st.columns(2)
-
- with col1:
- # تجميع البيانات حسب الفئة الفرعية للتكاليف غير المباشرة
- indirect_subcategory_totals = {}
-
- for item in st.session_state.cost_analysis:
- if item['category'] == 'تكاليف غير مباشرة':
- subcategory = item['subcategory']
- if subcategory in indirect_subcategory_totals:
- indirect_subcategory_totals[subcategory] += item['amount']
- else:
- indirect_subcategory_totals[subcategory] = item['amount']
+ with chart_tabs[2]: # مخطط نسبة التغيير
+ # مخطط للنسبة المئوية للتغيير
+ fig = go.Figure()
- indirect_subcategory_df = pd.DataFrame({
- 'الفئة الفرعية': list(indirect_subcategory_totals.keys()),
- 'المبلغ': list(indirect_subcategory_totals.values())
- })
+ # إضافة أعمدة لنسبة التغيير مع ألوان مختلفة حسب القيمة
+ fig.add_trace(go.Bar(
+ x=chart_data['وصف البند'],
+ y=chart_data['نسبة التغيير'],
+ name='نسبة التغيير',
+ marker_color=bar_colors,
+ text=[f"{val:.1f}%" for val in chart_data['نسبة التغيير']],
+ textposition='auto'
+ ))
+
+ # إضافة خط أفقي عند الصفر
+ fig.add_shape(
+ type="line",
+ x0=-0.5,
+ y0=0,
+ x1=len(chart_data['وصف البند'])-0.5,
+ y1=0,
+ line=dict(
+ color="black",
+ width=2,
+ dash="dash",
+ )
+ )
- fig = px.pie(
- indirect_subcategory_df,
- values='المبلغ',
- names='الفئة الفرعية',
- title='توزيع التكاليف غير المباشرة حسب الفئة الفرعية'
+ # تحديثات التخطيط
+ fig.update_layout(
+ title='نسبة التغيير في أسعار البنود (%)',
+ xaxis_tickfont_size=14,
+ yaxis=dict(
+ title='نسبة التغيير (%)',
+ titlefont_size=16,
+ tickfont_size=14,
+ gridcolor='rgba(200, 200, 200, 0.2)',
+ zeroline=True,
+ zerolinecolor='black',
+ zerolinewidth=2
+ ),
+ plot_bgcolor='rgba(240, 249, 255, 0.5)',
+ margin=dict(t=50, b=50, l=20, r=20)
)
st.plotly_chart(fig, use_container_width=True)
-
- with col2:
- # تجميع البيانات حسب الوصف للتكاليف غير المباشرة
- indirect_description_totals = {}
-
- for item in st.session_state.cost_analysis:
- if item['category'] == 'تكاليف غير مباشرة':
- description = item['description']
- if description in indirect_description_totals:
- indirect_description_totals[description] += item['amount']
- else:
- indirect_description_totals[description] = item['amount']
- indirect_description_df = pd.DataFrame({
- 'الوصف': list(indirect_description_totals.keys()),
- 'المبلغ': list(indirect_description_totals.values())
- })
+ # إضافة جدول مع نسب التغيير
+ st.markdown("#### جدول مفصل بنسب التغيير")
- # ترتيب البيانات تنازلياً حسب المبلغ
- indirect_description_df = indirect_description_df.sort_values(by='المبلغ', ascending=False)
+ # إعداد بيانات الجدول
+ table_data = chart_data[['وصف البند', 'التسعير المتوازن', 'التسعير غير المتوازن', 'نسبة التغيير']]
- fig = px.bar(
- indirect_description_df,
- x='الوصف',
- y='المبلغ',
- title='توزيع التكاليف غير المباشرة حسب البند',
- color='المبلغ',
- text_auto='.2s'
- )
+ # تنسيق الجدول
+ def highlight_change(row):
+ change = row['نسبة التغيير']
+ if change > 5:
+ return ['', '', '', 'background-color: rgba(31, 122, 140, 0.3); color: #1F7A8C; font-weight: bold;']
+ elif change > 0:
+ return ['', '', '', 'background-color: rgba(129, 178, 154, 0.3); color: #2A9D8F; font-weight: bold;']
+ elif change > -5:
+ return ['', '', '', 'background-color: rgba(242, 204, 143, 0.3); color: #BC6C25; font-weight: bold;']
+ else:
+ return ['', '', '', 'background-color: rgba(224, 122, 95, 0.3); color: #AE2012; font-weight: bold;']
- st.plotly_chart(fig, use_container_width=True)
+ # تطبيق التنسيق
+ styled_table = table_data.style.apply(highlight_change, axis=1).format({
+ 'التسعير المتوازن': '{:,.2f} ريال',
+ 'التسعير غير المتوازن': '{:,.2f} ريال',
+ 'نسبة التغيير': '{:+.1f}%'
+ })
+
+ st.dataframe(styled_table, use_container_width=True)
+
+ # قسم لإضافة أو حذف البنود
+ st.markdown("### إدارة بنود التسعير")
- # استيراد وتصدير تحليل التكاليف
- st.markdown("#### استيراد وتصدير تحليل التكاليف")
+ # إضافة بند جديد
+ with st.expander("إضافة بند جديد"):
+ col1, col2, col3, col4 = st.columns([3, 1, 1, 1])
+
+ with col1:
+ new_item_desc = st.text_input("وصف البند", placeholder="أدخل وصف البند الجديد")
+
+ with col2:
+ new_item_unit = st.selectbox(
+ "الوحدة",
+ options=["م3", "م2", "متر طولي", "طن", "قطعة", "كجم", "لتر"],
+ index=0,
+ key="construction_item_unit"
+ )
+
+ with col3:
+ new_item_qty = st.number_input("الكمية", min_value=0.1, value=1.0, format="%.2f")
+
+ with col4:
+ new_item_price = st.number_input("سعر الوحدة", min_value=0.1, value=100.0, format="%.2f")
+
+ if st.button("إضافة البند"):
+ if new_item_desc:
+ # إنشاء رقم البند الجديد
+ new_id = f"UB{len(items)+1}"
+
+ # إنشاء صف جديد
+ new_row = pd.DataFrame({
+ 'رقم البند': [new_id],
+ 'وصف البند': [new_item_desc],
+ 'الوحدة': [new_item_unit],
+ 'الكمية': [float(new_item_qty)],
+ 'سعر الوحدة': [float(new_item_price)],
+ 'الإجمالي': [float(new_item_qty * new_item_price)],
+ 'إستراتيجية التسعير': ['متوازن']
+ })
+
+ # إضافة الصف إلى DataFrame
+ items = pd.concat([items, new_row], ignore_index=True)
+
+ # تحديث الإجمالي
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
+
+ st.success(f"تم إضافة البند \"{new_item_desc}\" بنجاح!")
+ st.rerun()
+ else:
+ st.warning("يرجى إدخال وصف للبند")
+
+ # حذف بند
+ with st.expander("حذف بند"):
+ if len(items) > 0:
+ # قائمة بالبنود الحالية
+ item_options = [f"{row['رقم البند']} - {row['وصف البند']}" for idx, row in items.iterrows()]
+ selected_item_to_delete = st.selectbox(
+ "اختر البند المراد حذفه",
+ options=item_options,
+ key="item_to_delete_selector"
+ )
+
+ # استخراج رقم البند
+ item_id_to_delete = selected_item_to_delete.split(" - ")[0]
+
+ if st.button("حذف البند", key="delete_item_button_2"):
+ # البحث عن البند وحذفه
+ items = items[items['رقم البند'] != item_id_to_delete]
+ st.success(f"تم حذف البند {item_id_to_delete} بنجاح!")
+ st.rerun()
+ else:
+ st.info("لا توجد بنود لحذفها")
+
+ # أزرار الحفظ والتصدير مع تصميم محسن
+ st.divider()
+ st.subheader("حفظ وتصدير البيانات")
+
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
col1, col2 = st.columns(2)
with col1:
- if st.button("تصدير تحليل التكاليف إلى Excel", key="export_cost_button"):
- # محاكاة تصدير البيانات
- st.success("تم تصدير تحليل التكاليف إلى Excel بنجاح!")
+ # بطاقة حفظ التسعير
+ st.markdown("""
+
+
💾
+
حفظ التسعير غير المتوازن
+
قم بحفظ التسعير الحالي في المشروع لاستخدامه لاحقاً في التقارير وفي إجمالي التسعير.
+
+ """, unsafe_allow_html=True)
+
+ # زر حفظ التسعير غير المتوازن
+ if st.button("حفظ التسعير غير المتوازن", type="primary", use_container_width=True, key="save_unbalanced_pricing_button"):
+ st.session_state.current_pricing['items'] = items.copy()
+ st.session_state.current_pricing['method'] = "التسعير غير المتزن"
+ st.success("تم حفظ التسعير غير المتوازن بنجاح!")
+ st.balloons() # إضافة تأثير احتفالي عند الحفظ
with col2:
- uploaded_file = st.file_uploader("استيراد تحليل التكاليف من Excel", type=["xlsx"], key="import_cost_file")
+ # بطاقة تصدير التسعير
+ st.markdown("""
+
+
📊
+
تصدير البيانات
+
قم بتصدير جدول التسعير الحالي بصيغة CSV لاستخدامه في برامج أخرى مثل Excel.
+
+ """, unsafe_allow_html=True)
- if uploaded_file is not None:
- if st.button("استيراد البيانات", key="import_cost_button"):
- # محاكاة استيراد البيانات
- st.success("تم استيراد تحليل التكاليف بنجاح!")
-
- def _render_pricing_scenarios_tab(self):
- """عرض تبويب سيناريوهات التسعير"""
-
- st.markdown("### سيناريوهات التسعير")
-
- # عرض سيناريوهات التسعير الحالية
- st.markdown("#### قائمة السيناريوهات")
-
- # تحويل قائمة السيناريوهات إلى DataFrame
- scenarios_df = pd.DataFrame(st.session_state.price_scenarios)
-
- # عرض السيناريوهات كجدول قابل للتعديل
- edited_df = st.data_editor(
- scenarios_df,
- column_config={
- "id": st.column_config.NumberColumn("الرقم", disabled=True),
- "name": st.column_config.TextColumn("اسم السيناريو"),
- "description": st.column_config.TextColumn("الوصف"),
- "total_cost": st.column_config.NumberColumn("إجمالي التكلفة (ريال)", min_value=0, format="%.2f"),
- "profit_margin": st.column_config.NumberColumn("هامش الربح (%)", min_value=0, format="%.1f"),
- "total_price": st.column_config.NumberColumn("السعر الإجمالي (ريال)", min_value=0, format="%.2f"),
- "is_active": st.column_config.CheckboxColumn("نشط")
- },
- use_container_width=True,
- hide_index=True,
- num_rows="dynamic"
- )
+ # زر تصدير التسعير
+ export_button = st.button("تجهيز ملف للتصدير", use_container_width=True, key="prepare_export_file_button")
+ if export_button:
+ # تحويل البيانات إلى CSV
+ csv = items.to_csv(index=False)
+ st.success("تم تجهيز ملف التصدير بنجاح! يمكنك تنزيله الآن.")
+ # تقديم البيانات للتنزيل
+ st.download_button(
+ label="تنزيل ملف CSV",
+ data=csv,
+ file_name="unbalanced_pricing.csv",
+ mime="text/csv",
+ use_container_width=True
+ )
+
+ def _render_construction_calculator_tab(self):
+ """عرض تبويب حاسبة تكاليف البناء المتكاملة"""
- # تحديث قائمة السيناريوهات
- if not edited_df.equals(scenarios_df):
- # التأكد من وجود سيناريو نشط واحد فقط
- active_count = sum(edited_df['is_active'])
- if active_count != 1:
- st.error("يجب أن يكون هناك سيناريو نشط واحد فقط.")
- else:
- st.session_state.price_scenarios = edited_df.to_dict('records')
- st.success("تم تحديث سيناريوهات التسعير بنجاح!")
+ # استدعاء حاسبة تكاليف البناء المتكاملة الجديدة
+ from .construction_calculator import render_construction_calculator
- # إضافة سيناريو جديد
- st.markdown("#### إضافة سيناريو جديد")
+ st.markdown("### حاسبة تكاليف البناء المتكاملة")
- with st.form(key="add_scenario_form"):
+ # شرح حاسبة تكاليف البناء
+ with st.expander("دليل استخدام حاسبة تكاليف البناء", expanded=False):
+ st.markdown("""
+ **حاسبة تكاليف البناء المتكاملة** هي أداة تساعد في حساب تكاليف البناء بشكل تفصيلي، مع مراعاة جميع عناصر التكلفة:
+
+ ### مكونات التكلفة:
+ - المواد الخام
+ - العمالة
+ - المعدات
+ - المصاريف الإدارية
+ - هامش الربح
+
+ ### كيفية الاستخدام:
+ 1. اختر إما حساب تكلفة بند واحد أو حساب تكلفة مشروع.
+ 2. أدخل بيانات المواد والعمالة والمعدات.
+ 3. حدد نسب المصاريف الإدارية وهامش الربح.
+ 4. اضبط عوامل التعديل حسب ظروف المشروع.
+ 5. احصل على تحليل تفصيلي للتكاليف والسعر النهائي.
+
+ ### مميزات الحاسبة:
+ - قاعدة بيانات مدمجة للأسعار المرجعية للمواد والعمالة.
+ - تحليل نسب مساهمة كل عنصر في التكلفة.
+ - إمكانية تعديل عوامل التكلفة حسب الموقع والظروف.
+ - تصدير النتائج بتنسيقات متعددة.
+ - الربط مع وحدة التسعير لنقل النتائج مباشرة إلى جدول البنود.
+ - قاعدة بيانات للبنود النموذجية في أعمال المقاولات.
+ """)
+
+ render_construction_calculator()
+
+ # شرح حاسبة تكاليف البناء
+ with st.expander("دليل استخدام حاسبة تكاليف البناء", expanded=False):
+ st.markdown("""
+ **حاسبة تكاليف البناء المتكاملة** هي أداة تساعد في حساب تكاليف البناء بشكل تفصيلي، مع مراعاة جميع عناصر التكلفة:
+
+ ### مكونات التكلفة:
+
+ 1. **المواد الخام**: جميع المواد المستخدمة في البناء مثل الخرسانة، الحديد، الطوب، الأسمنت، وغيرها.
+ 2. **العمالة**: تكاليف جميع العمالة المطلوبة بمختلف تخصصاتها.
+ 3. **المعدات**: تكاليف استخدام أو استئجار المعدات اللازمة للمشروع.
+ 4. **المصاريف الإدارية**: النفقات العامة والإدارية للمشروع (نسبة من التكلفة المباشرة).
+ 5. **هامش الربح**: نسبة الربح المضافة على التكلفة.
+
+ ### طريقة الاستخدام:
+
+ 1. اختر إما حساب تكلفة بند واحد أو حساب تكلفة مشروع كامل.
+ 2. أدخل بيانات المواد والعمالة والمعدات المستخدمة.
+ 3. حدد نسب المصاريف الإدارية وهامش الربح.
+ 4. اضبط عوامل التعديل حسب ظروف المشروع.
+ 5. احصل على تحليل تفصيلي للتكاليف والسعر النهائي.
+
+ ### ميزات الحاسبة:
+
+ - قاعدة بيانات مدمجة للأسعار المرجعية للمواد والعمالة والمعدات.
+ - تحليل نسب مساهمة كل عنصر في التكلفة الإجمالية.
+ - إمكانية تعديل عوامل التكلفة حسب الموقع والظروف السوقية.
+ - تصدير النتائج بتنسيقات مختلفة.
+ - الربط مع وحدة التسعير لنقل النتائج مباشرة إلى جدول التسعير.
+ - قاعدة بيانات للبنود النموذجية في أعمال المقاولات (مناهل، مواسير، إسفلت، إلخ).
+ """)
+
+ # تبويبات حاسبة التكاليف
+ calc_tabs = st.tabs(["حساب تكلفة بند", "حساب تكلفة مشروع", "قوائم الأسعار المرجعية", "كتالوج أعمال المقاولات"])
+
+ with calc_tabs[0]: # حساب تكلفة بند
+ st.markdown("#### حساب تكلفة بند بناء")
+
+ # تهيئة بيانات البند الافتراضية إذا لم تكن موجودة
+ if 'construction_item' not in st.session_state:
+ st.session_state.construction_item = {
+ 'وصف_البند': "توريد وصب خرسانة مسلحة للأساسات",
+ 'الكمية': 25.0,
+ 'الوحدة': "م3",
+ 'المواد': [
+ {'الاسم': 'خرسانة جاهزة', 'الكمية': 25.0, 'الوحدة': 'م3', 'سعر_الوحدة': 750.0},
+ {'الاسم': 'حديد تسليح', 'الكمية': 3.5, 'الوحدة': 'طن', 'سعر_الوحدة': 5500.0}
+ ],
+ 'العمالة': [
+ {'النوع': 'عامل خرسانة', 'العدد': 6, 'المدة': 1.0, 'سعر_اليوم': 150.0},
+ {'النوع': 'حداد مسلح', 'العدد': 4, 'المدة': 3.0, 'سعر_اليوم': 250.0},
+ {'النوع': 'نجار مسلح', 'العدد': 4, 'المدة': 3.0, 'سعر_اليوم': 250.0}
+ ],
+ 'المعدات': [
+ {'النوع': 'مضخة خرسانة', 'العدد': 1.0, 'المدة': 0.5, 'سعر_اليوم': 5000.0},
+ {'النوع': 'هزاز خرسانة', 'العدد': 2.0, 'المدة': 1.0, 'سعر_اليوم': 150.0}
+ ],
+ 'المصاريف_الإدارية': 0.05, # 5%
+ 'هامش_الربح': 0.10, # 10%
+ 'عوامل_التعديل': {
+ 'location_factor': 1.0,
+ 'time_factor': 1.0,
+ 'risk_factor': 1.0,
+ 'market_factor': 1.0
+ }
+ }
+
+ # نموذج إدخال بيانات البند
col1, col2 = st.columns(2)
with col1:
- new_name = st.text_input("اسم السيناريو", key="new_scenario_name")
- new_description = st.text_area("الوصف", key="new_scenario_description")
+ st.session_state.construction_item['وصف_البند'] = st.text_area(
+ "وصف البند",
+ value=st.session_state.construction_item['وصف_البند'],
+ key="construction_item_description"
+ )
with col2:
- # حساب إجمالي التكاليف الحالي
- total_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] != 'أرباح')
-
- st.number_input("إجمالي التكلفة (ريال)", min_value=0.0, value=total_cost, key="new_scenario_total_cost", disabled=True)
- new_profit_margin = st.number_input("هامش الربح (%)", min_value=0.0, max_value=100.0, value=10.0, key="new_scenario_profit_margin")
-
- # حساب السعر الإجمالي
- new_profit_amount = total_cost * (new_profit_margin / 100)
- new_total_price = total_cost + new_profit_amount
-
- st.number_input("السعر الإجمالي (ريال)", min_value=0.0, value=new_total_price, key="new_scenario_total_price", disabled=True)
- new_is_active = st.checkbox("نشط", key="new_scenario_is_active")
-
- submit_button = st.form_submit_button("إضافة سيناريو")
-
- if submit_button:
- if new_name:
- # التحقق من حالة التنشيط
- if new_is_active:
- # إلغاء تنشيط جميع السيناريوهات الأخرى
- for scenario in st.session_state.price_scenarios:
- scenario['is_active'] = False
-
- # إنشاء معرف جديد
- new_id = max([item['id'] for item in st.session_state.price_scenarios], default=0) + 1
-
- # إضافة السيناريو الجديد
- st.session_state.price_scenarios.append({
- 'id': new_id,
- 'name': new_name,
- 'description': new_description,
- 'total_cost': total_cost,
- 'profit_margin': new_profit_margin,
- 'total_price': new_total_price,
- 'is_active': new_is_active
- })
+ st.session_state.construction_item['الكمية'] = st.number_input(
+ "الكمية",
+ value=st.session_state.construction_item['الكمية'],
+ min_value=0.1,
+ format="%.2f"
+ )
+
+ unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"]
+ st.session_state.construction_item['الوحدة'] = st.selectbox(
+ "الوحدة",
+ options=unit_options,
+ index=unit_options.index(st.session_state.construction_item['الوحدة']) if st.session_state.construction_item['الوحدة'] in unit_options else 0,
+ key="construction_item_unit_2"
+ )
+
+ # إدخال تفاصيل المواد
+ st.markdown("#### تفاصيل المواد")
+
+ material_controls = []
+ for i, material in enumerate(st.session_state.construction_item['المواد']):
+ col1, col2, col3, col4, col5 = st.columns([3, 2, 1, 2, 1])
+
+ with col1:
+ material_name = st.text_input(
+ "اسم المادة",
+ value=material['الاسم'],
+ key=f"material_name_{i}"
+ )
+
+ with col2:
+ material_qty = st.number_input(
+ "الكمية",
+ value=material['الكمية'],
+ min_value=0.0,
+ format="%.2f",
+ key=f"material_qty_{i}"
+ )
+
+ with col3:
+ material_unit = st.selectbox(
+ "الوحدة",
+ options=unit_options,
+ index=unit_options.index(material['الوحدة']) if material['الوحدة'] in unit_options else 0,
+ key=f"material_unit_{i}"
+ )
+
+ with col4:
+ material_price = st.number_input(
+ "سعر الوحدة",
+ value=material['سعر_الوحدة'],
+ min_value=0.0,
+ format="%.2f",
+ key=f"material_price_{i}"
+ )
- st.success(f"تمت إضافة السيناريو '{new_name}' بنجاح!")
+ with col5:
+ delete_button = st.button("حذف", key=f"delete_material_{i}")
+
+ material_controls.append({
+ 'الاسم': material_name,
+ 'الكمية': material_qty,
+ 'الوحدة': material_unit,
+ 'سعر_الوحدة': material_price,
+ 'delete': delete_button
+ })
+
+ # إضافة مادة جديدة
+ if st.button("إضافة مادة جديدة"):
+ st.session_state.construction_item['المواد'].append({
+ 'الاسم': '',
+ 'الكمية': 0.0,
+ 'الوحدة': 'م3',
+ 'سعر_الوحدة': 0.0
+ })
+ st.rerun()
+
+ # تحديث أو حذف المواد
+ new_materials = []
+ for i, control in enumerate(material_controls):
+ if not control['delete']:
+ new_materials.append({
+ 'الاسم': control['الاسم'],
+ 'الكمية': control['الكمية'],
+ 'الوحدة': control['الوحدة'],
+ 'سعر_الوحدة': control['سعر_الوحدة']
+ })
+
+ if len(new_materials) != len(st.session_state.construction_item['المواد']):
+ st.session_state.construction_item['المواد'] = new_materials
st.rerun()
else:
- st.error("يرجى إدخال اسم السيناريو.")
-
- # تحليل السيناريوهات
- st.markdown("#### تحليل السيناريوهات")
-
- # مقارنة السيناريوهات
- st.markdown("##### مقارنة السيناريوهات")
-
- # إنشاء DataFrame للرسم البياني
- scenarios_comparison_df = pd.DataFrame({
- 'السيناريو': [item['name'] for item in st.session_state.price_scenarios],
- 'التكلفة الإجمالية': [item['total_cost'] for item in st.session_state.price_scenarios],
- 'هامش الربح (%)': [item['profit_margin'] for item in st.session_state.price_scenarios],
- 'السعر الإجمالي': [item['total_price'] for item in st.session_state.price_scenarios],
- 'الحالة': ['نشط' if item['is_active'] else 'غير نشط' for item in st.session_state.price_scenarios]
- })
-
- # إنشاء رسم بياني شريطي مزدوج
- fig = go.Figure()
-
- # إضافة شريط للتكلفة الإجمالية
- fig.add_trace(go.Bar(
- x=scenarios_comparison_df['السيناريو'],
- y=scenarios_comparison_df['التكلفة الإجمالية'],
- name='التكلفة الإجمالية',
- marker_color='indianred'
- ))
-
- # إضافة شريط للسعر الإجمالي
- fig.add_trace(go.Bar(
- x=scenarios_comparison_df['السيناريو'],
- y=scenarios_comparison_df['السعر الإجمالي'],
- name='السعر الإجمالي',
- marker_color='lightsalmon'
- ))
-
- # إضافة خط لهامش الربح
- fig.add_trace(go.Scatter(
- x=scenarios_comparison_df['السيناريو'],
- y=scenarios_comparison_df['هامش الربح (%)'] * 10000, # تكبير القيم لتظهر على الرسم البياني
- name='هامش الربح (%)',
- yaxis='y2',
- line=dict(color='royalblue', width=4)
- ))
-
- # تعديل تخطيط الرسم البياني
- fig.update_layout(
- title='مقارنة سيناريوهات التسعير',
- xaxis_title='السيناريو',
- yaxis_title='المبلغ (ريال)',
- yaxis2=dict(
- title='هامش الربح (%)',
- titlefont=dict(color='royalblue'),
- tickfont=dict(color='royalblue'),
- overlaying='y',
- side='right',
- range=[0, 20]
- ),
- barmode='group',
- legend=dict(
- x=0,
- y=1.2,
- orientation='h'
- )
- )
-
- # تعديل النص على الأشرطة
- fig.update_traces(
- texttemplate='%{y:,.0f}',
- textposition='outside'
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل تأثير هامش الربح
- st.markdown("##### تحليل تأثير هامش الربح")
-
- # إنشاء نطاق من هوامش الربح
- profit_margins = list(range(0, 21, 2)) # من 0% إلى 20% بزيادة 2%
-
- # حساب السعر الإجمالي لكل هامش ربح
- total_cost = st.session_state.price_scenarios[0]['total_cost'] # استخدام التكلفة الإجمالية من السيناريو الأول
- total_prices = [total_cost * (1 + margin / 100) for margin in profit_margins]
-
- # إنشاء DataFrame للرسم البياني
- profit_analysis_df = pd.DataFrame({
- 'هامش الربح (%)': profit_margins,
- 'السعر الإجمالي': total_prices
- })
-
- # إنشاء رسم بياني خطي
- fig = px.line(
- profit_analysis_df,
- x='هامش الربح (%)',
- y='السعر الإجمالي',
- title='تأثير هامش الربح على السعر الإجمالي',
- markers=True
- )
-
- # تعديل النص على النقاط
- fig.update_traces(
- texttemplate='%{y:,.0f}',
- textposition='top center'
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل نقطة التعادل
- st.markdown("##### تحليل نقطة التعادل")
-
- # افتراض تكاليف ثابتة ومتغيرة
- fixed_costs = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
- variable_costs = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
-
- # افتراض سعر البيع من السيناريو النشط
- active_scenario = next((item for item in st.session_state.price_scenarios if item['is_active']), None)
- if active_scenario:
- selling_price = active_scenario['total_price']
- else:
- selling_price = st.session_state.price_scenarios[0]['total_price']
-
- # حساب نقطة التعادل
- if selling_price > variable_costs:
- breakeven_point = fixed_costs / (selling_price - variable_costs)
- st.metric("نقطة التعادل", f"{breakeven_point:.2f} وحدة")
-
- # إنشاء رسم بياني لنقطة التعادل
- units = list(range(0, int(breakeven_point * 2) + 1, max(1, int(breakeven_point / 10))))
-
- total_costs = [fixed_costs + variable_costs * unit for unit in units]
- total_revenues = [selling_price * unit for unit in units]
- profits = [revenue - cost for revenue, cost in zip(total_revenues, total_costs)]
-
- breakeven_df = pd.DataFrame({
- 'الوحدات': units,
- 'إجمالي التكاليف': total_costs,
- 'إجمالي الإيرادات': total_revenues,
- 'الربح': profits
- })
+ for i, material in enumerate(new_materials):
+ st.session_state.construction_item['المواد'][i] = material
- fig = go.Figure()
+ # إدخال تفاصيل العمالة
+ st.markdown("#### تفاصيل العمالة")
- fig.add_trace(go.Scatter(
- x=breakeven_df['الوحدات'],
- y=breakeven_df['إجمالي التكاليف'],
- name='إجمالي التكاليف',
- line=dict(color='red', width=2)
- ))
+ labor_controls = []
+ for i, labor in enumerate(st.session_state.construction_item['العمالة']):
+ col1, col2, col3, col4, col5 = st.columns([3, 1, 1, 2, 1])
+
+ with col1:
+ labor_type = st.text_input(
+ "نوع العامل",
+ value=labor['النوع'],
+ key=f"labor_type_{i}"
+ )
+
+ with col2:
+ labor_count = st.number_input(
+ "العدد",
+ value=float(labor['العدد']),
+ min_value=1.0,
+ step=1.0,
+ key=f"labor_count_{i}"
+ )
+
+ with col3:
+ labor_days = st.number_input(
+ "المدة (أيام)",
+ value=labor['المدة'],
+ min_value=0.1,
+ format="%.1f",
+ key=f"labor_days_{i}"
+ )
+
+ with col4:
+ labor_daily_rate = st.number_input(
+ "سعر اليوم",
+ value=labor['سعر_اليوم'],
+ min_value=0.0,
+ format="%.2f",
+ key=f"labor_daily_rate_{i}"
+ )
+
+ with col5:
+ delete_button = st.button("حذف", key=f"delete_labor_{i}")
+
+ labor_controls.append({
+ 'النوع': labor_type,
+ 'العدد': labor_count,
+ 'المدة': labor_days,
+ 'سعر_اليوم': labor_daily_rate,
+ 'delete': delete_button
+ })
- fig.add_trace(go.Scatter(
- x=breakeven_df['الوحدات'],
- y=breakeven_df['إجمالي الإيرادات'],
- name='إجمالي الإيرادات',
- line=dict(color='green', width=2)
- ))
+ # إضافة عامل جديد
+ if st.button("إضافة عامل جديد"):
+ st.session_state.construction_item['العمالة'].append({
+ 'النوع': '',
+ 'العدد': 1.0,
+ 'المدة': 1.0,
+ 'سعر_اليوم': 0.0
+ })
+ st.rerun()
- fig.add_trace(go.Scatter(
- x=breakeven_df['الوحدات'],
- y=breakeven_df['الربح'],
- name='الربح',
- line=dict(color='blue', width=2)
- ))
+ # تحديث أو حذف العمالة
+ new_labor = []
+ for i, control in enumerate(labor_controls):
+ if not control['delete']:
+ new_labor.append({
+ 'النوع': control['النوع'],
+ 'العدد': control['العدد'],
+ 'المدة': control['المدة'],
+ 'سعر_اليوم': control['سعر_اليوم']
+ })
- # إضافة خط عمودي عند نقطة التعادل
- fig.add_vline(
- x=breakeven_point,
- line_dash="dash",
- line_color="black",
- annotation_text=f"نقطة التعادل: {breakeven_point:.2f}",
- annotation_position="top right"
- )
+ if len(new_labor) != len(st.session_state.construction_item['العمالة']):
+ st.session_state.construction_item['العمالة'] = new_labor
+ st.rerun()
+ else:
+ for i, labor in enumerate(new_labor):
+ st.session_state.construction_item['العمالة'][i] = labor
- # إضافة خط أفقي عند الصفر
- fig.add_hline(
- y=0,
- line_dash="dash",
- line_color="gray"
- )
+ # إدخال تفاصيل المعدات
+ st.markdown("#### تفاصيل المعدات")
- fig.update_layout(
- title='تحليل نقطة التعادل',
- xaxis_title='الوحدات',
- yaxis_title='المبلغ (ريال)'
- )
+ equipment_controls = []
+ for i, equipment in enumerate(st.session_state.construction_item['المعدات']):
+ col1, col2, col3, col4, col5 = st.columns([3, 1, 1, 2, 1])
+
+ with col1:
+ equip_type = st.text_input(
+ "نوع المعدة",
+ value=equipment['النوع'],
+ key=f"equip_type_{i}"
+ )
+
+ with col2:
+ equip_count = st.number_input(
+ "العدد",
+ value=float(equipment['العدد']),
+ min_value=1.0,
+ step=1.0,
+ key=f"equip_count_{i}"
+ )
+
+ with col3:
+ equip_days = st.number_input(
+ "المدة (أيام)",
+ value=equipment['المدة'],
+ min_value=0.1,
+ format="%.1f",
+ key=f"equip_days_{i}"
+ )
+
+ with col4:
+ equip_daily_rate = st.number_input(
+ "سعر اليوم",
+ value=equipment['سعر_اليوم'],
+ min_value=0.0,
+ format="%.2f",
+ key=f"equip_daily_rate_{i}"
+ )
+
+ with col5:
+ delete_button = st.button("حذف", key=f"delete_equipment_{i}")
+
+ equipment_controls.append({
+ 'النوع': equip_type,
+ 'العدد': equip_count,
+ 'المدة': equip_days,
+ 'سعر_اليوم': equip_daily_rate,
+ 'delete': delete_button
+ })
- st.plotly_chart(fig, use_container_width=True)
- else:
- st.warning("لا يمكن حساب نقطة التعادل لأن سعر البيع أقل من التكاليف المتغيرة.")
-
- def _render_competitive_analysis_tab(self):
- """عرض تبويب المقارنة التنافسية"""
-
- st.markdown("### المقارنة التنافسية")
-
- # بيانات افتراضية للمنافسين
- competitors_data = [
- {
- 'name': 'شركتنا',
- 'price': 670000,
- 'quality': 4.5,
- 'delivery_time': 180,
- 'experience': 8,
- 'local_content': 85
- },
- {
- 'name': 'المنافس أ',
- 'price': 700000,
- 'quality': 4.2,
- 'delivery_time': 200,
- 'experience': 10,
- 'local_content': 75
- },
- {
- 'name': 'المنافس ب',
- 'price': 650000,
- 'quality': 3.8,
- 'delivery_time': 160,
- 'experience': 5,
- 'local_content': 90
- },
- {
- 'name': 'المنافس ج',
- 'price': 680000,
- 'quality': 4.0,
- 'delivery_time': 190,
- 'experience': 12,
- 'local_content': 80
- }
- ]
-
- # عرض بيانات المنافسين
- st.markdown("#### بيانات المنافسين")
-
- competitors_df = pd.DataFrame(competitors_data)
- st.dataframe(competitors_df, use_container_width=True, hide_index=True)
-
- # مقارنة الأسعار
- st.markdown("#### مقارنة الأسعار")
-
- fig = px.bar(
- competitors_df,
- x='name',
- y='price',
- title='مقارنة الأسعار بين المنافسين',
- color='price',
- text_auto='.2s'
- )
-
- fig.update_layout(
- xaxis_title='المنافس',
- yaxis_title='السعر (ريال)'
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # مقارنة متعددة الأبعاد
- st.markdown("#### مقارنة متعددة الأبعاد")
-
- # تحويل البيانات إلى تنسيق مناسب للرسم البياني الراداري
- categories = ['price', 'quality', 'delivery_time', 'experience', 'local_content']
-
- # تطبيع البيانات (لجعل القيم بين 0 و 1)
- normalized_data = {}
-
- for category in categories:
- if category == 'price' or category == 'delivery_time':
- # للسعر ووقت التسليم، القيمة الأقل أفضل
- min_val = min(item[category] for item in competitors_data)
- max_val = max(item[category] for item in competitors_data)
- normalized_data[category] = [(max_val - item[category]) / (max_val - min_val) if max_val != min_val else 0.5 for item in competitors_data]
+ # إضافة معدة جديدة
+ if st.button("إضافة معدة جديدة"):
+ st.session_state.construction_item['المعدات'].append({
+ 'النوع': '',
+ 'العدد': 1.0,
+ 'المدة': 1.0,
+ 'سعر_اليوم': 0.0
+ })
+ st.rerun()
+
+ # تحديث أو حذف المعدات
+ new_equipment = []
+ for i, control in enumerate(equipment_controls):
+ if not control['delete']:
+ new_equipment.append({
+ 'النوع': control['النوع'],
+ 'العدد': control['العدد'],
+ 'المدة': control['المدة'],
+ 'سعر_اليوم': control['سعر_اليوم']
+ })
+
+ if len(new_equipment) != len(st.session_state.construction_item['المعدات']):
+ st.session_state.construction_item['المعدات'] = new_equipment
+ st.rerun()
else:
- # للجودة والخبرة والمحتوى المحلي، القيمة الأعلى أفضل
- min_val = min(item[category] for item in competitors_data)
- max_val = max(item[category] for item in competitors_data)
- normalized_data[category] = [(item[category] - min_val) / (max_val - min_val) if max_val != min_val else 0.5 for item in competitors_data]
-
- # إنشاء الرسم البياني الراداري
- fig = go.Figure()
-
- for i, competitor in enumerate(competitors_data):
- fig.add_trace(go.Scatterpolar(
- r=[normalized_data[category][i] for category in categories],
- theta=['السعر', 'الجودة', 'وقت التسليم', 'الخبرة', 'المحتوى المحلي'],
- fill='toself',
- name=competitor['name']
- ))
-
- fig.update_layout(
- polar=dict(
- radialaxis=dict(
- visible=True,
- range=[0, 1]
+ for i, equipment in enumerate(new_equipment):
+ st.session_state.construction_item['المعدات'][i] = equipment
+
+ # النسب والعوامل
+ st.markdown("#### المصاريف الإدارية وهامش الربح")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.session_state.construction_item['المصاريف_الإدارية'] = st.slider(
+ "نسبة المصاريف الإدارية (%)",
+ min_value=0.0,
+ max_value=20.0,
+ value=st.session_state.construction_item['المصاريف_الإدارية'] * 100,
+ step=0.5
+ ) / 100
+
+ with col2:
+ st.session_state.construction_item['هامش_الربح'] = st.slider(
+ "نسبة هامش الربح (%)",
+ min_value=0.0,
+ max_value=30.0,
+ value=st.session_state.construction_item['هامش_الربح'] * 100,
+ step=0.5
+ ) / 100
+
+ # عوامل التعديل
+ st.markdown("#### عوامل تعديل التكلفة")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.session_state.construction_item['عوامل_التعديل']['location_factor'] = st.slider(
+ "معامل الموقع",
+ min_value=0.5,
+ max_value=2.0,
+ value=st.session_state.construction_item['عوامل_التعديل']['location_factor'],
+ step=0.05,
+ help="يؤثر في التكلفة حسب صعوبة أو سهولة الوصول للموقع وتوفر الخدمات"
)
- ),
- title='مقارنة متعددة الأبعاد بين المنافسين',
- showlegend=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تحليل نقاط القوة والضعف
- st.markdown("#### تحليل نقاط القوة والضعف")
-
- # تحديد نقاط القوة والضعف لشركتنا
- our_company = competitors_data[0]
-
- strengths = []
- weaknesses = []
-
- # مقارنة السعر
- other_prices = [comp['price'] for comp in competitors_data[1:]]
- if our_company['price'] <= min(other_prices):
- strengths.append("السعر تنافسي جداً")
- elif our_company['price'] <= sum(other_prices) / len(other_prices):
- strengths.append("السعر تنافسي")
- else:
- weaknesses.append("السعر أعلى من المتوسط")
-
- # مقارنة الجودة
- other_qualities = [comp['quality'] for comp in competitors_data[1:]]
- if our_company['quality'] >= max(other_qualities):
- strengths.append("الجودة ممتازة")
- elif our_company['quality'] >= sum(other_qualities) / len(other_qualities):
- strengths.append("الجودة جيدة")
- else:
- weaknesses.append("الجودة أقل من المتوسط")
-
- # مقارنة وقت التسليم
- other_delivery_times = [comp['delivery_time'] for comp in competitors_data[1:]]
- if our_company['delivery_time'] <= min(other_delivery_times):
- strengths.append("وقت التسليم سريع جداً")
- elif our_company['delivery_time'] <= sum(other_delivery_times) / len(other_delivery_times):
- strengths.append("وقت التسليم جيد")
- else:
- weaknesses.append("وقت التسليم أطول من المتوسط")
-
- # مقارنة الخبرة
- other_experiences = [comp['experience'] for comp in competitors_data[1:]]
- if our_company['experience'] >= max(other_experiences):
- strengths.append("خبرة واسعة جداً")
- elif our_company['experience'] >= sum(other_experiences) / len(other_experiences):
- strengths.append("خبرة جيدة")
- else:
- weaknesses.append("خبرة أقل من المتوسط")
-
- # مقارنة المحتوى المحلي
- other_local_contents = [comp['local_content'] for comp in competitors_data[1:]]
- if our_company['local_content'] >= max(other_local_contents):
- strengths.append("محتوى محلي ممتاز")
- elif our_company['local_content'] >= sum(other_local_contents) / len(other_local_contents):
- strengths.append("محتوى محلي جيد")
- else:
- weaknesses.append("محتوى محلي أقل من المتوسط")
-
- # عرض نقاط القوة والضعف
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown("##### نقاط القوة")
- for strength in strengths:
- st.markdown(f"- {strength}")
-
- with col2:
- st.markdown("##### نقاط الضعف")
- for weakness in weaknesses:
- st.markdown(f"- {weakness}")
-
- # توصيات للتسعير
- st.markdown("#### توصيات للتسعير")
-
- # تحديد التوصيات بناءً على المقارنة
- recommendations = []
-
- # توصية بناءً على السعر
- avg_price = sum(comp['price'] for comp in competitors_data) / len(competitors_data)
- if our_company['price'] > avg_price:
- recommendations.append("النظر في تخفيض السعر للمنافسة بشكل أفضل.")
-
- # توصية بناءً على الجودة
- if our_company['quality'] > sum(comp['quality'] for comp in competitors_data[1:]) / len(competitors_data[1:]):
- recommendations.append("التأكيد على جودة الخدمات في العروض التسويقية.")
-
- # توصية بناءً على وقت التسليم
- if our_company['delivery_time'] < sum(comp['delivery_time'] for comp in competitors_data[1:]) / len(competitors_data[1:]):
- recommendations.append("التأكيد على سرعة التسليم كميزة تنافسية.")
-
- # توصية بناءً على الخبرة
- if our_company['experience'] < max(comp['experience'] for comp in competitors_data[1:]):
- recommendations.append("تعزيز فريق العمل بخبرات إضافية.")
-
- # توصية بناءً على المحتوى المحلي
- if our_company['local_content'] > sum(comp['local_content'] for comp in competitors_data[1:]) / len(competitors_data[1:]):
- recommendations.append("التأكيد على نسبة المحتوى المحلي العالية في العروض.")
-
- # توصية عامة
- recommendations.append("مراجعة هيكل التكاليف بشكل دوري للحفاظ على القدرة التنافسية.")
-
- # عرض التوصيات
- for recommendation in recommendations:
- st.markdown(f"- {recommendation}")
-
- def _render_reports_tab(self):
- """عرض تبويب التقارير"""
-
- st.markdown("### التقارير")
-
- # قائمة التقارير المتاحة
- reports = [
- "تقرير جدول الكميات",
- "تقرير تحليل التكاليف",
- "تقرير سيناريوهات التسعير",
- "تقرير المقارنة التنافسية",
- "تقرير ملخص التسعير"
- ]
-
- # اختيار التقرير
- selected_report = st.selectbox("اختر التقرير", reports)
-
- # خيارات التصدير
- export_format = st.radio("صيغة التصدير", ["PDF", "Excel", "Word"])
-
- # زر إنشاء التقرير
- if st.button("إنشاء التقرير"):
- st.success(f"تم إنشاء {selected_report} بصيغة {export_format} بنجاح!")
-
- # عرض نموذج للتقرير
- st.markdown("#### نموذج التقرير")
-
- if selected_report == "تقرير جدول الكميات":
- self._render_boq_report()
- elif selected_report == "تقرير تحليل التكاليف":
- self._render_cost_analysis_report()
- elif selected_report == "تقرير سيناريوهات التسعير":
- self._render_pricing_scenarios_report()
- elif selected_report == "تقرير المقارنة التنافسية":
- self._render_competitive_analysis_report()
- elif selected_report == "تقرير ملخص التسعير":
- self._render_pricing_summary_report()
-
- def _render_boq_report(self):
- """عرض نموذج تقرير جدول الكميات"""
-
- st.markdown("### تقرير جدول الكميات")
- st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d"))
- st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري")
- st.markdown("**رقم المناقصة:** T-2024-001")
-
- st.markdown("#### جدول الكميات")
-
- # عرض جدول الكميات
- boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
- st.dataframe(boq_df, use_container_width=True, hide_index=True)
-
- # عرض إجمالي جدول الكميات
- total_boq = sum(item['total_price'] for item in st.session_state.bill_of_quantities)
- st.metric("إجمالي جدول الكميات", f"{total_boq:,.2f} ريال")
-
- # عرض توزيع البنود حسب الفئة
- st.markdown("#### توزيع البنود حسب الفئة")
-
- # تجميع البيانات حسب الفئة
- category_totals = {}
-
- for item in st.session_state.bill_of_quantities:
- category = item['category']
- if category in category_totals:
- category_totals[category] += item['total_price']
- else:
- category_totals[category] = item['total_price']
-
- category_df = pd.DataFrame({
- 'الفئة': list(category_totals.keys()),
- 'المبلغ': list(category_totals.values())
- })
+
+ st.session_state.construction_item['عوامل_التعديل']['time_factor'] = st.slider(
+ "معامل الوقت",
+ min_value=0.8,
+ max_value=1.5,
+ value=st.session_state.construction_item['عوامل_التعديل']['time_factor'],
+ step=0.05,
+ help="يؤثر في التكلفة حسب الجدول الزمني للمشروع وضرورة الإسراع في التنفيذ"
+ )
+
+ with col2:
+ st.session_state.construction_item['عوامل_التعديل']['risk_factor'] = st.slider(
+ "معامل المخاطر",
+ min_value=1.0,
+ max_value=1.5,
+ value=st.session_state.construction_item['عوامل_التعديل']['risk_factor'],
+ step=0.05,
+ help="يؤثر في التكلفة حسب المخاطر المتوقعة في المشروع"
+ )
+
+ st.session_state.construction_item['عوامل_التعديل']['market_factor'] = st.slider(
+ "معامل السوق",
+ min_value=0.8,
+ max_value=1.3,
+ value=st.session_state.construction_item['عوامل_التعديل']['market_factor'],
+ step=0.05,
+ help="يؤثر في التكلفة حسب حالة السوق الحالية وتغيرات الأسعار"
+ )
+
+ # صف أزرار العمليات
+ col1, col2 = st.columns(2)
+
+ with col1:
+ # زر حساب التكلفة
+ if st.button("حساب تكلفة البند", type="primary"):
+ with st.spinner("جاري حساب التكلفة..."):
+ # حساب التكلفة باستخدام الحاسبة
+ item_cost = self.construction_calculator.calculate_item_cost(st.session_state.construction_item)
+ st.session_state.item_cost_result = item_cost
+
+ with col2:
+ # زر استيراد بند من كتالوج أعمال المقاولات
+ if st.button("استيراد بند من الكتالوج"):
+ # تهيئة session state للاختيار من الكتالوج
+ if 'show_catalog_selection' not in st.session_state:
+ st.session_state.show_catalog_selection = True
+ else:
+ st.session_state.show_catalog_selection = True
+ st.rerun()
+
+ # عرض واجهة اختيار البند من الكتالوج
+ if 'show_catalog_selection' in st.session_state and st.session_state.show_catalog_selection:
+ st.markdown("#### اختيار بند من كتالوج أعمال المقاولات")
+
+ # الحصول على فئات البنود
+ categories = self.construction_templates.get_all_templates()['categories']
+ category_options = [f"{cat_data['name']}" for cat_id, cat_data in categories.items()]
+ category_ids = list(categories.keys())
+
+ selected_category_index = st.selectbox(
+ "اختر فئة البند",
+ options=range(len(category_options)),
+ format_func=lambda i: category_options[i],
+ key="construction_category_selector"
+ )
+
+ selected_category_id = category_ids[selected_category_index]
+
+ # الحصول على البنود في الفئة المحددة
+ templates = self.construction_templates.get_templates_by_category(selected_category_id)
+
+ if templates:
+ template_options = [f"{template['name']}" for template in templates]
+
+ selected_template_index = st.selectbox(
+ "اختر البند",
+ options=range(len(template_options)),
+ format_func=lambda i: template_options[i],
+ key="construction_template_selector"
+ )
+
+ selected_template = templates[selected_template_index]
+
+ # عرض تفاصيل البند المحدد
+ st.markdown(f"**وصف البند**: {selected_template['description']}")
+ st.markdown(f"**الوحدة**: {selected_template['unit']}")
+
+ if st.button("استخدام هذا البند", type="primary"):
+ # تحويل البند إلى صيغة الحاسبة
+ template_id = selected_template['id']
+ construction_item = self.construction_templates.convert_template_to_item(template_id)
+
+ # تحديث بيانات البند في session state
+ st.session_state.construction_item = construction_item
+ st.session_state.show_catalog_selection = False
+ st.rerun()
+ else:
+ st.info("لا توجد بنود في هذه الفئة")
+
+ if st.button("إلغاء"):
+ st.session_state.show_catalog_selection = False
+ st.rerun()
+
+ # عرض نتائج الحساب
+ if 'item_cost_result' in st.session_state:
+ st.markdown("### نتائج حساب تكلفة البند")
+
+ result = st.session_state.item_cost_result
+
+ # ملخص البند
+ st.markdown(f"**البند:** {result['وصف_البند']}")
+ st.markdown(f"**الكمية:** {result['الكمية']} {result['الوحدة']}")
+
+ # المكونات الرئيسية للتكلفة
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ st.metric(
+ "تكلفة المواد",
+ f"{result['تكاليف_مباشرة']['المواد']['الإجمالي']:,.2f} ريال"
+ )
+
+ with col2:
+ st.metric(
+ "تكلفة العمالة",
+ f"{result['تكاليف_مباشرة']['العمالة']['الإجمالي']:,.2f} ريال"
+ )
+
+ with col3:
+ st.metric(
+ "تكلفة المعدات",
+ f"{result['تكاليف_مباشرة']['المعدات']['الإجمالي']:,.2f} ريال"
+ )
+
+ with col4:
+ st.metric(
+ "التكلفة المباشرة",
+ f"{result['تكاليف_مباشرة']['إجمالي_تكاليف_مباشرة']:,.2f} ريال"
+ )
+
+ # تفاصيل المصاريف والربح والسعر النهائي
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric(
+ f"المصاريف الإدارية ({result['مصاريف_إدارية']['نسبة']}%)",
+ f"{result['مصاريف_إدارية']['قيمة']:,.2f} ريال"
+ )
+
+ with col2:
+ st.metric(
+ f"هامش الربح ({result['هامش_ربح']['نسبة']}%)",
+ f"{result['هامش_ربح']['قيمة']:,.2f} ريال"
+ )
+
+ with col3:
+ st.metric(
+ "التكلفة الإجمالية",
+ f"{result['التكلفة_الإجمالية']:,.2f} ريال"
+ )
+
+ # التكلفة بعد تطبيق عوامل التعديل
+ adjustment_factor = result['عوامل_التعديل']['المعامل_الإجمالي']
+ st.metric(
+ f"السعر النهائي المعدل (معامل التعديل: {adjustment_factor:.2f})",
+ f"{result['السعر_المعدل']['إجمالي']:,.2f} ريال",
+ delta=f"{(adjustment_factor - 1) * 100:.1f}%"
+ )
+
+ # سعر الوحدة وزر نقل السعر إلى جدول التسعير
+ col1, col2 = st.columns([2, 1])
+
+ with col1:
+ st.metric(
+ f"سعر الوحدة ({result['الوحدة']})",
+ f"{result['السعر_المعدل']['سعر_الوحدة']:,.2f} ريال"
+ )
+
+ with col2:
+ if st.button("نقل السعر إلى جدول التسعير", key="transfer_to_pricing"):
+ if 'manual_items' not in st.session_state:
+ st.session_state.manual_items = pd.DataFrame(columns=[
+ 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي'
+ ])
+
+ # إنشاء رقم البند الجديد
+ new_id = f"B{len(st.session_state.manual_items)+1}"
+
+ # إنشاء صف جديد
+ new_row = pd.DataFrame({
+ 'رقم البند': [new_id],
+ 'وصف البند': [result['وصف_البند']],
+ 'الوحدة': [result['الوحدة']],
+ 'الكمية': [float(result['الكمية'])],
+ 'سعر الوحدة': [float(result['السعر_المعدل']['سعر_الوحدة'])],
+ 'الإجمالي': [float(result['السعر_المعدل']['إجمالي'])]
+ })
+
+ # إضافة الصف إلى DataFrame
+ st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True)
+
+ # إنشاء حالة التسعير الحالي إذا لم تكن موجودة
+ if 'current_pricing' not in st.session_state:
+ st.session_state.current_pricing = {
+ 'name': 'تسعير جديد',
+ 'method': 'تسعير مستورد من حاسبة تكاليف البناء',
+ 'items': st.session_state.manual_items
+ }
+ else:
+ # تحديث البنود في التسعير الحالي
+ st.session_state.current_pricing['items'] = st.session_state.manual_items
+
+ st.success(f"تم نقل البند \"{result['وصف_البند']}\" إلى جدول التسعير بنجاح!")
+
+ # تفاصيل التكلفة - المواد
+ st.markdown("#### تفاصيل تكلفة المواد")
+ if len(result['تكاليف_مباشرة']['المواد']['التفاصيل']) > 0:
+ materials_df = pd.DataFrame(result['تكاليف_مباشرة']['المواد']['التفاصيل'])
+ st.dataframe(materials_df, use_container_width=True, hide_index=True)
+ else:
+ st.info("لا توجد مواد مضافة")
+
+ # تفاصيل التكلفة - العمالة
+ st.markdown("#### تفاصيل تكلفة العمالة")
+ if len(result['تكاليف_مباشرة']['العمالة']['التفاصيل']) > 0:
+ labor_df = pd.DataFrame(result['تكاليف_مباشرة']['العمالة']['التفاصيل'])
+ st.dataframe(labor_df, use_container_width=True, hide_index=True)
+ else:
+ st.info("لا توجد عمالة مضافة")
+
+ # تفاصيل التكلفة - المعدات
+ st.markdown("#### تفاصيل تكلفة المعدات")
+ if len(result['تكاليف_مباشرة']['المعدات']['التفاصيل']) > 0:
+ equipment_df = pd.DataFrame(result['تكاليف_مباشرة']['المعدات']['التفاصيل'])
+ st.dataframe(equipment_df, use_container_width=True, hide_index=True)
+ else:
+ st.info("لا توجد معدات مضافة")
+
+ # رسم بياني لتوزيع التكلفة
+ st.markdown("#### توزيع مكونات التكلفة")
+
+ cost_components = {
+ 'المواد': result['تكاليف_مباشرة']['المواد']['الإجمالي'],
+ 'العمالة': result['تكاليف_مباشرة']['العمالة']['الإجمالي'],
+ 'المعدات': result['تكاليف_مباشرة']['المعدات']['الإجمالي'],
+ 'المصاريف الإدارية': result['مصاريف_إدارية']['قيمة'],
+ 'هامش الربح': result['هامش_ربح']['قيمة']
+ }
+
+ colors = ['#2E86C1', '#28B463', '#EB984E', '#8E44AD', '#C0392B']
+
+ fig = px.pie(
+ values=list(cost_components.values()),
+ names=list(cost_components.keys()),
+ title='توزيع مكونات التكلفة',
+ color_discrete_sequence=colors
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
- fig = px.pie(
- category_df,
- values='المبلغ',
- names='الفئة',
- title='توزيع جدول الكميات حسب الفئة'
- )
+ with calc_tabs[1]: # حساب تكلفة مشروع
+ st.markdown("#### حساب تكلفة مشروع كامل")
+
+ # تهيئة بيانات المشروع الافتراضية إذا لم تكن موجودة
+ if 'construction_project' not in st.session_state:
+ st.session_state.construction_project = self.construction_calculator.generate_sample_project_data()
+
+ # نموذج إدخال بيانات المشروع
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.session_state.construction_project['اسم_المشروع'] = st.text_input(
+ "اسم المشروع",
+ value=st.session_state.construction_project['اسم_المشروع']
+ )
+
+ with col2:
+ st.session_state.construction_project['وصف_المشروع'] = st.text_area(
+ "وصف المشروع",
+ value=st.session_state.construction_project['وصف_المشروع'],
+ height=100,
+ key="construction_project_description"
+ )
+
+ # النسب والعوامل الإجمالية للمشروع
+ st.markdown("#### النسب والعوامل الإجمالية للمشروع")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.session_state.construction_project['المصاريف_الإدارية'] = st.slider(
+ "نسبة المصاريف الإدارية الإجمالية (%)",
+ min_value=0.0,
+ max_value=20.0,
+ value=st.session_state.construction_project['المصاريف_الإدارية'] * 100,
+ step=0.5,
+ key="project_admin_expenses"
+ ) / 100
+
+ with col2:
+ st.session_state.construction_project['هامش_الربح'] = st.slider(
+ "نسبة هامش الربح الإجمالي (%)",
+ min_value=0.0,
+ max_value=30.0,
+ value=st.session_state.construction_project['هامش_الربح'] * 100,
+ step=0.5,
+ key="project_profit_margin"
+ ) / 100
+
+ # عوامل التعديل للمشروع
+ st.markdown("#### عوامل تعديل التكلفة للمشروع")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.session_state.construction_project['عوامل_التعديل']['location_factor'] = st.slider(
+ "معامل الموقع",
+ min_value=0.5,
+ max_value=2.0,
+ value=st.session_state.construction_project['عوامل_التعديل']['location_factor'],
+ step=0.05,
+ help="يؤثر في التكلفة حسب صعوبة أو سهولة الوصول للموقع وتوفر الخدمات",
+ key="project_location_factor"
+ )
+
+ st.session_state.construction_project['عوامل_التعديل']['time_factor'] = st.slider(
+ "معامل الوقت",
+ min_value=0.8,
+ max_value=1.5,
+ value=st.session_state.construction_project['عوامل_التعديل']['time_factor'],
+ step=0.05,
+ help="يؤثر في التكلفة حسب الجدول الزمني للمشروع وضرورة الإسراع في التنفيذ",
+ key="project_time_factor"
+ )
+
+ with col2:
+ st.session_state.construction_project['عوامل_التعديل']['risk_factor'] = st.slider(
+ "معامل المخاطر",
+ min_value=1.0,
+ max_value=1.5,
+ value=st.session_state.construction_project['عوامل_التعديل']['risk_factor'],
+ step=0.05,
+ help="يؤثر في التكلفة حسب المخاطر المتوقعة في المشروع",
+ key="project_risk_factor"
+ )
+
+ st.session_state.construction_project['عوامل_التعديل']['market_factor'] = st.slider(
+ "معامل السوق",
+ min_value=0.8,
+ max_value=1.3,
+ value=st.session_state.construction_project['عوامل_التعديل']['market_factor'],
+ step=0.05,
+ help="يؤثر في التكلفة حسب حالة السوق الحالية وتغيرات الأسعار",
+ key="project_market_factor"
+ )
+
+ # زر حساب تكلفة المشروع
+ if st.button("حساب تكلفة المشروع", type="primary"):
+ with st.spinner("جاري حساب تكلفة المشروع..."):
+ # حساب التكلفة باستخدام الحاسبة
+ project_cost = self.construction_calculator.calculate_project_cost(st.session_state.construction_project)
+ st.session_state.project_cost_result = project_cost
+
+ # عرض نتائج حساب المشروع
+ if 'project_cost_result' in st.session_state:
+ st.markdown("### نتائج حساب تكلفة المشروع")
+
+ result = st.session_state.project_cost_result
+
+ # ملخص المشروع
+ st.markdown(f"**المشروع:** {result['اسم_المشروع']}")
+ st.markdown(f"**الوصف:** {result['وصف_المشروع']}")
+ st.markdown(f"**عدد البنود:** {result['عدد_البنود']} بند")
+
+ # المكونات الرئيسية للتكلفة
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric(
+ "إجمالي تكلفة المواد",
+ f"{result['تكاليف_مباشرة']['المواد']['الإجمالي']:,.2f} ريال",
+ delta=f"{result['تكاليف_مباشرة']['المواد']['النسبة_المئوية']:.1f}% من التكلفة المباشرة"
+ )
+
+ with col2:
+ st.metric(
+ "إجمالي تكلفة العمالة",
+ f"{result['تكاليف_مباشرة']['العمالة']['الإجمالي']:,.2f} ريال",
+ delta=f"{result['تكاليف_مباشرة']['العمالة']['النسبة_المئوية']:.1f}% من التكلفة المباشرة"
+ )
+
+ with col3:
+ st.metric(
+ "إجمالي تكلفة المعدات",
+ f"{result['تكاليف_مباشرة']['المعدات']['الإجمالي']:,.2f} ريال",
+ delta=f"{result['تكاليف_مباشرة']['المعدات']['النسبة_المئوية']:.1f}% من التكلفة المباشرة"
+ )
+
+ # إجمالي التكاليف المباشرة
+ st.metric(
+ "إجمالي التكاليف المباشرة",
+ f"{result['تكاليف_مباشرة']['إجمالي_تكاليف_مباشرة']:,.2f} ريال"
+ )
+
+ # تفاصيل المصاريف والربح والسعر النهائي
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric(
+ f"المصاريف الإدارية ({result['مصاريف_إدارية']['نسبة']}%)",
+ f"{result['مصاريف_إدارية']['قيمة']:,.2f} ريال"
+ )
+
+ with col2:
+ st.metric(
+ f"هامش الربح ({result['هامش_ربح']['نسبة']}%)",
+ f"{result['هامش_ربح']['قيمة']:,.2f} ريال"
+ )
+
+ with col3:
+ st.metric(
+ "التكلفة الإجمالية",
+ f"{result['التكلفة_الإجمالية']:,.2f} ريال"
+ )
+
+ # التكلفة بعد تطبيق عوامل التعديل
+ adjustment_factor = result['عوامل_التعديل']['المعامل_الإجمالي']
+ st.metric(
+ f"السعر النهائي المعدل (معامل التعديل: {adjustment_factor:.2f})",
+ f"{result['التكلفة_النهائية_المعدلة']:,.2f} ريال",
+ delta=f"{(adjustment_factor - 1) * 100:.1f}%"
+ )
+
+ # رسم بياني لتوزيع التكلفة
+ st.markdown("#### توزيع مكونات التكلفة")
+
+ cost_components = {
+ 'المواد': result['تكاليف_مباشرة']['المواد']['الإجمالي'],
+ 'العمالة': result['تكاليف_مباشرة']['العمالة']['الإجمالي'],
+ 'المعدات': result['تكاليف_مباشرة']['المعدات']['الإجمالي'],
+ 'المصاريف الإدارية': result['مصاريف_إدارية']['قيمة'],
+ 'هامش الربح': result['هامش_ربح']['قيمة']
+ }
+
+ colors = ['#2E86C1', '#28B463', '#EB984E', '#8E44AD', '#C0392B']
+
+ fig = px.pie(
+ values=list(cost_components.values()),
+ names=list(cost_components.keys()),
+ title='توزيع مكونات التكلفة',
+ color_discrete_sequence=colors
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # جدول تفاصيل البنود
+ st.markdown("#### تفاصيل بنود المشروع")
+
+ items_data = []
+ for i, item in enumerate(result['تفاصيل_البنود']):
+ items_data.append({
+ 'رقم': i + 1,
+ 'الوصف': item['وصف_البند'],
+ 'الكمية': item['الكمية'],
+ 'الوحدة': item['الوحدة'],
+ 'سعر الوحدة': item['سعر_الوحدة'],
+ 'الإجمالي': item['التكلفة_الإجمالية'],
+ 'بعد التعديل': item['السعر_المعدل']['إجمالي']
+ })
+
+ if len(items_data) > 0:
+ items_df = pd.DataFrame(items_data)
+
+ # تنسيق الجدول لعرض الأرقام بشكل أفضل
+ def highlight_row(row):
+ """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
+ color = '#F0F8FF' if row.name % 2 == 0 else 'white'
+ return ['background-color: %s' % color] * len(row)
+
+ styled_df = items_df.style.apply(highlight_row, axis=1)
+ styled_df = styled_df.format({
+ 'الكمية': '{:,.2f}',
+ 'سعر الوحدة': '{:,.2f}',
+ 'الإجمالي': '{:,.2f}',
+ 'بعد التعديل': '{:,.2f}'
+ })
+
+ st.dataframe(styled_df, use_container_width=True)
+ else:
+ st.info("لا توجد بنود في المشروع")
- st.plotly_chart(fig, use_container_width=True)
+ with calc_tabs[2]: # قوائم الأسعار المرجعية
+ st.markdown("#### قوائم الأسعار المرجعية")
+
+ ref_tabs = st.tabs(["قائمة المواد", "قائمة العمالة", "قائمة المعدات"])
+
+ with ref_tabs[0]: # قائمة المواد
+ st.markdown("#### قائمة أسعار المواد المرجعية")
+
+ # الحصول على قائمة المواد
+ materials = self.construction_calculator.get_all_rates(item_type='مادة')
+
+ if materials and 'المواد' in materials:
+ # تحويل قاموس المواد إلى DataFrame
+ materials_list = []
+ for name, data in materials['المواد'].items():
+ materials_list.append({
+ 'اسم المادة': name,
+ 'الوحدة': data.get('وحدة', ''),
+ 'سعر الوحدة': data.get('سعر_الوحدة', 0.0),
+ 'الوصف': data.get('وصف', ''),
+ 'الفئة': data.get('فئة', '')
+ })
+
+ if materials_list:
+ materials_df = pd.DataFrame(materials_list)
+
+ # تصفية حسب الفئة
+ categories = ['الكل'] + sorted(materials_df['الفئة'].unique().tolist())
+ selected_category = st.selectbox("تصفية حسب الفئة", categories, key="materials_cat_filter_section1")
+
+ if selected_category != 'الكل':
+ filtered_df = materials_df[materials_df['الفئة'] == selected_category]
+ else:
+ filtered_df = materials_df
+
+ # تنسيق الجدول
+ def highlight_materials_row(row):
+ """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
+ color = '#F0F8FF' if row.name % 2 == 0 else 'white'
+ return ['background-color: %s' % color] * len(row)
+
+ styled_df = filtered_df.style.apply(highlight_materials_row, axis=1)
+ styled_df = styled_df.format({
+ 'سعر الوحدة': '{:,.2f}'
+ })
+
+ st.dataframe(styled_df, use_container_width=True, hide_index=True)
+ else:
+ st.info("لا توجد مواد في قاعدة البيانات")
+ else:
+ st.info("لا توجد قائمة مواد متاحة")
+
+ with ref_tabs[1]: # قائمة العمالة
+ st.markdown("#### قائمة أسعار العمالة المرجعية")
+
+ # الحصول على قائمة العمالة
+ labor = self.construction_calculator.get_all_rates(item_type='عمالة')
+
+ if labor and 'العمالة' in labor:
+ # تحويل قاموس العمالة إلى DataFrame
+ labor_list = []
+ for name, data in labor['العمالة'].items():
+ labor_list.append({
+ 'نوع العامل': name,
+ 'وحدة الأجر': data.get('وحدة', ''),
+ 'سعر الوحدة': data.get('سعر_الوحدة', 0.0),
+ 'الوصف': data.get('وصف', ''),
+ 'الفئة': data.get('فئة', '')
+ })
+
+ if labor_list:
+ labor_df = pd.DataFrame(labor_list)
+
+ # تصفية حسب الفئة
+ categories = ['الكل'] + sorted(labor_df['الفئة'].unique().tolist())
+ selected_category = st.selectbox("تصفية حسب الفئة", categories, key="labor_cat_filter_section1")
+
+ if selected_category != 'الكل':
+ filtered_df = labor_df[labor_df['الفئة'] == selected_category]
+ else:
+ filtered_df = labor_df
+
+ # تنسيق الجدول
+ def highlight_labor_row(row):
+ """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
+ color = '#F0F8FF' if row.name % 2 == 0 else 'white'
+ return ['background-color: %s' % color] * len(row)
+
+ styled_df = filtered_df.style.apply(highlight_labor_row, axis=1)
+ styled_df = styled_df.format({
+ 'سعر الوحدة': '{:,.2f}'
+ })
+
+ st.dataframe(styled_df, use_container_width=True, hide_index=True)
+ else:
+ st.info("لا توجد عمالة في قاعدة البيانات")
+ else:
+ st.info("لا توجد قائمة عمالة متاحة")
+
+ with ref_tabs[2]: # قائمة المعدات
+ st.markdown("#### قائمة أسعار المعدات المرجعية")
+
+ # الحصول على قائمة المعدات
+ equipment = self.construction_calculator.get_all_rates(item_type='معدة')
+
+ if equipment and 'المعدات' in equipment:
+ # تحويل قاموس المعدات إلى DataFrame
+ equipment_list = []
+ for name, data in equipment['المعدات'].items():
+ equipment_list.append({
+ 'نوع المعدة': name,
+ 'وحدة الإيجار': data.get('وحدة', ''),
+ 'سعر الوحدة': data.get('سعر_الوحدة', 0.0),
+ 'الوصف': data.get('وصف', ''),
+ 'الفئة': data.get('فئة', '')
+ })
+
+ if equipment_list:
+ equipment_df = pd.DataFrame(equipment_list)
+
+ # تصفية حسب الفئة
+ categories = ['الكل'] + sorted(equipment_df['الفئة'].unique().tolist())
+ selected_category = st.selectbox("تصفية حسب الفئة", categories, key="equipment_cat_filter_section1")
+
+ if selected_category != 'الكل':
+ filtered_df = equipment_df[equipment_df['الفئة'] == selected_category]
+ else:
+ filtered_df = equipment_df
+
+ # تنسيق الجدول
+ def highlight_equipment_row(row):
+ """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
+ color = '#F0F8FF' if row.name % 2 == 0 else 'white'
+ return ['background-color: %s' % color] * len(row)
+
+ styled_df = filtered_df.style.apply(highlight_equipment_row, axis=1)
+ styled_df = styled_df.format({
+ 'سعر الوحدة': '{:,.2f}'
+ })
+
+ st.dataframe(styled_df, use_container_width=True, hide_index=True)
+ else:
+ st.info("لا توجد معدات في قاعدة البيانات")
+ else:
+ st.info("لا توجد قائمة معدات متاحة")
+
+ with calc_tabs[3]: # كتالوج أعمال المقاولات
+ st.markdown("#### كتالوج أعمال المقاولات")
+
+ # شرح كتالوج أعمال المقاولات
+ with st.expander("معلومات عن كتالوج أعمال المقاولات", expanded=False):
+ st.markdown("""
+ **كتالوج أعمال المقاولات** هو قاعدة بيانات شاملة للبنود النموذجية المستخدمة في مشاريع المقاولات ويوفر:
+
+ - بنود جاهزة لمختلف أنواع الأعمال الإنشائية (خرسانة مناهل مواسير طرق إلخ).
+ - تفاصيل دقيقة للمواد والعمالة والمعدات المطلوبة لكل بند.
+ - تحليل تكلفة تفصيلي يمكن استخدامه مباشرة في عروض الأسعار والمناقصات.
+ - ربط مباشر مع حاسبة تكاليف البناء وحاسبة التسعير.
+
+ **استخدامات الكتالوج:**
+
+ 1. استخدام البنود النموذجية مباشرة في التسعير.
+ 2. تعديل البنود النموذجية لتناسب متطلبات المشروع.
+ 3. إضافة بنود جديدة إلى الكتالوج للاستخدام المستقبلي.
+ """)
+
+ # الفئات وعرض محتوى الكتالوج
+ category_col, template_col = st.columns([1, 2])
+
+ with category_col:
+ st.markdown("### فئات البنود")
+
+ # الحصول على فئات البنود
+ templates = self.construction_templates.get_all_templates()
+ categories = templates['categories']
+
+ for cat_id, cat_data in categories.items():
+ st.markdown(f"#### {cat_data['name']}")
+ st.markdown(f"{cat_data['description']}")
+
+ if st.button(f"عرض بنود {cat_data['name']}", key=f"cat_btn_{cat_id}"):
+ st.session_state.selected_category = cat_id
+ st.rerun()
+
+ with template_col:
+ st.markdown("### البنود النموذجية")
+
+ selected_category = st.session_state.get("selected_category", list(categories.keys())[0])
+
+ # عرض البنود في الفئة المحددة
+ cat_templates = self.construction_templates.get_templates_by_category(selected_category)
+
+ if cat_templates:
+ st.markdown(f"#### بنود فئة {categories[selected_category]['name']}")
+
+ for template in cat_templates:
+ with st.expander(template['name'], expanded=False):
+ st.markdown(f"**الوصف**: {template['description']}")
+ st.markdown(f"**الوحدة**: {template['unit']}")
+
+ # عرض مكونات البند
+ st.markdown("##### مكونات البند")
+
+ # المواد
+ materials = template['components']['materials']
+ if materials:
+ materials_df = pd.DataFrame(materials)
+ st.markdown("**المواد:**")
+ st.dataframe(materials_df, hide_index=True)
+
+ # العمالة
+ labor = template['components']['labor']
+ if labor:
+ labor_df = pd.DataFrame(labor)
+ st.markdown("**العمالة:**")
+ st.dataframe(labor_df, hide_index=True)
+
+ # المعدات
+ equipment = template['components']['equipment']
+ if equipment:
+ equipment_df = pd.DataFrame(equipment)
+ st.markdown("**المعدات:**")
+ st.dataframe(equipment_df, hide_index=True)
+
+ # أزرار العمليات
+ col1, col2 = st.columns(2)
+
+ with col1:
+ if st.button("استخدام هذا البند في حاسبة التكاليف", key=f"use_template_{template['id']}"):
+ # تحويل البند إلى صيغة الحاسبة
+ construction_item = self.construction_templates.convert_template_to_item(template['id'])
+
+ # تحديث بيانات البند في session state
+ st.session_state.construction_item = construction_item
+ st.session_state.active_tab = 0 # الانتقال إلى تبويب حساب تكلفة البند
+ st.rerun()
+
+ with col2:
+ if st.button("إضافة مباشرة إلى جدول التسعير", key=f"add_to_pricing_{template['id']}"):
+ # تحويل البند إلى صيغة الحاسبة
+ construction_item = self.construction_templates.convert_template_to_item(template['id'])
+
+ # حساب التكلفة
+ item_cost = self.construction_calculator.calculate_item_cost(construction_item)
+
+ # إضافة البند إلى جدول التسعير
+ if 'manual_items' not in st.session_state:
+ st.session_state.manual_items = pd.DataFrame(columns=[
+ 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي'
+ ])
+
+ # إنشاء رقم البند الجديد
+ new_id = f"C{len(st.session_state.manual_items)+1}"
+
+ # إنشاء صف جديد
+ new_row = pd.DataFrame({
+ 'رقم البند': [new_id],
+ 'وصف البند': [item_cost['وصف_البند']],
+ 'الوحدة': [item_cost['الوحدة']],
+ 'الكمية': [float(item_cost['الكمية'])],
+ 'سعر الوحدة': [float(item_cost['السعر_المعدل']['سعر_الوحدة'])],
+ 'الإجمالي': [float(item_cost['السعر_المعدل']['إجمالي'])]
+ })
+
+ # إضافة الصف إلى DataFrame
+ st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True)
+
+ # إنشاء حالة التسعير الحالي إذا لم تكن موجودة
+ if 'current_pricing' not in st.session_state:
+ st.session_state.current_pricing = {
+ 'name': 'تسعير جديد',
+ 'method': 'تسعير مستورد من كتالوج المقاولات',
+ 'items': st.session_state.manual_items
+ }
+ else:
+ # تحديث البنود في التسعير الحالي
+ st.session_state.current_pricing['items'] = st.session_state.manual_items
+
+ st.success(f"تم إضافة البند \"{item_cost['وصف_البند']}\" إلى جدول التسعير بنجاح!")
+
+ # إضافة قسم لإضافة بند جديد إلى الكتالوج
+ st.markdown("### إضافة بند جديد إلى الكتالوج")
+
+ if st.button("إضافة البند الحالي إلى الكتالوج"):
+ if 'item_cost_result' in st.session_state:
+ # عرض نموذج لإضافة البند إلى الكتالوج
+ st.session_state.show_add_to_catalog = True
+ st.rerun()
+ else:
+ st.warning("يجب حساب تكلفة البند أولاً في تبويب 'حساب تكلفة بند' قبل إضافته إلى الكتالوج.")
+
+ # عرض نموذج إضافة البند إلى الكتالوج
+ if 'show_add_to_catalog' in st.session_state and st.session_state.show_add_to_catalog:
+ st.markdown("#### إضافة البند الحالي إلى كتالوج المقاولات")
+
+ # اختيار الفئة
+ category_options = [f"{cat_data['name']}" for cat_id, cat_data in categories.items()]
+ category_ids = list(categories.keys())
+
+ selected_category_index = st.selectbox(
+ "اختر فئة البند",
+ options=range(len(category_options)),
+ format_func=lambda i: category_options[i],
+ key="new_template_category"
+ )
+
+ selected_category_id = category_ids[selected_category_index]
+
+ # معلومات البند
+ item_result = st.session_state.item_cost_result
+
+ template_name = st.text_input("اسم البند في الكتالوج", value=item_result['وصف_البند'][:50])
+ template_description = st.text_area("وصف البند", value=item_result['وصف_البند'], key="template_item_description")
+
+ # الكلمات المفتاحية
+ tags_input = st.text_input("الكلمات المفتاحية (مفصولة بفواصل)", value="بناء, مقاولات")
+ tags = [tag.strip() for tag in tags_input.split(",")]
+
+ if st.button("إضافة إلى الكتالوج", type="primary"):
+ # تحويل البند إلى صيغة قالب
+ template_data = {
+ "category": selected_category_id,
+ "name": template_name,
+ "description": template_description,
+ "unit": item_result['الوحدة'],
+ "components": {
+ "materials": item_result['تكاليف_مباشرة']['المواد']['التفاصيل'],
+ "labor": item_result['تكاليف_مباشرة']['العمالة']['التفاصيل'],
+ "equipment": item_result['تكاليف_مباشرة']['المعدات']['التفاصيل']
+ },
+ "admin_expenses": item_result['مصاريف_إدارية']['نسبة'] / 100,
+ "profit_margin": item_result['هامش_ربح']['نسبة'] / 100,
+ "tags": tags
+ }
+
+ # إضافة القالب إلى الكتالوج
+ template_id = self.construction_templates.add_template(template_data)
+
+ st.success(f"تم إضافة البند \"{template_name}\" إلى كتالوج المقاولات بنجاح!")
+ st.session_state.show_add_to_catalog = False
+ st.rerun()
+
+ if st.button("إلغاء", key="cancel_add_to_catalog"):
+ st.session_state.show_add_to_catalog = False
+ st.rerun()
+
+ with ref_tabs[0]: # قائمة المواد
+ st.markdown("#### قائمة أسعار المواد المرجعية")
+
+ # الحصول على قائمة المواد
+ materials = self.construction_calculator.get_all_rates(item_type='مادة')
+
+ if materials and 'المواد' in materials:
+ # تحويل قاموس المواد إلى DataFrame
+ materials_list = []
+ for name, data in materials['المواد'].items():
+ materials_list.append({
+ 'اسم المادة': name,
+ 'الوحدة': data.get('وحدة', ''),
+ 'سعر الوحدة': data.get('سعر_الوحدة', 0.0),
+ 'الوصف': data.get('وصف', ''),
+ 'الفئة': data.get('فئة', '')
+ })
+
+ if materials_list:
+ materials_df = pd.DataFrame(materials_list)
+
+ # تصفية حسب الفئة
+ categories = ['الكل'] + sorted(materials_df['الفئة'].unique().tolist())
+ selected_category = st.selectbox("تصفية حسب الفئة", categories, key="materials_cat_filter_section2")
+
+ if selected_category != 'الكل':
+ filtered_df = materials_df[materials_df['الفئة'] == selected_category]
+ else:
+ filtered_df = materials_df
+
+ # تنسيق الجدول
+ def highlight_materials_row(row):
+ """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
+ color = '#F0F8FF' if row.name % 2 == 0 else 'white'
+ return ['background-color: %s' % color] * len(row)
+
+ styled_df = filtered_df.style.apply(highlight_materials_row, axis=1)
+ styled_df = styled_df.format({
+ 'سعر الوحدة': '{:,.2f}'
+ })
+
+ st.dataframe(styled_df, use_container_width=True, hide_index=True)
+ else:
+ st.info("لا توجد مواد في قاعدة البيانات")
+ else:
+ st.info("لا توجد قائمة مواد متاحة")
+
+ with ref_tabs[1]: # قائمة العمالة
+ st.markdown("#### قائمة أسعار العمالة المرجعية")
+
+ # الحصول على قائمة العمالة
+ labor = self.construction_calculator.get_all_rates(item_type='عمالة')
+
+ if labor and 'العمالة' in labor:
+ # تحويل قاموس العمالة إلى DataFrame
+ labor_list = []
+ for name, data in labor['العمالة'].items():
+ labor_list.append({
+ 'نوع العامل': name,
+ 'وحدة الأجر': data.get('وحدة', ''),
+ 'سعر الوحدة': data.get('سعر_الوحدة', 0.0),
+ 'الوصف': data.get('وصف', ''),
+ 'الفئة': data.get('فئة', '')
+ })
+
+ if labor_list:
+ labor_df = pd.DataFrame(labor_list)
+
+ # تصفية حسب الفئة
+ categories = ['الكل'] + sorted(labor_df['الفئة'].unique().tolist())
+ selected_category = st.selectbox("تصفية حسب الفئة", categories, key="labor_cat_filter_section2")
+
+ if selected_category != 'الكل':
+ filtered_df = labor_df[labor_df['الفئة'] == selected_category]
+ else:
+ filtered_df = labor_df
+
+ # تنسيق الجدول
+ def highlight_labor_row(row):
+ """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
+ color = '#F0F8FF' if row.name % 2 == 0 else 'white'
+ return ['background-color: %s' % color] * len(row)
+
+ styled_df = filtered_df.style.apply(highlight_labor_row, axis=1)
+ styled_df = styled_df.format({
+ 'سعر الوحدة': '{:,.2f}'
+ })
+
+ st.dataframe(styled_df, use_container_width=True, hide_index=True)
+ else:
+ st.info("لا توجد عمالة في قاعدة البيانات")
+ else:
+ st.info("لا توجد قائمة عمالة متاحة")
+
+ with ref_tabs[2]: # قائمة المعدات
+ st.markdown("#### قائمة أسعار المعدات المرجعية")
+
+ # الحصول على قائمة المعدات
+ equipment = self.construction_calculator.get_all_rates(item_type='معدة')
+
+ if equipment and 'المعدات' in equipment:
+ # تحويل قاموس المعدات إلى DataFrame
+ equipment_list = []
+ for name, data in equipment['المعدات'].items():
+ equipment_list.append({
+ 'نوع المعدة': name,
+ 'وحدة الأجر': data.get('وحدة', ''),
+ 'سعر الوحدة': data.get('سعر_الوحدة', 0.0),
+ 'الوصف': data.get('وصف', ''),
+ 'الفئة': data.get('فئة', '')
+ })
+
+ if equipment_list:
+ equipment_df = pd.DataFrame(equipment_list)
+
+ # تصفية حسب الفئة
+ categories = ['الكل'] + sorted(equipment_df['الفئة'].unique().tolist())
+ selected_category = st.selectbox("تصفية حسب الفئة", categories, key="equipment_cat_filter_section2")
+
+ if selected_category != 'الكل':
+ filtered_df = equipment_df[equipment_df['الفئة'] == selected_category]
+ else:
+ filtered_df = equipment_df
+
+ # تنسيق الجدول
+ def highlight_equipment_row(row):
+ """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
+ color = '#F0F8FF' if row.name % 2 == 0 else 'white'
+ return ['background-color: %s' % color] * len(row)
+
+ styled_df = filtered_df.style.apply(highlight_equipment_row, axis=1)
+ styled_df = styled_df.format({
+ 'سعر الوحدة': '{:,.2f}'
+ })
+
+ st.dataframe(styled_df, use_container_width=True, hide_index=True)
+ else:
+ st.info("لا توجد معدات في قاعدة البيانات")
+ else:
+ st.info("لا توجد قائمة معدات متاحة")
- def _render_cost_analysis_report(self):
- """عرض نموذج تقرير تحليل التكاليف"""
-
- st.markdown("### تقرير تحليل التكاليف")
- st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d"))
- st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري")
- st.markdown("**رقم المناقصة:** T-2024-001")
-
- st.markdown("#### تحليل التكاليف")
-
- # عرض تحليل التكاليف
- cost_df = pd.DataFrame(st.session_state.cost_analysis)
- st.dataframe(cost_df, use_container_width=True, hide_index=True)
-
- # عرض إجمالي التكاليف
- total_cost = sum(item['amount'] for item in st.session_state.cost_analysis)
- st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال")
-
- # عرض توزيع التكاليف حسب الفئة
- st.markdown("#### توزيع التكاليف حسب الفئة")
-
- # تجميع البيانات حسب الفئة
- category_totals = {}
+ def _render_local_content_tab(self):
+ """عرض تبويب المحتوى المحلي"""
- for item in st.session_state.cost_analysis:
- category = item['category']
- if category in category_totals:
- category_totals[category] += item['amount']
- else:
- category_totals[category] = item['amount']
-
- category_df = pd.DataFrame({
- 'الفئة': list(category_totals.keys()),
- 'المبلغ': list(category_totals.values())
- })
-
- fig = px.pie(
- category_df,
- values='المبلغ',
- names='الفئة',
- title='توزيع التكاليف حسب الفئة'
- )
+ st.markdown("
تحليل المحتوى المحلي
", unsafe_allow_html=True)
- st.plotly_chart(fig, use_container_width=True)
+ # التحقق من وجود تسعير حالي
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
+ return
- # عرض توزيع التكاليف المباشرة
- st.markdown("#### توزيع التكاليف المباشرة")
+ # شرح المحتوى المحلي
+ with st.expander("ما هو المحتوى المحلي؟", expanded=False):
+ st.markdown("""
+ **المحتوى المحلي** هو نسبة المنتجات والخدمات والقوى العاملة المحلية المستخدمة في المشروع. يهدف إلى زيادة مساهمة المنتجات والخدمات المحلية في المشاريع.
+
+ ### مكونات المحتوى المحلي:
+
+ 1. **المنتجات**: المنتجات والمواد المصنعة محلياً.
+ 2. **الخدمات**: الخدمات المقدمة من شركات محلية.
+ 3. **القوى العاملة**: العمالة والكوادر الفنية والإدارية المحلية.
+
+ ### أهمية المحتوى المحلي:
+
+ - تعزيز الاقتصاد المحلي وخلق فرص عمل.
+ - تحقيق أهداف رؤية 2030 في زيادة المحتوى المحلي.
+ - التأهل للمشاريع الحكومية التي تتطلب نسبة محتوى محلي محددة.
+ - الحصول على حوافز وأفضلية في المناقصات الحكومية.
+
+ ### متطلبات المحتوى المحلي:
+
+ - نسبة المحتوى المحلي للقوى العاملة: 80%
+ - نسبة المحتوى المحلي للمنتجات: 70%
+ - نسبة المحتوى المحلي للخدمات: 60%
+ """)
+
+ # عرض لوحة إدخال بيانات المحتوى المحلي
+ st.markdown("### بيانات المحتوى المحلي")
- # تجميع البيانات حسب الفئة الفرعية للتكاليف المباشرة
- direct_subcategory_totals = {}
+ # التبويبات لأنواع المحتوى المحلي
+ lc_tabs = st.tabs(["المنتجات", "الخدمات", "القوى العاملة", "التحليل"])
- for item in st.session_state.cost_analysis:
- if item['category'] == 'تكاليف مباشرة':
- subcategory = item['subcategory']
- if subcategory in direct_subcategory_totals:
- direct_subcategory_totals[subcategory] += item['amount']
+ with lc_tabs[0]: # المنتجات
+ st.markdown("#### بيانات المنتجات")
+
+ # إنشاء بيانات افتراضية للمنتجات إذا لم تكن موجودة
+ if 'local_content_products' not in st.session_state:
+ st.session_state.local_content_products = pd.DataFrame({
+ 'المنتج': [
+ "خرسانة مسلحة",
+ "حديد تسليح",
+ "بلوك خرساني",
+ "عزل مائي",
+ "دهانات"
+ ],
+ 'الكمية': [250, 25, 400, 500, 600],
+ 'سعر_الوحدة': [1200, 6000, 200, 100, 50],
+ 'التكلفة_الإجمالية': [300000, 150000, 80000, 50000, 30000],
+ 'نسبة_المحتوى_المحلي': [0.95, 0.70, 0.98, 0.60, 0.80]
+ })
+
+ # حساب التكلفة الإجمالية
+ st.session_state.local_content_products['التكلفة_الإجمالية'] = st.session_state.local_content_products['الكمية'] * st.session_state.local_content_products['سعر_الوحدة']
+
+ # نموذج إضافة منتج جديد
+ st.markdown("#### إضافة منتج جديد")
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ new_product_name = st.text_input("اسم المنتج", key="new_product_name", value="")
+ with col2:
+ new_product_quantity = st.number_input("الكمية", key="new_product_quantity", min_value=0, value=0)
+ with col3:
+ new_product_price = st.number_input("سعر الوحدة", key="new_product_price", min_value=0, value=0)
+ with col4:
+ new_product_local_content = st.slider("نسبة المحتوى المحلي", key="new_product_local_content", min_value=0.0, max_value=1.0, value=0.8, step=0.01, format="%.2f")
+
+ if st.button("إضافة المنتج"):
+ if new_product_name:
+ # حساب التكلفة الإجمالية
+ total_cost = new_product_quantity * new_product_price
+
+ # إضافة منتج جديد للجدول
+ new_product = pd.DataFrame({
+ 'المنتج': [new_product_name],
+ 'الكمية': [new_product_quantity],
+ 'سعر_الوحدة': [new_product_price],
+ 'التكلفة_الإجمالية': [total_cost],
+ 'نسبة_المحتوى_المحلي': [new_product_local_content]
+ })
+
+ # إضافة المنتج الجديد للجدول الحالي
+ st.session_state.local_content_products = pd.concat([st.session_state.local_content_products, new_product], ignore_index=True)
+ st.success(f"تم إضافة المنتج {new_product_name} بنجاح!")
else:
- direct_subcategory_totals[subcategory] = item['amount']
-
- direct_subcategory_df = pd.DataFrame({
- 'الفئة الفرعية': list(direct_subcategory_totals.keys()),
- 'المبلغ': list(direct_subcategory_totals.values())
- })
-
- fig = px.bar(
- direct_subcategory_df,
- x='الفئة الفرعية',
- y='المبلغ',
- title='توزيع التكاليف المباشرة',
- color='الفئة الفرعية',
- text_auto='.2s'
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- def _render_pricing_scenarios_report(self):
- """عرض نموذج تقرير سيناريوهات التسعير"""
-
- st.markdown("### تقرير سيناريوهات التسعير")
- st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d"))
- st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري")
- st.markdown("**رقم المناقصة:** T-2024-001")
-
- st.markdown("#### سيناريوهات التسعير")
-
- # عرض سيناريوهات التسعير
- scenarios_df = pd.DataFrame(st.session_state.price_scenarios)
- st.dataframe(scenarios_df, use_container_width=True, hide_index=True)
-
- # عرض السيناريو النشط
- active_scenario = next((item for item in st.session_state.price_scenarios if item['is_active']), None)
- if active_scenario:
- st.markdown(f"**السيناريو النشط:** {active_scenario['name']}")
- st.markdown(f"**السعر الإجمالي:** {active_scenario['total_price']:,.2f} ريال")
- st.markdown(f"**هامش الربح:** {active_scenario['profit_margin']:.1f}%")
-
- # عرض مقارنة السيناريوهات
- st.markdown("#### مقارنة السيناريوهات")
-
- # إنشاء DataFrame للرسم البياني
- scenarios_comparison_df = pd.DataFrame({
- 'السيناريو': [item['name'] for item in st.session_state.price_scenarios],
- 'التكلفة الإجمالية': [item['total_cost'] for item in st.session_state.price_scenarios],
- 'هامش الربح (%)': [item['profit_margin'] for item in st.session_state.price_scenarios],
- 'السعر الإجمالي': [item['total_price'] for item in st.session_state.price_scenarios],
- 'الحالة': ['نشط' if item['is_active'] else 'غير نشط' for item in st.session_state.price_scenarios]
- })
-
- # إنشاء رسم بياني شريطي مزدوج
- fig = go.Figure()
-
- # إضافة شريط للتكلفة الإجمالية
- fig.add_trace(go.Bar(
- x=scenarios_comparison_df['السيناريو'],
- y=scenarios_comparison_df['التكلفة الإجمالية'],
- name='التكلفة الإجمالية',
- marker_color='indianred'
- ))
-
- # إضافة شريط للسعر الإجمالي
- fig.add_trace(go.Bar(
- x=scenarios_comparison_df['السيناريو'],
- y=scenarios_comparison_df['السعر الإجمالي'],
- name='السعر الإجمالي',
- marker_color='lightsalmon'
- ))
-
- # إضافة خط لهامش الربح
- fig.add_trace(go.Scatter(
- x=scenarios_comparison_df['السيناريو'],
- y=scenarios_comparison_df['هامش الربح (%)'] * 10000, # تكبير القيم لتظهر على الرسم البياني
- name='هامش الربح (%)',
- yaxis='y2',
- line=dict(color='royalblue', width=4)
- ))
-
- # تعديل تخطيط الرسم البياني
- fig.update_layout(
- title='مقارنة سيناريوهات التسعير',
- xaxis_title='السيناريو',
- yaxis_title='المبلغ (ريال)',
- yaxis2=dict(
- title='هامش الربح (%)',
- titlefont=dict(color='royalblue'),
- tickfont=dict(color='royalblue'),
- overlaying='y',
- side='right',
- range=[0, 20]
- ),
- barmode='group',
- legend=dict(
- x=0,
- y=1.2,
- orientation='h'
+ st.warning("يرجى إدخال اسم المنتج")
+
+ # عرض جدول البنود مع إمكانية التعديل
+ st.markdown("#### جدول المنتجات")
+ edited_products = st.data_editor(
+ st.session_state.local_content_products,
+ use_container_width=True,
+ hide_index=True,
+ key="products_editor"
)
- )
-
- # تعديل النص على الأشرطة
- fig.update_traces(
- texttemplate='%{y:,.0f}',
- textposition='outside'
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- def _render_competitive_analysis_report(self):
- """عرض نموذج تقرير المقارنة التنافسية"""
-
- st.markdown("### تقرير المقارنة التنافسية")
- st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d"))
- st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري")
- st.markdown("**رقم المناقصة:** T-2024-001")
-
- # بيانات افتراضية للمنافسين
- competitors_data = [
- {
- 'name': 'شركتنا',
- 'price': 670000,
- 'quality': 4.5,
- 'delivery_time': 180,
- 'experience': 8,
- 'local_content': 85
- },
- {
- 'name': 'المنافس أ',
- 'price': 700000,
- 'quality': 4.2,
- 'delivery_time': 200,
- 'experience': 10,
- 'local_content': 75
- },
- {
- 'name': 'المنافس ب',
- 'price': 650000,
- 'quality': 3.8,
- 'delivery_time': 160,
- 'experience': 5,
- 'local_content': 90
- },
- {
- 'name': 'المنافس ج',
- 'price': 680000,
- 'quality': 4.0,
- 'delivery_time': 190,
- 'experience': 12,
- 'local_content': 80
- }
- ]
-
- # عرض بيانات المنافسين
- st.markdown("#### بيانات المنافسين")
-
- competitors_df = pd.DataFrame(competitors_data)
- st.dataframe(competitors_df, use_container_width=True, hide_index=True)
-
- # مقارنة الأسعار
- st.markdown("#### مقارنة الأسعار")
-
- fig = px.bar(
- competitors_df,
- x='name',
- y='price',
- title='مقارنة الأسعار بين المنافسين',
- color='price',
- text_auto='.2s'
- )
-
- fig.update_layout(
- xaxis_title='المنافس',
- yaxis_title='السعر (ريال)'
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # مقارنة متعددة الأبعاد
- st.markdown("#### مقارنة متعددة الأبعاد")
-
- # تحويل البيانات إلى تنسيق مناسب للرسم البياني الراداري
- categories = ['price', 'quality', 'delivery_time', 'experience', 'local_content']
+ st.session_state.local_content_products = edited_products
+
+ # زر لحذف المنتجات المحددة
+ if st.button("حذف المنتجات المحددة"):
+ st.session_state.local_content_products = pd.DataFrame({
+ 'المنتج': [],
+ 'الكمية': [],
+ 'سعر_الوحدة': [],
+ 'التكلفة_الإجمالية': [],
+ 'نسبة_المحتوى_المحلي': []
+ })
+ st.success("تم حذف جميع المنتجات!")
+ st.rerun()
+
+ # عرض ملخص المنتجات
+ total_products_cost = edited_products['التكلفة_الإجمالية'].sum()
+ avg_local_content = (edited_products['التكلفة_الإجمالية'] * edited_products['نسبة_المحتوى_المحلي']).sum() / total_products_cost if total_products_cost > 0 else 0
+
+ st.markdown(f"""
+ **إجمالي تكلفة المنتجات**: {total_products_cost:,.2f} ريال
+
+ **متوسط نسبة المحتوى المحلي للمنتجات**: {avg_local_content*100:.2f}%
+
+ **المستهدف**: 70%
+
+ **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.7 else "❌ غير ملتزم"}
+ """)
- # تطبيع البيانات (لجعل القيم بين 0 و 1)
- normalized_data = {}
+ with lc_tabs[1]: # الخدمات
+ st.markdown("#### بيانات الخدمات")
+
+ # إنشاء بيانات افتراضية للخدمات إذا لم تكن موجودة
+ if 'local_content_services' not in st.session_state:
+ st.session_state.local_content_services = pd.DataFrame({
+ 'الخدمة': [
+ "تصميم معماري",
+ "إشراف هندسي",
+ "خدمات نقل",
+ "خدمات أمن وسلامة",
+ "صيانة ونظافة"
+ ],
+ 'التكلفة': [100000, 120000, 50000, 30000, 20000],
+ 'نسبة_المحتوى_المحلي': [0.90, 0.85, 0.90, 0.95, 0.95]
+ })
+
+ # نموذج إضافة خدمة جديدة
+ st.markdown("#### إضافة خدمة جديدة")
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ new_service_name = st.text_input("اسم الخدمة", key="new_service_name", value="")
+ with col2:
+ new_service_cost = st.number_input("التكلفة", key="new_service_cost", min_value=0, value=0)
+ with col3:
+ new_service_local_content = st.slider("نسبة المحتوى المحلي", key="new_service_local_content", min_value=0.0, max_value=1.0, value=0.8, step=0.01, format="%.2f")
+
+ if st.button("إضافة الخدمة"):
+ if new_service_name:
+ # إضافة خدمة جديدة للجدول
+ new_service = pd.DataFrame({
+ 'الخدمة': [new_service_name],
+ 'التكلفة': [new_service_cost],
+ 'نسبة_المحتوى_المحلي': [new_service_local_content]
+ })
+
+ # إضافة الخدمة الجديدة للجدول الحالي
+ st.session_state.local_content_services = pd.concat([st.session_state.local_content_services, new_service], ignore_index=True)
+ st.success(f"تم إضافة الخدمة {new_service_name} بنجاح!")
+ else:
+ st.warning("يرجى إدخال اسم الخدمة")
+
+ # عرض جدول الخدمات مع إمكانية التعديل
+ st.markdown("#### جدول الخدمات")
+ edited_services = st.data_editor(
+ st.session_state.local_content_services,
+ use_container_width=True,
+ hide_index=True,
+ key="services_editor"
+ )
+ st.session_state.local_content_services = edited_services
+
+ # زر لحذف الخدمات المحددة
+ if st.button("حذف الخدمات المحددة"):
+ st.session_state.local_content_services = pd.DataFrame({
+ 'الخدمة': [],
+ 'التكلفة': [],
+ 'نسبة_المحتوى_المحلي': []
+ })
+ st.success("تم حذف جميع الخدمات!")
+ st.rerun()
+
+ # عرض ملخص الخدمات
+ total_services_cost = edited_services['التكلفة'].sum()
+ avg_local_content = (edited_services['التكلفة'] * edited_services['نسبة_المحتوى_المحلي']).sum() / total_services_cost if total_services_cost > 0 else 0
+
+ st.markdown(f"""
+ **إجمالي تكلفة الخدمات**: {total_services_cost:,.2f} ريال
+
+ **متوسط نسبة المحتوى المحلي للخدمات**: {avg_local_content*100:.2f}%
+
+ **المستهدف**: 60%
+
+ **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.6 else "❌ غير ملتزم"}
+ """)
- for category in categories:
- if category == 'price' or category == 'delivery_time':
- # للسعر ووقت التسليم، القيمة الأقل أفضل
- min_val = min(item[category] for item in competitors_data)
- max_val = max(item[category] for item in competitors_data)
- normalized_data[category] = [(max_val - item[category]) / (max_val - min_val) if max_val != min_val else 0.5 for item in competitors_data]
- else:
- # للجودة والخبرة والمحتوى المحلي، القيمة الأعلى أفضل
- min_val = min(item[category] for item in competitors_data)
- max_val = max(item[category] for item in competitors_data)
- normalized_data[category] = [(item[category] - min_val) / (max_val - min_val) if max_val != min_val else 0.5 for item in competitors_data]
-
- # إنشاء الرسم البياني الراداري
- fig = go.Figure()
-
- for i, competitor in enumerate(competitors_data):
- fig.add_trace(go.Scatterpolar(
- r=[normalized_data[category][i] for category in categories],
- theta=['السعر', 'الجودة', 'وقت التسليم', 'الخبرة', 'المحتوى المحلي'],
- fill='toself',
- name=competitor['name']
- ))
+ with lc_tabs[2]: # القوى العاملة
+ st.markdown("#### بيانات القوى العاملة")
+
+ # إنشاء بيانات افتراضية للقوى العاملة إذا لم تكن موجودة
+ if 'local_content_labor' not in st.session_state:
+ st.session_state.local_content_labor = pd.DataFrame({
+ 'فئة_العمالة': [
+ "مهندسون",
+ "فنيون",
+ "عمال بناء",
+ "إداريون",
+ "مشرفون"
+ ],
+ 'العدد': [5, 10, 30, 3, 4],
+ 'الراتب_الشهري': [15000, 8000, 3000, 10000, 12000],
+ 'المدة_بالأشهر': [12, 12, 12, 12, 12],
+ 'نسبة_المحتوى_المحلي': [0.75, 0.65, 0.60, 0.90, 0.80]
+ })
+
+ # حساب التكلفة الإجمالية
+ st.session_state.local_content_labor['التكلفة_الإجمالية'] = st.session_state.local_content_labor['العدد'] * st.session_state.local_content_labor['الراتب_الشهري'] * st.session_state.local_content_labor['المدة_بالأشهر']
+
+ # نموذج إضافة فئة عمالة جديدة
+ st.markdown("#### إضافة فئة عمالة جديدة")
+ col1, col2 = st.columns(2)
+
+ with col1:
+ new_labor_category = st.text_input("فئة العمالة", key="new_labor_category", value="")
+ new_labor_count = st.number_input("العدد", key="new_labor_count", min_value=0, value=0)
+
+ with col2:
+ new_labor_salary = st.number_input("الراتب الشهري", key="new_labor_salary", min_value=0, value=0)
+ new_labor_months = st.number_input("المدة بالأشهر", key="new_labor_months", min_value=1, value=12)
+
+ new_labor_local_content = st.slider("نسبة المحتوى المحلي", key="new_labor_local_content", min_value=0.0, max_value=1.0, value=0.8, step=0.01, format="%.2f")
+
+ if st.button("إضافة فئة العمالة"):
+ if new_labor_category:
+ # حساب التكلفة الإجمالية
+ total_cost = new_labor_count * new_labor_salary * new_labor_months
+
+ # إضافة فئة عمالة جديدة للجدول
+ new_labor = pd.DataFrame({
+ 'فئة_العمالة': [new_labor_category],
+ 'العدد': [new_labor_count],
+ 'الراتب_الشهري': [new_labor_salary],
+ 'المدة_بالأشهر': [new_labor_months],
+ 'نسبة_المحتوى_المحلي': [new_labor_local_content],
+ 'التكلفة_الإجمالية': [total_cost]
+ })
+
+ # إضافة فئة العمالة الجديدة للجدول الحالي
+ st.session_state.local_content_labor = pd.concat([st.session_state.local_content_labor, new_labor], ignore_index=True)
+ st.success(f"تم إضافة فئة العمالة {new_labor_category} بنجاح!")
+ else:
+ st.warning("يرجى إدخال اسم فئة العمالة")
+
+ # عرض جدول القوى العاملة مع إمكانية التعديل
+ st.markdown("#### جدول القوى العاملة")
+ edited_labor = st.data_editor(
+ st.session_state.local_content_labor,
+ use_container_width=True,
+ hide_index=True,
+ key="labor_editor"
+ )
+
+ # إعادة حساب التكلفة الإجمالية بعد التعديل
+ edited_labor['التكلفة_الإجمالية'] = edited_labor['العدد'] * edited_labor['الراتب_الشهري'] * edited_labor['المدة_بالأشهر']
+ st.session_state.local_content_labor = edited_labor
+
+ # زر لحذف فئات العمالة المحددة
+ if st.button("حذف فئات العمالة المحددة"):
+ st.session_state.local_content_labor = pd.DataFrame({
+ 'فئة_العمالة': [],
+ 'العدد': [],
+ 'الراتب_الشهري': [],
+ 'المدة_بالأشهر': [],
+ 'نسبة_المحتوى_المحلي': [],
+ 'التكلفة_الإجمالية': []
+ })
+ st.success("تم حذف جميع فئات العمالة!")
+ st.rerun()
+
+ # عرض ملخص القوى العاملة
+ total_labor_cost = edited_labor['التكلفة_الإجمالية'].sum()
+ avg_local_content = (edited_labor['التكلفة_الإجمالية'] * edited_labor['نسبة_المحتوى_المحلي']).sum() / total_labor_cost if total_labor_cost > 0 else 0
+
+ st.markdown(f"""
+ **إجمالي تكلفة القوى العاملة**: {total_labor_cost:,.2f} ريال
+
+ **متوسط نسبة المحتوى المحلي للقوى العاملة**: {avg_local_content*100:.2f}%
+
+ **المستهدف**: 80%
+
+ **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.8 else "❌ غير ملتزم"}
+ """)
- fig.update_layout(
- polar=dict(
- radialaxis=dict(
- visible=True,
- range=[0, 1]
+ with lc_tabs[3]: # التحليل
+ st.markdown("#### تحليل المحتوى المحلي")
+
+ # حساب المحتوى المحلي الإجمالي
+ try:
+ # تجميع بيانات تحليل المحتوى المحلي
+ products_cost = st.session_state.local_content_products['التكلفة_الإجمالية'].sum()
+ products_local_content = (st.session_state.local_content_products['التكلفة_الإجمالية'] * st.session_state.local_content_products['نسبة_المحتوى_المحلي']).sum() / products_cost if products_cost > 0 else 0
+
+ services_cost = st.session_state.local_content_services['التكلفة'].sum()
+ services_local_content = (st.session_state.local_content_services['التكلفة'] * st.session_state.local_content_services['نسبة_المحتوى_المحلي']).sum() / services_cost if services_cost > 0 else 0
+
+ labor_cost = st.session_state.local_content_labor['التكلفة_الإجمالية'].sum()
+ labor_local_content = (st.session_state.local_content_labor['التكلفة_الإجمالية'] * st.session_state.local_content_labor['نسبة_المحتوى_المحلي']).sum() / labor_cost if labor_cost > 0 else 0
+
+ # حساب الوزن النسبي لكل مكون
+ total_cost = products_cost + services_cost + labor_cost
+ products_weight = products_cost / total_cost if total_cost > 0 else 0
+ services_weight = services_cost / total_cost if total_cost > 0 else 0
+ labor_weight = labor_cost / total_cost if total_cost > 0 else 0
+
+ # حساب المحتوى المحلي الإجمالي
+ total_local_content = (products_local_content * products_weight) + (services_local_content * services_weight) + (labor_local_content * labor_weight)
+
+ # عرض ملخص المحتوى المحلي
+ st.markdown("### ملخص المحتوى المحلي")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال")
+
+ with col2:
+ st.metric("نسبة المحتوى المحلي الإجمالية", f"{total_local_content*100:.2f}%")
+
+ with col3:
+ target_local_content = 0.7 # 70%
+ st.metric("الحالة", "ملتزم" if total_local_content >= target_local_content else "غير ملتزم", delta=f"{(total_local_content - target_local_content)*100:.2f}%")
+
+ # عرض رسم بياني للمقارنة
+ st.markdown("### تحليل بصري للمحتوى المحلي")
+
+ # رسم بياني شريطي لنسب المحتوى المحلي
+ categories = ['المنتجات', 'الخدمات', 'القوى العاملة', 'الإجمالي']
+ actual_values = [products_local_content * 100, services_local_content * 100, labor_local_content * 100, total_local_content * 100]
+ target_values = [70, 60, 80, 70] # المستهدفات
+
+ # تهيئة البيانات للرسم البياني
+ chart_data = pd.DataFrame({
+ 'الفئة': categories,
+ 'النسبة الفعلية': actual_values,
+ 'النسبة المستهدفة': target_values
+ })
+
+ # رسم بياني شريطي للمقارنة
+ fig = go.Figure()
+
+ fig.add_trace(go.Bar(
+ x=chart_data['الفئة'],
+ y=chart_data['النسبة الفعلية'],
+ name='النسبة الفعلية',
+ marker_color='rgb(26, 118, 255)'
+ ))
+
+ fig.add_trace(go.Bar(
+ x=chart_data['الفئة'],
+ y=chart_data['النسبة المستهدفة'],
+ name='النسبة المستهدفة',
+ marker_color='rgb(55, 83, 109)'
+ ))
+
+ fig.update_layout(
+ title='مقارنة بين النسب الفعلية والمستهدفة للمحتوى المحلي',
+ xaxis_tickfont_size=14,
+ yaxis=dict(
+ title='النسبة %',
+ titlefont_size=16,
+ tickfont_size=14,
+ ),
+ legend=dict(
+ x=0,
+ y=1.0,
+ bgcolor='rgba(255, 255, 255, 0)',
+ bordercolor='rgba(255, 255, 255, 0)'
+ ),
+ barmode='group',
+ bargap=0.15,
+ bargroupgap=0.1
)
- ),
- title='مقارنة متعددة الأبعاد بين المنافسين',
- showlegend=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- def _render_pricing_summary_report(self):
- """عرض نموذج تقرير ملخص التسعير"""
-
- st.markdown("### تقرير ملخص التسعير")
- st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d"))
- st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري")
- st.markdown("**رقم المناقصة:** T-2024-001")
-
- # عرض ملخص التسعير
- st.markdown("#### ملخص التسعير")
-
- # حساب إجمالي التكاليف
- total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
- total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
- total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح')
- total_cost = total_direct_cost + total_indirect_cost
- total_price = total_cost + total_profit
-
- # إنشاء جدول ملخص
- summary_data = {
- 'البند': ['التكاليف المباشرة', 'التكاليف غير المباشرة', 'إجمالي التكاليف', 'هامش الربح', 'السعر الإجمالي'],
- 'المبلغ (ريال)': [total_direct_cost, total_indirect_cost, total_cost, total_profit, total_price],
- 'النسبة (%)': [
- (total_direct_cost / total_price) * 100,
- (total_indirect_cost / total_price) * 100,
- (total_cost / total_price) * 100,
- (total_profit / total_price) * 100,
- 100.0
- ]
- }
-
- summary_df = pd.DataFrame(summary_data)
- st.dataframe(summary_df, use_container_width=True, hide_index=True)
-
- # عرض توزيع التكاليف
- st.markdown("#### توزيع التكاليف")
-
- # إنشاء DataFrame للرسم البياني
- cost_distribution_df = pd.DataFrame({
- 'البند': ['التكاليف المباشرة', 'التكاليف غير المباشرة', 'هامش الربح'],
- 'المبلغ': [total_direct_cost, total_indirect_cost, total_profit]
- })
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض توصيات لتحسين نسبة المحتوى المحلي
+ st.markdown("### توصيات لتحسين نسبة المحتوى المحلي")
+
+ recommendations = []
+
+ if products_local_content < 0.7:
+ recommendations.append("- زيادة نسبة المحتوى المحلي للمنتجات من خلال:")
+ recommendations.append(" - البحث عن موردين محليين للمنتجات ذات النسبة المنخفضة")
+ recommendations.append(" - استبدال المنتجات المستوردة ببدائل محلية")
+ recommendations.append(" - التعاون مع المصانع المحلية لتوطين صناعة المنتجات")
+
+ if services_local_content < 0.6:
+ recommendations.append("- زيادة نسبة المحتوى المحلي للخدمات من خلال:")
+ recommendations.append(" - التعاقد مع شركات خدمات محلية")
+ recommendations.append(" - تحويل الخدمات المستعان بها من الخارج إلى شركات محلية")
+ recommendations.append(" - تأهيل الشركات المحلية لتقديم الخدمات المطلوبة")
+
+ if labor_local_content < 0.8:
+ recommendations.append("- زيادة نسبة المحتوى المحلي للقوى العاملة من خلال:")
+ recommendations.append(" - زيادة توظيف الكوادر المحلية")
+ recommendations.append(" - تدريب وتأهيل العمالة المحلية")
+ recommendations.append(" - استبدال العمالة الأجنبية بكوادر محلية تدريجياً")
+
+ if total_local_content < 0.7:
+ recommendations.append("- زيادة نسبة المحتوى المحلي الإجمالية من خلال:")
+ recommendations.append(" - إعادة توزيع الميزانية لصالح المكونات ذات النسبة العالية من المحتوى المحلي")
+ recommendations.append(" - وضع خطة مرحلية لزيادة المحتوى المحلي")
+ recommendations.append(" - التعاون مع اللجنة المحلية لزيادة المحتوى المحلي")
+
+ if recommendations:
+ for rec in recommendations:
+ st.markdown(rec)
+ else:
+ st.success("تهانينا! نسبة المحتوى المحلي متوافقة مع المتطلبات.")
+
+ # حساب تأثير المحتوى المحلي على التسعير
+ st.markdown("### تأثير المحتوى المحلي على التسعير")
+
+ # تحديد عامل تعديل السعر بناءً على نسبة المحتوى المحلي
+ price_adjustment_factor = 1.0
+
+ if total_local_content >= 0.9:
+ price_adjustment_factor = 0.92 # خصم 8% للمحتوى المحلي العالي جداً
+ price_discount = "8%"
+ elif total_local_content >= 0.8:
+ price_adjustment_factor = 0.94 # خصم 6% للمحتوى المحلي العالي
+ price_discount = "6%"
+ elif total_local_content >= 0.7:
+ price_adjustment_factor = 0.96 # خصم 4% للمحتوى المحلي المتوسط
+ price_discount = "4%"
+ elif total_local_content >= 0.6:
+ price_adjustment_factor = 0.98 # خصم 2% للمحتوى المحلي المنخفض
+ price_discount = "2%"
+ else:
+ price_adjustment_factor = 1.0 # لا خصم
+ price_discount = "0%"
+
+ # عرض تأثير المحتوى المحلي على التسعير
+ original_total = st.session_state.current_pricing['items']['الإجمالي'].sum()
+ adjusted_total = original_total * price_adjustment_factor
+ discount_amount = original_total - adjusted_total
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric("إجمالي التسعير الأصلي", f"{original_total:,.2f} ريال")
+
+ with col2:
+ st.metric("نسبة الخصم بسبب المحتوى المحلي", price_discount)
+
+ with col3:
+ st.metric("إجمالي التسعير بعد الخصم", f"{adjusted_total:,.2f} ريال", delta=f"-{discount_amount:,.2f} ريال")
+
+ # أزرار العمليات
+ col1, col2 = st.columns(2)
+
+ with col1:
+ if st.button("حفظ تحليل المحتوى المحلي"):
+ # حفظ بيانات المحتوى المحلي في التسعير الحالي
+ st.session_state.current_pricing['local_content'] = {
+ 'products': st.session_state.local_content_products.copy(),
+ 'services': st.session_state.local_content_services.copy(),
+ 'labor': st.session_state.local_content_labor.copy(),
+ 'total_local_content': total_local_content,
+ 'price_adjustment_factor': price_adjustment_factor
+ }
+
+ st.success("تم حفظ تحليل المحتوى المحلي بنجاح!")
+
+ with col2:
+ if st.button("تصدير تقرير المحتوى المحلي"):
+ st.success("تم تصدير تقرير المحتوى المحلي بنجاح!")
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء تحليل المحتوى المحلي: {str(e)}")
+ st.warning("تأكد من إدخال بيانات المحتوى المحلي بشكل صحيح في التبويبات السابقة.")
+
+ def _render_utilities_tab(self):
+ """عرض تبويب الأدوات المساعدة"""
+ import json
+ import copy
+ from datetime import datetime
+
+ st.markdown("## الأدوات المساعدة")
+
+ utilities_tab1, utilities_tab2, utilities_tab3, utilities_tab4, utilities_tab5 = st.tabs([
+ "الرسوم البيانية المتقدمة",
+ "استيراد/تصدير الإعدادات",
+ "النسخ الاحتياطي والاستعادة",
+ "مقارنة نماذج التسعير",
+ "إنشاء التقارير"
+ ])
- fig = px.pie(
- cost_distribution_df,
- values='المبلغ',
- names='البند',
- title='توزيع التكاليف والأرباح',
- color_discrete_sequence=px.colors.qualitative.Set3
- )
+ with utilities_tab1:
+ st.markdown("### الرسوم البيانية المتقدمة لتحليل التكلفة")
+
+ if 'item_cost_result' in st.session_state:
+ result = st.session_state.item_cost_result
+
+ # تبويب الرسوم البيانية
+ chart_tab1, chart_tab2, chart_tab3 = st.tabs(["توزيع التكلفة", "مقارنة المكونات", "تأثير العوامل"])
+
+ with chart_tab1:
+ # رسم بياني دائري لتوزيع التكلفة
+ fig = go.Figure(data=[go.Pie(
+ labels=["المواد", "العمالة", "المعدات", "المصاريف الإدارية", "هامش الربح"],
+ values=[
+ result['تكاليف_مباشرة']['المواد']['الإجمالي'],
+ result['تكاليف_مباشرة']['العمالة']['الإجمالي'],
+ result['تكاليف_مباشرة']['المعدات']['الإجمالي'],
+ result['مصاريف_إدارية']['قيمة'],
+ result['هامش_ربح']['قيمة']
+ ],
+ hole=.3,
+ marker_colors=['#36a2eb', '#ff6384', '#4bc0c0', '#ffcd56', '#9966ff']
+ )])
+ fig.update_layout(title_text="توزيع مكونات التكلفة")
+ st.plotly_chart(fig, use_container_width=True)
+
+ with chart_tab2:
+ # رسم بياني شريطي للمكونات الرئيسية
+ categories = ["المواد", "العمالة", "المعدات"]
+ values = [
+ result['تكاليف_مباشرة']['المواد']['الإجمالي'],
+ result['تكاليف_مباشرة']['العمالة']['الإجمالي'],
+ result['تكاليف_مباشرة']['المعدات']['الإجمالي']
+ ]
+
+ fig = go.Figure(data=[go.Bar(x=categories, y=values, marker_color=['#36a2eb', '#ff6384', '#4bc0c0'])])
+ fig.update_layout(title_text="مقارنة بين المكونات الرئيسية للتكلفة")
+ st.plotly_chart(fig, use_container_width=True)
+
+ with chart_tab3:
+ # مخطط شريطي يوضح تأثير عوامل التعديل على التكلفة
+ original_cost = result['التكلفة_الإجمالية']
+ final_cost = result['السعر_المعدل']['إجمالي']
+
+ # التحقق من وجود العوامل، وإضافتها بقيم افتراضية إذا كانت غير موجودة
+ if 'عوامل_التعديل' not in result:
+ result['عوامل_التعديل'] = {}
+
+ # إضافة المفاتيح الناقصة بقيم افتراضية
+ default_factors = {
+ 'location_factor': 1.0,
+ 'time_factor': 1.0,
+ 'risk_factor': 1.0,
+ 'market_factor': 1.0
+ }
+
+ for key, default_value in default_factors.items():
+ if key not in result['عوامل_التعديل']:
+ result['عوامل_التعديل'][key] = default_value
+
+ factors = {
+ "معامل الموقع": result['عوامل_التعديل']['location_factor'],
+ "معامل الوقت": result['عوامل_التعديل']['time_factor'],
+ "معامل المخاطر": result['عوامل_التعديل']['risk_factor'],
+ "معامل السوق": result['عوامل_التعديل']['market_factor']
+ }
+
+ # حساب القيمة المضافة من كل عامل
+ factor_effects = {}
+ for factor_name, factor_value in factors.items():
+ factor_effects[factor_name] = original_cost * (factor_value - 1)
+
+ fig = go.Figure()
+ fig.add_trace(go.Waterfall(
+ name="تأثير العوامل",
+ orientation="v",
+ measure=["absolute"] + ["relative"] * len(factor_effects) + ["total"],
+ x=["التكلفة الأصلية"] + list(factor_effects.keys()) + ["التكلفة النهائية"],
+ y=[original_cost] + list(factor_effects.values()) + [0],
+ text=[f"{original_cost:,.2f}"] + [f"{val:,.2f}" for val in factor_effects.values()] + [f"{final_cost:,.2f}"],
+ connector={"line": {"color": "rgb(63, 63, 63)"}},
+ ))
+
+ fig.update_layout(title_text="تأثير عوامل التعديل على التكلفة النهائية")
+ st.plotly_chart(fig, use_container_width=True)
+ else:
+ st.warning("لم يتم العثور على بيانات تحليل التكلفة. يرجى إجراء تحليل تكلفة في تبويب 'تحليل سعر البند' أولاً.")
- st.plotly_chart(fig, use_container_width=True)
+ with utilities_tab2:
+ st.markdown("### استيراد/تصدير إعدادات التسعير")
+
+ export_col, import_col = st.columns(2)
+
+ with export_col:
+ st.markdown("#### تصدير الإعدادات الحالية")
+ if st.button("تصدير إعدادات التسعير", key="export_pricing_settings"):
+ pricing_settings = {
+ "construction_item": st.session_state.construction_item if 'construction_item' in st.session_state else None,
+ "current_pricing": st.session_state.current_pricing if 'current_pricing' in st.session_state else None
+ }
+ settings_json = json.dumps(pricing_settings, ensure_ascii=False, indent=2)
+ st.download_button(
+ label="تنزيل إعدادات التسعير",
+ data=settings_json,
+ file_name="pricing_settings.json",
+ mime="application/json",
+ key="download_settings_button"
+ )
+
+ with import_col:
+ st.markdown("#### استيراد إعدادات سابقة")
+ uploaded_file = st.file_uploader("استيراد إعدادات تسعير سابقة", type=["json"], key="upload_pricing_settings")
+ if uploaded_file is not None:
+ try:
+ settings_data = json.loads(uploaded_file.getvalue().decode('utf-8'))
+ # تحديث بيانات التسعير في الجلسة
+ if 'construction_item' in settings_data and settings_data['construction_item']:
+ st.session_state.construction_item = settings_data['construction_item']
+ if 'current_pricing' in settings_data and settings_data['current_pricing']:
+ st.session_state.current_pricing = settings_data['current_pricing']
+ st.success("تم استيراد الإعدادات بنجاح!")
+ st.rerun()
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء استيراد الإعدادات: {str(e)}")
+
+ with utilities_tab3:
+ st.markdown("### النسخ الاحتياطي والاستعادة")
+ backup_tab1, backup_tab2 = st.tabs(["إنشاء نسخة احتياطية", "استعادة من نسخة احتياطية"])
+
+ with backup_tab1:
+ st.markdown("#### إنشاء نسخة احتياطية كاملة")
+ st.markdown("تقوم هذه العملية بإنشاء نسخة احتياطية كاملة لجميع بيانات التسعير الحالية.")
+
+ if st.button("إنشاء نسخة احتياطية كاملة", key="create_full_backup"):
+ backup_data = {
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "construction_item": st.session_state.construction_item if 'construction_item' in st.session_state else None,
+ "current_pricing": st.session_state.current_pricing if 'current_pricing' in st.session_state else None,
+ "pricing_models": st.session_state.pricing_models if 'pricing_models' in st.session_state else [],
+ "manual_items": st.session_state.manual_items.to_dict('records') if 'manual_items' in st.session_state else []
+ }
+
+ backup_json = json.dumps(backup_data, ensure_ascii=False, indent=2)
+
+ filename = f"wahbi_pricing_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
+ st.download_button(
+ label="تنزيل النسخة الاحتياطية",
+ data=backup_json,
+ file_name=filename,
+ mime="application/json",
+ key="download_backup_button"
+ )
+ st.success("تم إنشاء النسخة الاحتياطية بنجاح!")
+
+ with backup_tab2:
+ st.markdown("#### استعادة من نسخة احتياطية")
+ st.markdown("يمكنك استعادة بيانات التسعير من نسخة احتياطية سابقة.")
+
+ backup_file = st.file_uploader("استعادة من نسخة احتياطية", type=["json"], key="restore_backup_file")
+ if backup_file is not None:
+ if st.button("استعادة البيانات", key="restore_from_backup"):
+ try:
+ backup_data = json.loads(backup_file.getvalue().decode('utf-8'))
+
+ # استعادة البيانات إلى الحالة الحالية
+ if 'construction_item' in backup_data and backup_data['construction_item']:
+ st.session_state.construction_item = backup_data['construction_item']
+
+ if 'current_pricing' in backup_data and backup_data['current_pricing']:
+ st.session_state.current_pricing = backup_data['current_pricing']
+
+ if 'pricing_models' in backup_data:
+ st.session_state.pricing_models = backup_data['pricing_models']
+
+ if 'manual_items' in backup_data and backup_data['manual_items']:
+ st.session_state.manual_items = pd.DataFrame(backup_data['manual_items'])
+
+ st.success(f"تم استعادة البيانات من النسخة الاحتياطية بنجاح! (تاريخ النسخة: {backup_data.get('timestamp', 'غير معروف')})")
+ st.rerun()
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء استعادة البيانات: {str(e)}")
+
+ with utilities_tab4:
+ st.markdown("### مقارنة نماذج التسعير")
+ st.markdown("هذه الأداة تتيح لك مقارنة نماذج التسعير المختلفة واختيار الأفضل منها.")
+
+ # تهيئة قائمة نماذج التسعير إذا لم تكن موجودة
+ if 'pricing_models' not in st.session_state:
+ st.session_state.pricing_models = []
+
+ # إضافة النموذج الحالي للمقارنة
+ if st.button("إضافة النموذج الحالي للمقارنة", key="add_current_model"):
+ if 'current_pricing' in st.session_state and st.session_state.current_pricing is not None:
+ model_name = st.session_state.current_pricing.get('name', 'نموذج بدون اسم')
+ model_data = copy.deepcopy(st.session_state.current_pricing)
+ # تحقق من عدم وجود نموذج بنفس الاسم
+ exists = False
+ for model in st.session_state.pricing_models:
+ if model.get('name') == model_name:
+ exists = True
+ break
+
+ if not exists:
+ st.session_state.pricing_models.append(model_data)
+ st.success(f"تم إضافة نموذج '{model_name}' للمقارنة!")
+ else:
+ st.warning("يوجد نموذج بنفس الاسم في المقارنة بالفعل!")
+ else:
+ st.error("لا يوجد نموذج تسعير حالي للإضافة. يرجى إنشاء تسعير جديد أولاً.")
+
+ # عرض جدول المقارنة إذا كان هناك نماذج مضافة
+ if len(st.session_state.pricing_models) > 0:
+ st.markdown("### جدول مقارنة نماذج التسعير")
+
+ comparison_data = []
+ for model in st.session_state.pricing_models:
+ # حساب إجمالي التكلفة
+ items_df = pd.DataFrame(model.get('items', {}))
+ total_price = 0
+ if not items_df.empty and 'الإجمالي' in items_df.columns:
+ total_price = items_df['الإجمالي'].sum()
+
+ comparison_data.append({
+ "اسم النموذج": model.get('name', 'غير معروف'),
+ "طريقة التسعير": model.get('method', 'غير معروفة'),
+ "إجمالي التكلفة": f"{total_price:,.2f} ريال",
+ "عدد البنود": len(items_df) if not items_df.empty else 0,
+ })
+
+ comparison_df = pd.DataFrame(comparison_data)
+ st.dataframe(comparison_df, use_container_width=True, hide_index=True)
+
+ # عرض رسم بياني للمقارنة
+ if len(comparison_data) > 1:
+ st.markdown("### رسم بياني للمقارنة")
+
+ # استخراج البيانات للرسم البياني
+ models = [data["اسم النموذج"] for data in comparison_data]
+ values = [float(data["إجمالي التكلفة"].replace("ريال", "").replace(",", "")) for data in comparison_data]
+
+ # رسم بياني شريطي
+ fig = go.Figure(data=[
+ go.Bar(x=models, y=values, marker_color='rgb(26, 118, 255)')
+ ])
+
+ fig.update_layout(
+ title="مقارنة التكلفة الإجمالية بين نماذج التسعير",
+ xaxis_title="نموذج التسعير",
+ yaxis_title="التكلفة الإجمالية (ريال)"
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # أزرار إدارة النماذج
+ col1, col2 = st.columns(2)
+ with col1:
+ if st.button("حذف جميع النماذج", key="clear_comparison"):
+ st.session_state.pricing_models = []
+ st.success("تم مسح جميع نماذج المقارنة!")
+ st.rerun()
+
+ with col2:
+ # تحديد نموذج للحذف
+ model_to_delete = st.selectbox(
+ "اختر نموذج للحذف من المقارنة",
+ options=[model.get('name', f"نموذج {i+1}") for i, model in enumerate(st.session_state.pricing_models)],
+ key="model_to_delete"
+ )
+
+ if st.button("حذف النموذج المحدد", key="delete_selected_model"):
+ for i, model in enumerate(st.session_state.pricing_models):
+ if model.get('name') == model_to_delete:
+ st.session_state.pricing_models.pop(i)
+ st.warning(f"تم حذف النموذج '{model_to_delete}'")
+ st.rerun()
+ break
+ else:
+ st.info("لا توجد نماذج للمقارنة حالياً. يرجى إضافة النموذج الحالي للمقارنة أولاً.")
- # عرض ملخص السيناريو النشط
- st.markdown("#### السيناريو النشط")
-
- active_scenario = next((item for item in st.session_state.price_scenarios if item['is_active']), None)
- if active_scenario:
- st.markdown(f"**اسم السيناريو:** {active_scenario['name']}")
- st.markdown(f"**الوصف:** {active_scenario['description']}")
- st.markdown(f"**إجمالي التكلفة:** {active_scenario['total_cost']:,.2f} ريال")
- st.markdown(f"**هامش الربح:** {active_scenario['profit_margin']:.1f}%")
- st.markdown(f"**السعر الإجمالي:** {active_scenario['total_price']:,.2f} ريال")
-
- # عرض توصيات التسعير
- st.markdown("#### توصيات التسعير")
-
- st.markdown("- مراجعة هيكل التكاليف بشكل دوري للحفاظ على القدرة التنافسية.")
- st.markdown("- التأكيد على جودة الخدمات في العروض التسويقية.")
- st.markdown("- التأكيد على نسبة المحتوى المحلي العالية في العروض.")
- st.markdown("- مراقبة أسعار المنافسين وتعديل الاستراتيجية التسعيرية عند الحاجة.")
- st.markdown("- تحليل نقاط القوة والضعف بشكل مستمر لتحسين العروض المستقبلية.")
+ with utilities_tab5:
+ st.markdown("### إنشاء تقارير التسعير")
+ st.markdown("يمكنك استخدام هذه الأداة لإنشاء تقارير مفصلة عن التسعير.")
+
+ report_type = st.selectbox(
+ "اختر نوع التقرير",
+ options=["تقرير ملخص", "تقرير تفصيلي", "تقرير المقارنة"],
+ key="report_type_selector"
+ )
+
+ if st.button("إنشاء التقرير", key="generate_report_button"):
+ if report_type == "تقرير ملخص" and 'current_pricing' in st.session_state and st.session_state.current_pricing is not None:
+ # إنشاء تقرير ملخص
+ if isinstance(st.session_state.current_pricing.get('items'), pd.DataFrame):
+ df = st.session_state.current_pricing['items'].copy()
+
+ # حساب الإجماليات
+ total_price = df['الإجمالي'].sum() if 'الإجمالي' in df.columns else 0
+
+ # تقدير تكاليف المكونات
+ materials_cost = total_price * 0.6 # تقدير تقريبي للمواد
+ labor_cost = total_price * 0.25 # تقدير تقريبي للعمالة
+ equipment_cost = total_price * 0.15 # تقدير تقريبي للمعدات
+ admin_cost = total_price * 0.05 # تقدير تقريبي للمصاريف الإدارية
+ profit_margin = total_price * 0.1 # تقدير تقريبي لهامش الربح
+ final_total = total_price * 1.15 # إجمالي بعد إضافة المصاريف الإدارية وهامش الربح
+
+ # إنشاء جدول الملخص
+ summary = pd.DataFrame({
+ "بند التكلفة": ["إجمالي المواد", "إجمالي الأجور", "إجمالي المعدات", "المصاريف الإدارية", "هامش الربح", "الإجمالي النهائي"],
+ "القيمة": [
+ materials_cost,
+ labor_cost,
+ equipment_cost,
+ admin_cost,
+ profit_margin,
+ final_total
+ ]
+ })
+
+ # تنسيق التقرير
+ styled_df = summary.style.format({
+ "القيمة": "{:,.2f} ريال"
+ })
+
+ # تحويل الجدول إلى HTML
+ html = f"""
+
+
+
+ تقرير ملخص التسعير
+
+
+
+
+
تقرير ملخص التسعير
+
{st.session_state.current_pricing.get('name', 'تسعير بدون اسم')}
+
تاريخ التقرير: {datetime.now().strftime('%Y-%m-%d %H:%M')}
+
+
+
ملخص التكاليف
+ {styled_df.to_html()}
+
+
البيانات الأساسية
+
+
عدد البنود: {len(df)}
+
طريقة التسعير: {st.session_state.current_pricing.get('method', 'غير محددة')}
+
+
+
+
+
+ """
+
+ # تقديم زر التنزيل
+ st.download_button(
+ label="تنزيل التقرير الملخص",
+ data=html,
+ file_name="pricing_summary_report.html",
+ mime="text/html",
+ key="download_summary_report"
+ )
+
+ st.success("تم إنشاء التقرير الملخص بنجاح!")
+ else:
+ st.error("تعذر قراءة بيانات التسعير الحالي. يرجى التأكد من وجود تسعير صالح.")
+
+ elif report_type == "تقرير تفصيلي" and 'current_pricing' in st.session_state and st.session_state.current_pricing is not None:
+ # سيتم تنفيذ التقرير التفصيلي
+ st.info("جاري إعداد التقرير التفصيلي...")
+ # هنا يمكن تنفيذ كود إنشاء التقرير التفصيلي
+ st.success("تم إنشاء التقرير التفصيلي. سيتم تطوير هذه الميزة قريباً.")
+
+ elif report_type == "تقرير المقارنة" and 'pricing_models' in st.session_state and len(st.session_state.pricing_models) > 0:
+ # سيتم تنفيذ تقرير المقارنة
+ st.info("جاري إعداد تقرير المقارنة...")
+ # هنا يمكن تنفيذ كود إنشاء تقرير المقارنة
+ st.success("تم إنشاء تقرير المقارنة. سيتم تطوير هذه الميزة قريباً.")
+
+ else:
+ st.error("لا توجد بيانات كافية لإنشاء التقرير المطلوب. يرجى التأكد من وجود تسعير أو نماذج مقارنة.")
\ No newline at end of file
diff --git a/modules/pricing/pricing_app.py.backup b/modules/pricing/pricing_app.py.backup
new file mode 100644
index 0000000000000000000000000000000000000000..a3a67844c9b1aebbe11cb78b097f39389a221963
--- /dev/null
+++ b/modules/pricing/pricing_app.py.backup
@@ -0,0 +1,1242 @@
+"""
+تطبيق وحدة التسعير المتكاملة
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import plotly.express as px
+import plotly.graph_objects as go
+from datetime import datetime
+import random
+import os
+import time
+import io
+
+from modules.pricing.services.standard_pricing import StandardPricing
+from modules.pricing.services.unbalanced_pricing import UnbalancedPricing
+from modules.pricing.services.local_content_calculator import LocalContentCalculator
+from modules.pricing.services.price_prediction import PricePrediction
+from utils.excel_handler import export_to_excel
+from utils.helpers import format_number, format_currency
+
+
+class PricingApp:
+ """وحدة التسعير المتكاملة"""
+
+ def __init__(self):
+ """تهيئة وحدة التسعير المتكاملة"""
+ self.pricing_methods = [
+ "التسعير القياسي",
+ "التسعير غير المتزن",
+ "التسعير التنافسي",
+ "التسعير الموجه بالربحية"
+ ]
+
+ # تهيئة خدمات التسعير
+ self.standard_pricing = StandardPricing()
+ self.unbalanced_pricing = UnbalancedPricing()
+ self.local_content = LocalContentCalculator()
+ self.price_prediction = PricePrediction()
+
+ def render(self):
+ """عرض واجهة وحدة التسعير"""
+
+ st.markdown("
وحدة التسعير المتكاملة
", unsafe_allow_html=True)
+
+ tabs = st.tabs([
+ "إنشاء تسعير جديد",
+ "تحليل سعر البند",
+ "نموذج التسعير الشامل",
+ "التسعير غير المتزن",
+ "المحتوى المحلي"
+ ])
+
+ with tabs[0]:
+ self._render_new_pricing_tab()
+
+ with tabs[1]:
+ self._render_item_analysis_tab()
+
+ with tabs[2]:
+ self._render_comprehensive_pricing_tab()
+
+ with tabs[3]:
+ self._render_unbalanced_pricing_tab()
+
+ with tabs[4]:
+ self._render_local_content_tab()
+
+ def _render_item_analysis_tab(self):
+ """عرض تبويب تحليل سعر البند"""
+
+ st.markdown("### تحليل سعر البند")
+
+ # التحقق من وجود تسعير حالي
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
+ return
+
+ # اختيار البند للتحليل
+ if 'current_pricing' in st.session_state and st.session_state.current_pricing is not None:
+ items = st.session_state.current_pricing['items']
+ item_options = items['رقم البند'].tolist()
+ selected_item = st.selectbox("اختر البند للتحليل", item_options)
+
+ if selected_item:
+ item_data = items[items['رقم البند'] == selected_item].iloc[0]
+
+ st.markdown(f"### تحليل البند: {selected_item}")
+ st.markdown(f"**وصف البند**: {item_data['وصف البند']}")
+ st.markdown(f"**الوحدة**: {item_data['الوحدة']}")
+ st.markdown(f"**الكمية**: {item_data['الكمية']}")
+ st.markdown(f"**سعر الوحدة**: {item_data['سعر الوحدة']:,.2f} ريال")
+
+ # تحليل مكونات السعر
+ st.markdown("### تحليل مكونات السعر")
+
+ # عناصر التكلفة الافتراضية
+ cost_components = {
+ 'المواد': 0.6, # 60% من التكلفة
+ 'العمالة': 0.25, # 25% من التكلفة
+ 'المعدات': 0.1, # 10% من التكلفة
+ 'نفقات عامة': 0.05 # 5% من التكلفة
+ }
+
+ # حساب تكلفة كل عنصر
+ unit_price = item_data['سعر الوحدة']
+ component_values = {k: v * unit_price for k, v in cost_components.items()}
+
+ # عرض مكونات التكلفة في جدول
+ components_df = pd.DataFrame({
+ 'العنصر': component_values.keys(),
+ 'نسبة من التكلفة': [f"{v*100:.1f}%" for v in cost_components.values()],
+ 'القيمة (ريال)': [f"{v:,.2f}" for v in component_values.values()]
+ })
+
+ st.table(components_df)
+
+ # رسم بياني لمكونات التكلفة
+ fig = px.pie(
+ names=list(component_values.keys()),
+ values=list(component_values.values()),
+ title='توزيع مكونات التكلفة'
+ )
+
+ st.plotly_chart(fig)
+
+ # تحليل تاريخي للأسعار
+ st.markdown("### تحليل تاريخي للأسعار")
+
+ # بيانات تاريخية افتراضية
+ historical_data = {
+ 'التاريخ': ['2020-01', '2020-07', '2021-01', '2021-07', '2022-01', '2022-07', '2023-01', '2023-07'],
+ 'السعر': [
+ unit_price * 0.7,
+ unit_price * 0.75,
+ unit_price * 0.8,
+ unit_price * 0.85,
+ unit_price * 0.9,
+ unit_price * 0.95,
+ unit_price,
+ unit_price * 1.05
+ ]
+ }
+
+ hist_df = pd.DataFrame(historical_data)
+
+ # رسم بياني للتحليل التاريخي
+ fig = px.line(
+ hist_df,
+ x='التاريخ',
+ y='السعر',
+ title='تطور سعر الوحدة عبر الزمن',
+ markers=True
+ )
+
+ st.plotly_chart(fig)
+
+ # المقارنة مع الأسعار المرجعية
+ st.markdown("### المقارنة مع الأسعار المرجعية")
+
+ # بيانات مرجعية افتراضية
+ reference_data = {
+ 'المصدر': ['قاعدة البيانات الداخلية', 'دليل الأسعار الاسترشادي', 'متوسط أسعار السوق', 'أسعار المشاريع المماثلة'],
+ 'السعر المرجعي': [
+ unit_price * 0.95,
+ unit_price * 1.05,
+ unit_price * 1.1,
+ unit_price * 0.9
+ ]
+ }
+
+ ref_df = pd.DataFrame(reference_data)
+ ref_df['الفرق عن السعر الحالي'] = ref_df['السعر المرجعي'] - unit_price
+ ref_df['نسبة الفرق'] = (ref_df['الفرق عن السعر الحالي'] / unit_price * 100).round(2).astype(str) + '%'
+
+ st.table(ref_df)
+
+ def _render_new_pricing_tab(self):
+ """عرض تبويب إنشاء تسعير جديد"""
+
+ st.markdown("### إنشاء تسعير جديد")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ tender_name = st.text_input("اسم المناقصة")
+ client = st.text_input("الجهة المالكة")
+ pricing_method = st.selectbox("طريقة التسعير", self.pricing_methods)
+
+ with col2:
+ tender_number = st.text_input("رقم المناقصة")
+ location = st.text_input("الموقع")
+ submission_date = st.date_input("تاريخ التقديم")
+
+ # خيارات بيانات البنود
+ st.markdown("### بيانات البنود")
+
+ data_source = st.radio(
+ "مصدر بيانات البنود",
+ ["إدخال يدوي", "استيراد من Excel", "استيراد من وحدة تحليل المستندات"]
+ )
+
+ if data_source == "إدخال يدوي":
+ # ضبط CSS لتحسين ظهور الواجهة العربية
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+ # تهيئة قائمة الوحدات المتاحة
+ unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"]
+
+ # إنشاء بيانات افتراضية إذا لم تكن موجودة
+ if 'manual_items' not in st.session_state:
+ # إنشاء DataFrame فارغ
+ manual_items = pd.DataFrame(columns=[
+ 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي'
+ ])
+
+ # إضافة بضعة صفوف افتراضية
+ default_items = pd.DataFrame({
+ 'رقم البند': ["A1", "A2", "A3", "A4", "A5"],
+ 'وصف البند': [
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
+ "توريد وتركيب حديد التسليح للأساسات",
+ "أعمال العزل المائي للأساسات",
+ "أعمال الردم والدك للأساسات",
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة"
+ ],
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3"],
+ 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0],
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0],
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0]
+ })
+
+ manual_items = pd.concat([manual_items, default_items])
+ st.session_state.manual_items = manual_items
+
+ # عرض واجهة إدخال البنود
+ st.markdown("### إدخال تفاصيل البنود")
+
+ # التحقق من استخدام طريقة الإدخال البسيطة
+ use_simple_input = st.checkbox("استخدام طريقة الإدخال البسيطة", value=True)
+
+ if use_simple_input:
+ # عرض البنود الحالية كجدول للعرض فقط
+ st.markdown("### جدول البنود الحالية")
+ st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True)
+
+ # إضافة بند جديد
+ st.markdown("### إضافة بند جديد")
+ col1, col2 = st.columns(2)
+
+ with col1:
+ new_id = st.text_input("رقم البند", value=f"A{len(st.session_state.manual_items)+1}")
+ new_desc = st.text_area("وصف البند", value="")
+
+ with col2:
+ new_unit = st.selectbox("الوحدة", options=unit_options)
+ new_qty = st.number_input("الكمية", value=0.0, min_value=0.0, format="%.2f")
+ new_price = st.number_input("سعر الوحدة", value=0.0, min_value=0.0, format="%.2f")
+
+ new_total = new_qty * new_price
+ st.info(f"إجمالي البند الجديد: {new_total:,.2f} ريال")
+
+ if st.button("إضافة البند"):
+ # التحقق من صحة البيانات
+ if new_id and new_desc and new_qty > 0:
+ # إنشاء صف جديد
+ new_row = pd.DataFrame({
+ 'رقم البند': [new_id],
+ 'وصف البند': [new_desc],
+ 'الوحدة': [new_unit],
+ 'الكمية': [float(new_qty)],
+ 'سعر الوحدة': [float(new_price)],
+ 'الإجمالي': [float(new_total)]
+ })
+
+ # إضافة الصف إلى DataFrame
+ st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True)
+ st.success("تم إضافة البند بنجاح!")
+ st.rerun()
+ else:
+ st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.")
+
+ # تعديل البنود الحالية
+ st.markdown("### تعديل البنود الحالية")
+
+ # تحديد البند المراد تعديله
+ item_to_edit = st.selectbox(
+ "اختر البند للتعديل",
+ options=st.session_state.manual_items['رقم البند'].tolist(),
+ format_func=lambda x: f"{x}: {st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == x]['وصف البند'].values[0][:30]}..."
+ )
+
+ if item_to_edit:
+ # الحصول على مؤشر الصف للبند المحدد
+ idx = st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == item_to_edit].index[0]
+ row = st.session_state.manual_items.loc[idx]
+
+ # إنشاء نموذج تعديل
+ col1, col2 = st.columns(2)
+
+ with col1:
+ edited_id = st.text_input("رقم البند (تعديل)", value=row['رقم البند'], key="edit_id")
+ edited_desc = st.text_area("وصف البند (تعديل)", value=row['وصف البند'], key="edit_desc")
+
+ with col2:
+ edited_unit = st.selectbox(
+ "الوحدة (تعديل)",
+ options=unit_options,
+ index=unit_options.index(row['الوحدة']) if row['الوحدة'] in unit_options else 0,
+ key="edit_unit"
+ )
+ edited_qty = st.number_input("الكمية (تعديل)", value=float(row['الكمية']), min_value=0.0, format="%.2f", key="edit_qty")
+ edited_price = st.number_input("سعر الوحدة (تعديل)", value=float(row['سعر الوحدة']), min_value=0.0, format="%.2f", key="edit_price")
+
+ edited_total = edited_qty * edited_price
+ st.info(f"إجمالي البند بعد التعديل: {edited_total:,.2f} ريال")
+
+ col1, col2 = st.columns(2)
+ with col1:
+ if st.button("حفظ التعديلات"):
+ # تحديث البند
+ st.session_state.manual_items.at[idx, 'رقم البند'] = edited_id
+ st.session_state.manual_items.at[idx, 'وصف البند'] = edited_desc
+ st.session_state.manual_items.at[idx, 'الوحدة'] = edited_unit
+ st.session_state.manual_items.at[idx, 'الكمية'] = edited_qty
+ st.session_state.manual_items.at[idx, 'سعر الوحدة'] = edited_price
+ st.session_state.manual_items.at[idx, 'الإجمالي'] = edited_total
+
+ st.success("تم تحديث البند بنجاح!")
+ st.rerun()
+
+ with col2:
+ if st.button("حذف هذا البند"):
+ st.session_state.manual_items = st.session_state.manual_items.drop(idx).reset_index(drop=True)
+ st.warning("تم حذف البند!")
+ st.rerun()
+
+ # المجموع الكلي
+ total = st.session_state.manual_items['الإجمالي'].sum()
+ st.metric("المجموع الكلي", f"{total:,.2f} ريال")
+
+ # جعل هذه البيانات متاحة للاستخدام في الخطوات التالية
+ edited_items = st.session_state.manual_items.copy()
+
+ else:
+ # عرض رسالة توضح أن طريقة الإدخال البسيطة هي الأفضل
+ st.warning("لتجنب مشاكل عدم التوافق في أنواع البيانات، يُفضل استخدام طريقة الإدخال البسيطة.")
+
+ # محاولة استخدام المحرر القياسي مع معالجة الأخطاء
+ try:
+ # تحويل البيانات إلى الأنواع المناسبة
+ for col in st.session_state.manual_items.columns:
+ if col in ['رقم البند', 'وصف البند', 'الوحدة']:
+ st.session_state.manual_items[col] = st.session_state.manual_items[col].astype(str)
+
+ # عرض المحرر (للقراءة فقط)
+ st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True)
+
+ # إنشاء نظام تعديل منفصل
+ st.markdown("### تعديل أسعار الوحدات")
+
+ for idx, row in st.session_state.manual_items.iterrows():
+ col1, col2 = st.columns([3, 1])
+
+ with col1:
+ st.text(f"{row['رقم البند']}: {row['وصف البند'][:50]}")
+
+ with col2:
+ price = st.number_input(
+ f"سعر الوحدة ({row['الوحدة']})",
+ value=float(row['سعر الوحدة']),
+ min_value=0.0,
+ key=f"price_{idx}"
+ )
+
+ # تحديث السعر والإجمالي
+ st.session_state.manual_items.at[idx, 'سعر الوحدة'] = price
+ st.session_state.manual_items.at[idx, 'الإجمالي'] = price * row['الكمية']
+
+ # المجموع الكلي
+ total = st.session_state.manual_items['الإجمالي'].sum()
+ st.metric("المجموع الكلي", f"{total:,.2f} ريال")
+
+ # جعل هذه البيانات متاحة للاستخدام في الخطوات التالية
+ edited_items = st.session_state.manual_items.copy()
+
+ except Exception as e:
+ st.error(f"حدث خطأ: {str(e)}")
+ st.info("يرجى استخدام طريقة الإدخال البسيطة لتجنب هذه المشكلة.")
+
+ elif data_source == "استيراد من Excel":
+ uploaded_file = st.file_uploader("رفع ملف Excel", type=["xlsx", "xls"])
+
+ if uploaded_file is not None:
+ st.success("تم رفع الملف بنجاح")
+ # محاكاة قراءة الملف
+ st.markdown("### معاينة البيانات المستوردة")
+
+ # إنشاء بيانات افتراضية
+ import_items = pd.DataFrame({
+ 'رقم البند': ["A1", "A2", "A3", "A4", "A5", "A6", "A7"],
+ 'وصف البند': [
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
+ "توريد وتركيب حديد التسليح للأساسات",
+ "أعمال العزل المائي للأساسات",
+ "أعمال الردم والدك للأساسات",
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة",
+ "توريد وتركيب حديد التسليح للأعمدة",
+ "أعمال البلوك للجدران"
+ ],
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"],
+ 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0, 10.0, 400.0],
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+ })
+
+ st.dataframe(import_items)
+
+ if st.button("استيراد البيانات"):
+ st.session_state.manual_items = import_items.copy()
+ st.session_state.manual_items_modified = True
+ st.success("تم استيراد البيانات بنجاح!")
+ st.rerun()
+
+ else: # استيراد من وحدة تحليل المستندات
+ available_documents = [
+ "كراسة شروط مشروع توسعة مستشفى الملك فهد",
+ "جدول كميات صيانة محطات المياه",
+ "مخططات إنشاء مدرسة ثانوية"
+ ]
+
+ selected_doc = st.selectbox("اختر المستند", available_documents)
+
+ if st.button("استيراد البيانات من تحليل المستند"):
+ # محاكاة استيراد البيانات
+ with st.spinner("جاري استيراد البيانات..."):
+ time.sleep(2)
+
+ # إنشاء بيانات افتراضية
+ doc_items = pd.DataFrame({
+ 'رقم البند': ["A1", "A2", "A3", "A4", "A5", "A6", "A7"],
+ 'وصف البند': [
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
+ "توريد وتركيب حديد التسليح للأساسات",
+ "أعمال العزل المائي للأساسات",
+ "أعمال الردم والدك للأساسات",
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة",
+ "توريد وتركيب حديد التسليح للأعمدة",
+ "أعمال البلوك للجدران"
+ ],
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"],
+ 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0, 10.0, 400.0],
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+ })
+
+ st.session_state.manual_items = doc_items.copy()
+ st.success("تم استيراد البيانات من تحليل المستند بنجاح!")
+ st.dataframe(doc_items)
+
+ # زر بدء التسعير
+ if st.button("بدء التسعير"):
+ # تحقق من صحة البيانات
+ if 'manual_items' in st.session_state and not st.session_state.manual_items.empty:
+ # التأكد من حساب الإجمالي قبل الحفظ
+ st.session_state.manual_items['الإجمالي'] = st.session_state.manual_items['الكمية'] * st.session_state.manual_items['سعر الوحدة']
+
+ # حفظ بيانات التسعير الحالي
+ st.session_state.current_pricing = {
+ 'name': tender_name,
+ 'number': tender_number,
+ 'client': client,
+ 'location': location,
+ 'method': pricing_method,
+ 'submission_date': submission_date,
+ 'items': st.session_state.manual_items.copy(),
+ 'status': 'جديد',
+ 'created_at': datetime.now()
+ }
+
+ # الانتقال إلى تبويب نموذج التسعير الشامل
+ st.success("تم إنشاء التسعير بنجاح! يمكنك الانتقال إلى نموذج التسعير الشامل.")
+ else:
+ st.error("يرجى إدخال بيانات البنود أولاً.")
+
+ def _render_comprehensive_pricing_tab(self):
+ """عرض تبويب نموذج التسعير الشامل"""
+
+ st.markdown("### نموذج التسعير الشامل")
+
+ # التحقق من وجود تسعير حالي
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
+ return
+
+ # عرض معلومات التسعير الحالي
+ pricing = st.session_state.current_pricing
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric("اسم المناقصة", pricing['name'])
+ st.metric("الجهة المالكة", pricing['client'])
+
+ with col2:
+ st.metric("رقم المناقصة", pricing['number'])
+ st.metric("تاريخ التقديم", pricing['submission_date'].strftime("%Y-%m-%d"))
+
+ with col3:
+ st.metric("طريقة التسعير", pricing['method'])
+ st.metric("الموقع", pricing['location'])
+
+ # عرض البنود والتسعير
+ st.markdown("### بنود التسعير")
+
+ items = pricing['items'].copy()
+
+ # إضافة أسعار الوحدة للمحاكاة
+ if 'سعر الوحدة' in items.columns and (items['سعر الوحدة'] == 0).all():
+ items['سعر الوحدة'] = [
+ round(random.uniform(1000, 3000), 2), # الخرسانة
+ round(random.uniform(5000, 7000), 2), # الحديد
+ round(random.uniform(100, 200), 2), # العزل
+ round(random.uniform(50, 100), 2), # الردم
+ round(random.uniform(1200, 3500), 2), # الخرسانة للأعمدة
+ ]
+
+ if len(items) > 5:
+ for i in range(5, len(items)):
+ items.at[i, 'سعر الوحدة'] = round(random.uniform(500, 5000), 2)
+
+ # حساب الإجمالي
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
+
+ # عرض البنود
+ st.dataframe(items, use_container_width=True, hide_index=True)
+
+
+ # ✅ التوصية الذكية باستخدام OpenAI
+ with st.expander("🔍 توليد توصية ذكية باستخدام AI"):
+ if st.button("🔍 توليد توصية ذكية باستخدام AI", use_container_width=True):
+ import openai
+ import os
+
+ client = openai.OpenAI(api_key=os.environ.get("ai"))
+
+ items_df = items.copy()
+ prompt = f"""قم بتحليل الجدول التالي للبنود في مشروع إنشاء، وقدم توصية ذكية لتحسين التسعير وضمان التوازن المالي. الجدول يحتوي على البنود، الكميات، الأسعار، والإجماليات:\n\n{items_df.to_string(index=False)}\n\nالتوصية:\n"""
+
+ try:
+ with st.spinner("جاري توليد التوصية..."):
+ response = client.chat.completions.create(
+ model="gpt-4",
+ messages=[
+ {"role": "system", "content": "أنت خبير في تسعير مشاريع البناء والبنية التحتية."},
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.4,
+ max_tokens=500
+ )
+
+ recommendation = response.choices[0].message.content
+ st.success("تم توليد التوصية بنجاح!")
+ st.markdown("#### التوصية الذكية:")
+ st.info(recommendation)
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء الاتصال بنموذج OpenAI: {e}")
+ st.info("يجب التأكد من تثبيت أحدث إصدار من مكتبة OpenAI: `pip install openai --upgrade`")
+
+ # واجهة تعديل أسعار الوحدات
+ st.markdown("### تعديل أسعار الوحدات")
+
+ # تقسيم البنود إلى مجموعتين للعرض
+ col1, col2 = st.columns(2)
+ half = len(items) // 2 + len(items) % 2
+
+ with col1:
+ for idx in range(half):
+ if idx < len(items):
+ row = items.iloc[idx]
+ price = st.number_input(
+ f"{row['رقم البند']}: {row['وصف البند'][:30]}... ({row['الوحدة']})",
+ value=float(row['سعر الوحدة']),
+ min_value=0.0,
+ key=f"price1_{idx}"
+ )
+ items.at[idx, 'سعر الوحدة'] = price
+ items.at[idx, 'الإجمالي'] = price * items.at[idx, 'الكمية']
+
+ with col2:
+ for idx in range(half, len(items)):
+ row = items.iloc[idx]
+ price = st.number_input(
+ f"{row['رقم البند']}: {row['وصف البند'][:30]}... ({row['الوحدة']})",
+ value=float(row['سعر الوحدة']),
+ min_value=0.0,
+ key=f"price2_{idx}"
+ )
+ items.at[idx, 'سعر الوحدة'] = price
+ items.at[idx, 'الإجمالي'] = price * items.at[idx, 'الكمية']
+
+ # حساب وعرض إجماليات التسعير
+ total_price = items['الإجمالي'].sum()
+
+ st.markdown("### إجماليات التسعير")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric("إجمالي التكاليف المباشرة", f"{total_price:,.2f} ريال")
+
+ with col2:
+ overhead_percentage = st.slider("نسبة المصاريف العامة والأرباح (%)", 5, 30, 15)
+ overhead_value = total_price * overhead_percentage / 100
+ st.metric("المصاريف العامة والأرباح", f"{overhead_value:,.2f} ريال")
+
+ with col3:
+ grand_total = total_price + overhead_value
+ st.metric("الإجمالي النهائي", f"{grand_total:,.2f} ريال")
+
+ # رسم بياني لتوزيع التكاليف
+ st.markdown("### تحليل التكاليف")
+
+ # حساب النسب المئوية لكل بند
+ pie_data = items.copy()
+ pie_data['نسبة من إجمالي التكاليف'] = pie_data['الإجمالي'] / total_price * 100
+
+ fig = px.pie(
+ pie_data,
+ values='نسبة من إجمالي التكاليف',
+ names='وصف البند',
+ title='توزيع التكاليف حسب البنود',
+ hole=0.4
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # أزرار العمليات
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ if st.button("حفظ التسعير"):
+ # تحديث بيانات التسعير الحالي
+ st.session_state.current_pricing['items'] = items.copy()
+ st.success("تم حفظ التسعير بنجاح!")
+
+ with col2:
+ if st.button("تصدير إلى Excel"):
+ st.success("تم تصدير التسعير إلى Excel بنجاح!")
+
+ with col3:
+ if st.button("تحليل المخاطر المالية"):
+ st.success("تم إرسال الطلب إلى وحدة تحليل المخاطر!")
+
+ def _render_unbalanced_pricing_tab(self):
+ """عرض تبويب التسعير غير المتزن"""
+
+ st.markdown("### التسعير غير المتزن")
+
+ # التحقق من وجود تسعير حالي
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
+ return
+
+ # شرح التسعير غير المتزن
+ with st.expander("ما هو التسعير غير المتزن؟", expanded=False):
+ st.markdown("""
+ **التسعير غير المتزن** هو استراتيجية تسعير تقوم على توزيع التكاليف بين بنود المناقصة بشكل غير متساوٍ، مع الحفاظ على إجمالي قيمة العطاء.
+
+ ### استراتيجيات التسعير غير المتزن:
+
+ 1. **التحميل الأمامي (Front Loading)**: زيادة أسعار البنود المبكرة في المشروع للحصول على تدفق نقدي أفضل في بداية المشروع.
+ 2. **التحميل الخلفي (Back Loading)**: زيادة أسعار البنود المتأخرة في المشروع.
+ 3. **تحميل البنود المؤكدة**: زيادة أسعار البنود التي من المؤكد تنفيذها بالكميات المحددة.
+ 4. **تخفيض أسعار البنود المحتملة**: تخفيض أسعار البنود التي قد تزيد كمياتها أثناء التنفيذ.
+
+ ### مزايا التسعير غير المتزن:
+
+ - تحسين التدفق النقدي للمشروع.
+ - تعظيم الربحية في حالة التغييرات والأوامر التغييرية.
+ - زيادة فرص الفوز بالمناقصة.
+
+ ### مخاطر التسعير غير المتزن:
+
+ - قد يتم رفض العطاء إذا كان عدم التوازن واضحاً.
+ - قد تتأثر السمعة سلباً إذا تم استخدامه بشكل مفرط.
+ - قد يؤدي إلى خسائر إذا لم يتم تنفيذ البنود ذات الأسعار العالية.
+ """)
+
+ # عرض بنود التسعير الحالي
+ items = st.session_state.current_pricing['items'].copy()
+
+ # إضافة عمود إستراتيجية التسعير
+ if 'إستراتيجية التسعير' not in items.columns:
+ items['إستراتيجية التسعير'] = 'متوازن'
+
+ st.markdown("### إستراتيجية التسعير غير المتزن")
+
+ # اختيار الإستراتيجية
+ strategy = st.selectbox(
+ "اختر إستراتيجية التسعير",
+ [
+ "تحميل أمامي (Front Loading)",
+ "تحميل البنود المؤكدة",
+ "تخفيض البنود المحتمل زيادتها",
+ "إستراتيجية مخصصة"
+ ]
+ )
+
+ # تطبيق الإستراتيجية المختارة
+ if strategy == "تحميل أمامي (Front Loading)":
+ # محاكاة تحميل أمامي
+ items_count = len(items)
+ early_items = items.iloc[:items_count//3].index
+ middle_items = items.iloc[items_count//3:2*items_count//3].index
+ late_items = items.iloc[2*items_count//3:].index
+
+ # تطبيق الزيادة والنقصان
+ for idx in early_items:
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.3 # زيادة 30%
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
+
+ for idx in middle_items:
+ items.at[idx, 'إستراتيجية التسعير'] = 'متوازن'
+
+ for idx in late_items:
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30%
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
+
+ elif strategy == "تحميل البنود المؤكدة":
+ # محاكاة - اعتبار بعض البنود مؤكدة
+ confirmed_items = [0, 2, 4] # الأصفار-مستندة
+ variable_items = [idx for idx in range(len(items)) if idx not in confirmed_items]
+
+ # تطبيق الزيادة والنقصان
+ for idx in confirmed_items:
+ if idx < len(items):
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.25 # زيادة 25%
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
+
+ for idx in variable_items:
+ if idx < len(items):
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.85 # نقص 15%
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
+
+ elif strategy == "تخفيض البنود المحتمل زيادتها":
+ # محاكاة - اعتبار بعض البنود محتمل زيادتها
+ variable_items = [1, 3] # الأصفار-مستندة
+ other_items = [idx for idx in range(len(items)) if idx not in variable_items]
+
+ # تطبيق الزيادة والنقصان
+ for idx in variable_items:
+ if idx < len(items):
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30%
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
+
+ for idx in other_items:
+ if idx < len(items):
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.15 # زيادة 15%
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
+
+ else: # إستراتيجية مخصصة
+ st.markdown("### تعديل أسعار البنود يدوياً")
+ st.markdown("قم بتعديل أسعار البنود وإستراتيجية التسعير يدوياً.")
+
+ # حساب الإجمالي بعد التعديل
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
+
+ # تعيين ألوان للإستراتيجيات
+ def highlight_strategy(val):
+ if val == 'زيادة':
+ return 'background-color: #a8e6cf'
+ elif val == 'نقص':
+ return 'background-color: #ff9aa2'
+ return ''
+
+ # عرض الجدول مع تنسيق
+ st.markdown("### بنود التسعير غير المتزن")
+ styled_items = items.style.applymap(highlight_strategy, subset=['إستراتيجية التسعير'])
+ st.dataframe(styled_items, use_container_width=True)
+
+ # المقارنة بين التسعير المتوازن وغير المتوازن
+ st.markdown("### مقارنة التسعير المتوازن وغير المتوازن")
+
+ original_items = st.session_state.current_pricing['items'].copy()
+ original_total = original_items['الإجمالي'].sum()
+ unbalanced_total = items['الإجمالي'].sum()
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric("إجمالي التسعير المتوازن", f"{original_total:,.2f} ريال")
+
+ with col2:
+ st.metric("إجمالي التسعير غير المتوازن", f"{unbalanced_total:,.2f} ريال")
+
+ with col3:
+ diff = unbalanced_total - original_total
+ st.metric("الفرق", f"{diff:,.2f} ريال", delta=f"{diff/original_total*100:.1f}%")
+
+ # المعايرة للحفاظ على إجمالي التسعير
+ if abs(diff) > 1: # إذا كان هناك فرق كبير
+ if st.button("معايرة الأسعار للحفاظ على إجمالي التسعير"):
+ # تعديل الأسعار للحفاظ على إجمالي التكلفة
+ adjustment_factor = original_total / unbalanced_total
+ items['سعر الوحدة'] = items['سعر الوحدة'] * adjustment_factor
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
+
+ st.success(f"تم تعديل الأسعار للحفاظ على إجمالي التسعير الأصلي ({original_total:,.2f} ريال)")
+ st.dataframe(items, use_container_width=True)
+
+ # رسم بياني للمقارنة
+ st.markdown("### تحليل بصري للتسعير غير المتوازن")
+
+ # إعداد البيانات للرسم البياني
+ chart_data = pd.DataFrame({
+ 'وصف البند': original_items['وصف البند'],
+ 'التسعير المتوازن': original_items['الإجمالي'],
+ 'التسعير غير المتوازن': items['الإجمالي']
+ })
+
+ # رسم بياني شريطي للمقارنة
+ fig = go.Figure()
+
+ fig.add_trace(go.Bar(
+ x=chart_data['وصف البند'],
+ y=chart_data['التسعير المتوازن'],
+ name='التسعير المتوازن',
+ marker_color='rgb(55, 83, 109)'
+ ))
+
+ fig.add_trace(go.Bar(
+ x=chart_data['وصف البند'],
+ y=chart_data['التسعير غير المتوازن'],
+ name='التسعير غير المتوازن',
+ marker_color='rgb(26, 118, 255)'
+ ))
+
+ fig.update_layout(
+ title='مقارنة بين التسعير المتوازن وغير المتوازن',
+ xaxis_tickfont_size=14,
+ yaxis=dict(
+ title='الإجمالي (ريال)',
+ titlefont_size=16,
+ tickfont_size=14,
+ ),
+ legend=dict(
+ x=0,
+ y=1.0,
+ bgcolor='rgba(255, 255, 255, 0)',
+ bordercolor='rgba(255, 255, 255, 0)'
+ ),
+ barmode='group',
+ bargap=0.15,
+ bargroupgap=0.1
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # زر حفظ التسعير غير المتوازن
+ if st.button("حفظ التسعير غير المتوازن"):
+ st.session_state.current_pricing['items'] = items.copy()
+ st.session_state.current_pricing['method'] = "التسعير غير المتزن"
+ st.success("تم حفظ التسعير غير المتوازن بنجاح!")
+
+ def _render_local_content_tab(self):
+ """عرض تبويب المحتوى المحلي"""
+
+ st.markdown("### تحليل المحتوى المحلي")
+
+ # التحقق من وجود تسعير حالي
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
+ return
+
+ # شرح المحتوى المحلي
+ with st.expander("ما هو المحتوى المحلي؟", expanded=False):
+ st.markdown("""
+ **المحتوى المحلي** هو نسبة المنتجات والخدمات والقوى العاملة المحلية المستخدمة في المشروع. يهدف إلى زيادة مساهمة المنتجات والخدمات المحلية في المشاريع.
+
+ ### مكونات المحتوى المحلي:
+
+ 1. **المنتجات**: المنتجات والمواد المصنعة محلياً.
+ 2. **الخدمات**: الخدمات المقدمة من شركات محلية.
+ 3. **القوى العاملة**: العمالة والكوادر الفنية والإدارية المحلية.
+
+ ### أهمية المحتوى المحلي:
+
+ - تعزيز الاقتصاد المحلي وخلق فرص عمل.
+ - تحقيق أهداف رؤية 2030 في زيادة المحتوى المحلي.
+ - التأهل للمشاريع الحكومية التي تتطلب نسبة محتوى محلي محددة.
+ - الحصول على حوافز وأفضلية في المناقصات الحكومية.
+
+ ### متطلبات المحتوى المحلي:
+
+ - نسبة المحتوى المحلي للقوى العاملة: 80%
+ - نسبة المحتوى المحلي للمنتجات: 70%
+ - نسبة المحتوى المحلي للخدمات: 60%
+ """)
+
+ # عرض لوحة إدخال بيانات المحتوى المحلي
+ st.markdown("### بيانات المحتوى المحلي")
+
+ # التبويبات لأنواع المحتوى المحلي
+ lc_tabs = st.tabs(["المنتجات", "الخدمات", "القوى العاملة", "التحليل"])
+
+ with lc_tabs[0]: # المنتجات
+ st.markdown("#### بيانات المنتجات")
+
+ # إنشاء بيانات افتراضية للمنتجات إذا لم تكن موجودة
+ if 'local_content_products' not in st.session_state:
+ st.session_state.local_content_products = pd.DataFrame({
+ 'المنتج': [
+ "خرسانة مسلحة",
+ "حديد تسليح",
+ "بلوك خرساني",
+ "عزل مائي",
+ "دهانات"
+ ],
+ 'الكمية': [250, 25, 400, 500, 600],
+ 'سعر_الوحدة': [1200, 6000, 200, 100, 50],
+ 'التكلفة_الإجمالية': [300000, 150000, 80000, 50000, 30000],
+ 'نسبة_المحتوى_المحلي': [0.95, 0.70, 0.98, 0.60, 0.80]
+ })
+
+ # حساب التكلفة الإجمالية
+ st.session_state.local_content_products['التكلفة_الإجمالية'] = st.session_state.local_content_products['الكمية'] * st.session_state.local_content_products['سعر_الوحدة']
+
+ # عرض جدول البنود مع إمكانية التعديل
+ edited_products = st.data_editor(
+ st.session_state.local_content_products,
+ use_container_width=True,
+ hide_index=True,
+ num_rows="dynamic"
+ )
+ st.session_state.local_content_products = edited_products
+
+ # عرض ملخص المنتجات
+ total_products_cost = edited_products['التكلفة_الإجمالية'].sum()
+ avg_local_content = (edited_products['التكلفة_الإجمالية'] * edited_products['نسبة_المحتوى_المحلي']).sum() / total_products_cost if total_products_cost > 0 else 0
+
+ st.markdown(f"""
+ **إجمالي تكلفة المنتجات**: {total_products_cost:,.2f} ريال
+
+ **متوسط نسبة المحتوى المحلي للمنتجات**: {avg_local_content*100:.2f}%
+
+ **المستهدف**: 70%
+
+ **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.7 else "❌ غير ملتزم"}
+ """)
+
+ with lc_tabs[1]: # الخدمات
+ st.markdown("#### بيانات الخدمات")
+
+ # إنشاء بيانات افتراضية للخدمات إذا لم تكن موجودة
+ if 'local_content_services' not in st.session_state:
+ st.session_state.local_content_services = pd.DataFrame({
+ 'الخدمة': [
+ "تصميم معماري",
+ "إشراف هندسي",
+ "خدمات نقل",
+ "خدمات أمن وسلامة",
+ "صيانة ونظافة"
+ ],
+ 'التكلفة': [100000, 120000, 50000, 30000, 20000],
+ 'نسبة_المحتوى_المحلي': [0.90, 0.85, 0.90, 0.95, 0.95]
+ })
+
+ # عرض جدول الخدمات مع إمكانية التعديل
+ edited_services = st.data_editor(
+ st.session_state.local_content_services,
+ use_container_width=True,
+ hide_index=True,
+ num_rows="dynamic"
+ )
+ st.session_state.local_content_services = edited_services
+
+ # عرض ملخص الخدمات
+ total_services_cost = edited_services['التكلفة'].sum()
+ avg_local_content = (edited_services['التكلفة'] * edited_services['نسبة_المحتوى_المحلي']).sum() / total_services_cost if total_services_cost > 0 else 0
+
+ st.markdown(f"""
+ **إجمالي تكلفة الخدمات**: {total_services_cost:,.2f} ريال
+
+ **متوسط نسبة المحتوى المحلي للخدمات**: {avg_local_content*100:.2f}%
+
+ **المستهدف**: 60%
+
+ **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.6 else "❌ غير ملتزم"}
+ """)
+
+ with lc_tabs[2]: # القوى العاملة
+ st.markdown("#### بيانات القوى العاملة")
+
+ # إنشاء بيانات افتراضية للقوى العاملة إذا لم تكن موجودة
+ if 'local_content_labor' not in st.session_state:
+ st.session_state.local_content_labor = pd.DataFrame({
+ 'فئة_العمالة': [
+ "مهندسون",
+ "فنيون",
+ "عمال بناء",
+ "إداريون",
+ "مشرفون"
+ ],
+ 'العدد': [5, 10, 30, 3, 4],
+ 'الراتب_الشهري': [15000, 8000, 3000, 10000, 12000],
+ 'المدة_بالأشهر': [12, 12, 12, 12, 12],
+ 'نسبة_المحتوى_المحلي': [0.75, 0.65, 0.60, 0.90, 0.80]
+ })
+
+ # حساب التكلفة الإجمالية
+ st.session_state.local_content_labor['التكلفة_الإجمالية'] = st.session_state.local_content_labor['العدد'] * st.session_state.local_content_labor['الراتب_الشهري'] * st.session_state.local_content_labor['المدة_بالأشهر']
+
+ # عرض جدول القوى العاملة مع إمكانية التعديل
+ edited_labor = st.data_editor(
+ st.session_state.local_content_labor,
+ use_container_width=True,
+ hide_index=True,
+ num_rows="dynamic"
+ )
+
+ # إعادة حساب التكلفة الإجمالية بعد التعديل
+ edited_labor['التكلفة_الإجمالية'] = edited_labor['العدد'] * edited_labor['الراتب_الشهري'] * edited_labor['المدة_بالأشهر']
+ st.session_state.local_content_labor = edited_labor
+
+ # عرض ملخص القوى العاملة
+ total_labor_cost = edited_labor['التكلفة_الإجمالية'].sum()
+ avg_local_content = (edited_labor['التكلفة_الإجمالية'] * edited_labor['نسبة_المحتوى_المحلي']).sum() / total_labor_cost if total_labor_cost > 0 else 0
+
+ st.markdown(f"""
+ **إجمالي تكلفة القوى العاملة**: {total_labor_cost:,.2f} ريال
+
+ **متوسط نسبة المحتوى المحلي للقوى العاملة**: {avg_local_content*100:.2f}%
+
+ **المستهدف**: 80%
+
+ **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.8 else "❌ غير ملتزم"}
+ """)
+
+ with lc_tabs[3]: # التحليل
+ st.markdown("#### تحليل المحتوى المحلي")
+
+ # حساب المحتوى المحلي الإجمالي
+ try:
+ # تجميع بيانات تحليل المحتوى المحلي
+ products_cost = st.session_state.local_content_products['التكلفة_الإجمالية'].sum()
+ products_local_content = (st.session_state.local_content_products['التكلفة_الإجمالية'] * st.session_state.local_content_products['نسبة_المحتوى_المحلي']).sum() / products_cost if products_cost > 0 else 0
+
+ services_cost = st.session_state.local_content_services['التكلفة'].sum()
+ services_local_content = (st.session_state.local_content_services['التكلفة'] * st.session_state.local_content_services['نسبة_المحتوى_المحلي']).sum() / services_cost if services_cost > 0 else 0
+
+ labor_cost = st.session_state.local_content_labor['التكلفة_الإجمالية'].sum()
+ labor_local_content = (st.session_state.local_content_labor['التكلفة_الإجمالية'] * st.session_state.local_content_labor['نسبة_المحتوى_المحلي']).sum() / labor_cost if labor_cost > 0 else 0
+
+ # حساب الوزن النسبي لكل مكون
+ total_cost = products_cost + services_cost + labor_cost
+ products_weight = products_cost / total_cost if total_cost > 0 else 0
+ services_weight = services_cost / total_cost if total_cost > 0 else 0
+ labor_weight = labor_cost / total_cost if total_cost > 0 else 0
+
+ # حساب المحتوى المحلي الإجمالي
+ total_local_content = (products_local_content * products_weight) + (services_local_content * services_weight) + (labor_local_content * labor_weight)
+
+ # عرض ملخص المحتوى المحلي
+ st.markdown("### ملخص المحتوى المحلي")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال")
+
+ with col2:
+ st.metric("نسبة المحتوى المحلي الإجمالية", f"{total_local_content*100:.2f}%")
+
+ with col3:
+ target_local_content = 0.7 # 70%
+ st.metric("الحالة", "ملتزم" if total_local_content >= target_local_content else "غير ملتزم", delta=f"{(total_local_content - target_local_content)*100:.2f}%")
+
+ # عرض رسم بياني للمقارنة
+ st.markdown("### تحليل بصري للمحتوى المحلي")
+
+ # رسم بياني شريطي لنسب المحتوى المحلي
+ categories = ['المنتجات', 'الخدمات', 'القوى العاملة', 'الإجمالي']
+ actual_values = [products_local_content * 100, services_local_content * 100, labor_local_content * 100, total_local_content * 100]
+ target_values = [70, 60, 80, 70] # المستهدفات
+
+ # تهيئة البيانات للرسم البياني
+ chart_data = pd.DataFrame({
+ 'الفئة': categories,
+ 'النسبة الفعلية': actual_values,
+ 'النسبة المستهدفة': target_values
+ })
+
+ # رسم بياني شريطي للمقارنة
+ fig = go.Figure()
+
+ fig.add_trace(go.Bar(
+ x=chart_data['الفئة'],
+ y=chart_data['النسبة الفعلية'],
+ name='النسبة الفعلية',
+ marker_color='rgb(26, 118, 255)'
+ ))
+
+ fig.add_trace(go.Bar(
+ x=chart_data['الفئة'],
+ y=chart_data['النسبة المستهدفة'],
+ name='النسبة المستهدفة',
+ marker_color='rgb(55, 83, 109)'
+ ))
+
+ fig.update_layout(
+ title='مقارنة بين النسب الفعلية والمستهدفة للمحتوى المحلي',
+ xaxis_tickfont_size=14,
+ yaxis=dict(
+ title='النسبة %',
+ titlefont_size=16,
+ tickfont_size=14,
+ ),
+ legend=dict(
+ x=0,
+ y=1.0,
+ bgcolor='rgba(255, 255, 255, 0)',
+ bordercolor='rgba(255, 255, 255, 0)'
+ ),
+ barmode='group',
+ bargap=0.15,
+ bargroupgap=0.1
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض توصيات لتحسين نسبة المحتوى المحلي
+ st.markdown("### توصيات لتحسين نسبة المحتوى المحلي")
+
+ recommendations = []
+
+ if products_local_content < 0.7:
+ recommendations.append("- زيادة نسبة المحتوى المحلي للمنتجات من خلال:")
+ recommendations.append(" - البحث عن موردين محليين للمنتجات ذات النسبة المنخفضة")
+ recommendations.append(" - استبدال المنتجات المستوردة ببدائل محلية")
+ recommendations.append(" - التعاون مع المصانع المحلية لتوطين صناعة المنتجات")
+
+ if services_local_content < 0.6:
+ recommendations.append("- زيادة نسبة المحتوى المحلي للخدمات من خلال:")
+ recommendations.append(" - التعاقد مع شركات خدمات محلية")
+ recommendations.append(" - تحويل الخدمات المستعان بها من الخارج إلى شركات محلية")
+ recommendations.append(" - تأهيل الشركات المحلية لتقديم الخدمات المطلوبة")
+
+ if labor_local_content < 0.8:
+ recommendations.append("- زيادة نسبة المحتوى المحلي للقوى العاملة من خلال:")
+ recommendations.append(" - زيادة توظيف الكوادر المحلية")
+ recommendations.append(" - تدريب وتأهيل العمالة المحلية")
+ recommendations.append(" - استبدال العمالة الأجنبية بكوادر محلية تدريجياً")
+
+ if total_local_content < 0.7:
+ recommendations.append("- زيادة نسبة المحتوى المحلي الإجمالية من خلال:")
+ recommendations.append(" - إعادة توزيع الميزانية لصالح المكونات ذات النسبة العالية من المحتوى المحلي")
+ recommendations.append(" - وضع خطة مرحلية لزيادة المحتوى المحلي")
+ recommendations.append(" - التعاون مع اللجنة المحلية لزيادة المحتوى المحلي")
+
+ if recommendations:
+ for rec in recommendations:
+ st.markdown(rec)
+ else:
+ st.success("تهانينا! نسبة المحتوى المحلي متوافقة مع المتطلبات.")
+
+ # حساب تأثير المحتوى المحلي على التسعير
+ st.markdown("### تأثير المحتوى المحلي على التسعير")
+
+ # تحديد عامل تعديل السعر بناءً على نسبة المحتوى المحلي
+ price_adjustment_factor = 1.0
+
+ if total_local_content >= 0.9:
+ price_adjustment_factor = 0.92 # خصم 8% للمحتوى المحلي العالي جداً
+ price_discount = "8%"
+ elif total_local_content >= 0.8:
+ price_adjustment_factor = 0.94 # خصم 6% للمحتوى المحلي العالي
+ price_discount = "6%"
+ elif total_local_content >= 0.7:
+ price_adjustment_factor = 0.96 # خصم 4% للمحتوى المحلي المتوسط
+ price_discount = "4%"
+ elif total_local_content >= 0.6:
+ price_adjustment_factor = 0.98 # خصم 2% للمحتوى المحلي المنخفض
+ price_discount = "2%"
+ else:
+ price_adjustment_factor = 1.0 # لا خصم
+ price_discount = "0%"
+
+ # عرض تأثير المحتوى المحلي على التسعير
+ original_total = st.session_state.current_pricing['items']['الإجمالي'].sum()
+ adjusted_total = original_total * price_adjustment_factor
+ discount_amount = original_total - adjusted_total
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric("إجمالي التسعير الأصلي", f"{original_total:,.2f} ريال")
+
+ with col2:
+ st.metric("نسبة الخصم بسبب المحتوى المحلي", price_discount)
+
+ with col3:
+ st.metric("إجمالي التسعير بعد الخصم", f"{adjusted_total:,.2f} ريال", delta=f"-{discount_amount:,.2f} ريال")
+
+ # أزرار العمليات
+ col1, col2 = st.columns(2)
+
+ with col1:
+ if st.button("حفظ تحليل المحتوى المحلي"):
+ # حفظ بيانات المحتوى المحلي في التسعير الحالي
+ st.session_state.current_pricing['local_content'] = {
+ 'products': st.session_state.local_content_products.copy(),
+ 'services': st.session_state.local_content_services.copy(),
+ 'labor': st.session_state.local_content_labor.copy(),
+ 'total_local_content': total_local_content,
+ 'price_adjustment_factor': price_adjustment_factor
+ }
+
+ st.success("تم حفظ تحليل المحتوى المحلي بنجاح!")
+
+ with col2:
+ if st.button("تصدير تقرير المحتوى المحلي"):
+ st.success("تم تصدير تقرير المحتوى المحلي بنجاح!")
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء تحليل المحتوى المحلي: {str(e)}")
+ st.warning("تأكد من إدخال بيانات المحتوى المحلي بشكل صحيح في التبويبات السابقة.")
\ No newline at end of file
diff --git a/modules/pricing/services/construction_cost_calculator.py b/modules/pricing/services/construction_cost_calculator.py
new file mode 100644
index 0000000000000000000000000000000000000000..60c49b9505c8e5137b78c6794c4103e2f8114559
--- /dev/null
+++ b/modules/pricing/services/construction_cost_calculator.py
@@ -0,0 +1,1006 @@
+"""
+خدمة حاسبة تكاليف البناء
+تقوم هذه الخدمة بحساب تكاليف البناء بشكل تفصيلي بناءً على المكونات المختلفة:
+- المواد الخام
+- العمالة
+- المعدات
+- المصاريف الإدارية
+- هامش الربح
+"""
+
+import pandas as pd
+import numpy as np
+from datetime import datetime
+import os
+import json
+import sys
+from typing import Dict, List, Optional, Union, Any
+
+# إضافة مسار النظام للوصول لملفات التكوين
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")))
+try:
+ import config
+except ImportError:
+ # إذا لم يتم العثور على ملف التكوين، نستخدم قيم افتراضية
+ class DefaultConfig:
+ DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../data"))
+ config = DefaultConfig
+
+ # إنشاء مجلد البيانات إذا لم يكن موجودًا
+ if not os.path.exists(config.DATA_DIR):
+ os.makedirs(config.DATA_DIR)
+
+class ConstructionCostCalculator:
+ """خدمة حاسبة تكاليف البناء"""
+
+ def __init__(self):
+ """تهيئة حاسبة تكاليف البناء"""
+ # تحميل بيانات المواد والأسعار المرجعية
+ self.material_rates = self._load_material_rates()
+ self.labor_rates = self._load_labor_rates()
+ self.equipment_rates = self._load_equipment_rates()
+
+ # النسب الافتراضية للمصاريف الإدارية وهامش الربح
+ self.default_admin_expenses_percentage = 0.05 # 5%
+ self.default_profit_margin_percentage = 0.10 # 10%
+
+ # معاملات التعديل الافتراضية
+ self.default_adjustment_factors = {
+ 'location_factor': 1.0, # معامل الموقع
+ 'time_factor': 1.0, # معامل الوقت
+ 'risk_factor': 1.0, # معامل المخاطر
+ 'market_factor': 1.0 # معامل السوق
+ }
+
+ def _load_material_rates(self) -> Dict[str, Dict[str, Any]]:
+ """تحميل أسعار المواد"""
+ # محاكاة تحميل البيانات من مصدر بيانات
+ material_rates = {
+ # مواد الخرسانة
+ 'خرسانة جاهزة': {
+ 'وحدة': 'م3',
+ 'سعر_الوحدة': 750.0,
+ 'وصف': 'خرسانة جاهزة بقوة 350 كجم/سم2',
+ 'فئة': 'أعمال خرسانية'
+ },
+ 'حديد تسليح': {
+ 'وحدة': 'طن',
+ 'سعر_الوحدة': 5500.0,
+ 'وصف': 'حديد تسليح قطر 8-32 مم',
+ 'فئة': 'أعمال خرسانية'
+ },
+ 'أسمنت': {
+ 'وحدة': 'كيس',
+ 'سعر_الوحدة': 30.0,
+ 'وصف': 'أسمنت بورتلاندي عادي',
+ 'فئة': 'أعمال خرسانية'
+ },
+ 'رمل': {
+ 'وحدة': 'م3',
+ 'سعر_الوحدة': 120.0,
+ 'وصف': 'رمل خشن للخرسانة',
+ 'فئة': 'أعمال خرسانية'
+ },
+ 'زلط': {
+ 'وحدة': 'م3',
+ 'سعر_الوحدة': 150.0,
+ 'وصف': 'زلط مقاس 10-20 مم للخرسانة',
+ 'فئة': 'أعمال خرسانية'
+ },
+
+ # مواد البناء
+ 'طوب أحمر': {
+ 'وحدة': '1000 قطعة',
+ 'سعر_الوحدة': 900.0,
+ 'وصف': 'طوب أحمر مقاس 25×12×6 سم',
+ 'فئة': 'أعمال بناء'
+ },
+ 'طوب أسمنتي': {
+ 'وحدة': 'قطعة',
+ 'سعر_الوحدة': 4.5,
+ 'وصف': 'بلوك أسمنتي مقاس 20×20×40 سم',
+ 'فئة': 'أعمال بناء'
+ },
+ 'مونة بناء': {
+ 'وحدة': 'م3',
+ 'سعر_الوحدة': 350.0,
+ 'وصف': 'مونة أسمنتية للبناء',
+ 'فئة': 'أعمال بناء'
+ },
+
+ # مواد التشطيبات
+ 'بلاط سيراميك': {
+ 'وحدة': 'م2',
+ 'سعر_الوحدة': 120.0,
+ 'وصف': 'بلاط سيراميك للأرضيات مقاس 40×40 سم',
+ 'فئة': 'تشطيبات'
+ },
+ 'بلاط بورسلين': {
+ 'وحدة': 'م2',
+ 'سعر_الوحدة': 180.0,
+ 'وصف': 'بلاط بورسلين للأرضيات مقاس 60×60 سم',
+ 'فئة': 'تشطيبات'
+ },
+ 'دهانات بلاستيك': {
+ 'وحدة': 'لتر',
+ 'سعر_الوحدة': 35.0,
+ 'وصف': 'دهان بلاستيك أساس وتشطيب',
+ 'فئة': 'تشطيبات'
+ },
+ 'جبس بورد': {
+ 'وحدة': 'م2',
+ 'سعر_الوحدة': 95.0,
+ 'وصف': 'ألواح جبس بورد سمك 12 مم',
+ 'فئة': 'تشطيبات'
+ },
+
+ # مواد العزل
+ 'عزل مائي': {
+ 'وحدة': 'م2',
+ 'سعر_الوحدة': 45.0,
+ 'وصف': 'عزل مائي من البيتومين المؤكسد',
+ 'فئة': 'أعمال عزل'
+ },
+ 'عزل حراري': {
+ 'وحدة': 'م2',
+ 'سعر_الوحدة': 65.0,
+ 'وصف': 'ألواح عزل حراري من البوليسترين سمك 5 سم',
+ 'فئة': 'أعمال عزل'
+ }
+ }
+
+ # محاولة تحميل البيانات من ملف إذا كان متاحًا
+ try:
+ file_path = os.path.join(config.DATA_DIR, 'material_rates.json')
+ if os.path.exists(file_path):
+ with open(file_path, 'r', encoding='utf-8') as f:
+ loaded_data = json.load(f)
+ material_rates.update(loaded_data)
+ except Exception as e:
+ print(f"خطأ في تحميل بيانات أسعار المواد: {str(e)}")
+
+ return material_rates
+
+ def _load_labor_rates(self) -> Dict[str, Dict[str, Any]]:
+ """تحميل أسعار العمالة"""
+ # محاكاة تحميل البيانات من مصدر بيانات
+ labor_rates = {
+ # عمالة الخرسانات
+ 'نجار مسلح': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 250.0,
+ 'وصف': 'نجار مسلح لأعمال الشدات والفرم',
+ 'فئة': 'أعمال خرسانية',
+ 'إنتاجية_يومية': {
+ 'شدة أساسات': 12, # متر مربع
+ 'شدة أعمدة': 10, # متر مربع
+ 'شدة أسقف': 12 # متر مربع
+ }
+ },
+ 'حداد مسلح': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 250.0,
+ 'وصف': 'حداد مسلح لأعمال حديد التسليح',
+ 'فئة': 'أعمال خرسانية',
+ 'إنتاجية_يومية': {
+ 'تجهيز وتركيب حديد أساسات': 700, # كجم
+ 'تجهيز وتركيب حديد أعمدة': 600, # كجم
+ 'تجهيز وتركيب حديد أسقف': 650 # كجم
+ }
+ },
+ 'عامل خرسانة': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 150.0,
+ 'وصف': 'عامل لصب وتسوية الخرسانة',
+ 'فئة': 'أعمال خرسانية',
+ 'إنتاجية_يومية': {
+ 'صب خرسانة': 15 # متر مكعب
+ }
+ },
+
+ # عمالة البناء
+ 'بناء': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 200.0,
+ 'وصف': 'عامل بناء للطوب والبلوك',
+ 'فئة': 'أعمال بناء',
+ 'إنتاجية_يومية': {
+ 'بناء طوب أحمر': 500, # قطعة
+ 'بناء بلوك أسمنتي': 80 # قطعة
+ }
+ },
+ 'مساعد بناء': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 120.0,
+ 'وصف': 'مساعد عامل بناء',
+ 'فئة': 'أعمال بناء',
+ 'إنتاجية_يومية': {}
+ },
+
+ # عمالة التشطيبات
+ 'مبلط': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 250.0,
+ 'وصف': 'عامل تركيب بلاط وسيراميك',
+ 'فئة': 'تشطيبات',
+ 'إنتاجية_يومية': {
+ 'تركيب سيراميك أرضيات': 15, # متر مربع
+ 'تركيب سيراميك حوائط': 12, # متر مربع
+ 'تركيب بورسلين': 12 # متر مربع
+ }
+ },
+ 'نقاش': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 200.0,
+ 'وصف': 'عامل دهانات',
+ 'فئة': 'تشطيبات',
+ 'إنتاجية_يومية': {
+ 'دهانات بلاستيك': 35, # متر مربع
+ 'دهانات زيتية': 25 # متر مربع
+ }
+ },
+ 'كهربائي': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 270.0,
+ 'وصف': 'فني كهرباء',
+ 'فئة': 'تشطيبات',
+ 'إنتاجية_يومية': {
+ 'تأسيس نقاط كهرباء': 15, # نقطة
+ 'تركيب لوحات توزيع': 2 # لوحة
+ }
+ },
+ 'سباك': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 250.0,
+ 'وصف': 'فني سباكة',
+ 'فئة': 'تشطيبات',
+ 'إنتاجية_يومية': {
+ 'تأسيس نقاط صرف': 8, # نقطة
+ 'تأسيس نقاط تغذية': 10, # نقطة
+ 'تركيب أطقم حمامات': 2 # طقم
+ }
+ },
+
+ # مراقبة وإشراف
+ 'مهندس موقع': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 500.0,
+ 'وصف': 'مهندس إشراف موقع',
+ 'فئة': 'إشراف'
+ },
+ 'مراقب فني': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 300.0,
+ 'وصف': 'مراقب فني للتنفيذ',
+ 'فئة': 'إشراف'
+ }
+ }
+
+ # محاولة تحميل البيانات من ملف إذا كان متاحًا
+ try:
+ file_path = os.path.join(config.DATA_DIR, 'labor_rates.json')
+ if os.path.exists(file_path):
+ with open(file_path, 'r', encoding='utf-8') as f:
+ loaded_data = json.load(f)
+ labor_rates.update(loaded_data)
+ except Exception as e:
+ print(f"خطأ في تحميل بيانات أسعار العمالة: {str(e)}")
+
+ return labor_rates
+
+ def _load_equipment_rates(self) -> Dict[str, Dict[str, Any]]:
+ """تحميل أسعار المعدات"""
+ # محاكاة تحميل البيانات من مصدر بيانات
+ equipment_rates = {
+ # معدات الحفر والتسوية
+ 'حفار صغير': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 1200.0,
+ 'وصف': 'حفار صغير (بوبكات) بقدرة 70 حصان',
+ 'فئة': 'معدات حفر',
+ 'إنتاجية_يومية': {
+ 'حفر في تربة عادية': 60 # متر مكعب
+ }
+ },
+ 'حفار متوسط': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 2500.0,
+ 'وصف': 'حفار متوسط الحجم بقدرة 150 حصان',
+ 'فئة': 'معدات حفر',
+ 'إنتاجية_يومية': {
+ 'حفر في تربة عادية': 200 # متر مكعب
+ }
+ },
+ 'لودر': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 2000.0,
+ 'وصف': 'لودر أمامي لنقل التربة',
+ 'فئة': 'معدات حفر',
+ 'إنتاجية_يومية': {
+ 'تحميل تربة': 300, # متر مكعب
+ 'تسوية موقع': 1500 # متر مربع
+ }
+ },
+ 'جريدر': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 2200.0,
+ 'وصف': 'جريدر لتسوية الموقع',
+ 'فئة': 'معدات حفر',
+ 'إنتاجية_يومية': {
+ 'تسوية طرق': 3000 # متر مربع
+ }
+ },
+
+ # معدات الخرسانة
+ 'خلاطة خرسانة': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 350.0,
+ 'وصف': 'خلاطة خرسانة بسعة 0.5 متر مكعب',
+ 'فئة': 'معدات خرسانة',
+ 'إنتاجية_يومية': {
+ 'خلط خرسانة': 15 # متر مكعب
+ }
+ },
+ 'هزاز خرسانة': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 150.0,
+ 'وصف': 'هزاز خرسانة كهربائي',
+ 'فئة': 'معدات خرسانة',
+ 'إنتاجية_يومية': {
+ 'دمك خرسانة': 40 # متر مكعب
+ }
+ },
+ 'شاحنة خرسانة جاهزة': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 3000.0,
+ 'وصف': 'شاحنة خرسانة جاهزة (مكسر) سعة 8 متر مكعب',
+ 'فئة': 'معدات خرسانة',
+ 'إنتاجية_يومية': {
+ 'نقل وصب خرسانة': 50 # متر مكعب
+ }
+ },
+ 'مضخة خرسانة': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 5000.0,
+ 'وصف': 'مضخة خرسانة بذراع 42 متر',
+ 'فئة': 'معدات خرسانة',
+ 'إنتاجية_يومية': {
+ 'ضخ خرسانة': 120 # متر مكعب
+ }
+ },
+
+ # معدات رفع ونقل
+ 'رافعة برجية': {
+ 'وحدة': 'شهر',
+ 'سعر_الوحدة': 35000.0,
+ 'وصف': 'رافعة برجية بارتفاع 40 متر',
+ 'فئة': 'معدات رفع',
+ },
+ 'ونش شوكة': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 1500.0,
+ 'وصف': 'ونش شوكة لرفع مواد البناء',
+ 'فئة': 'معدات رفع',
+ 'إنتاجية_يومية': {
+ 'رفع ونقل مواد': 100 # طن
+ }
+ },
+ 'شاحنة نقل': {
+ 'وحدة': 'يوم',
+ 'سعر_الوحدة': 1200.0,
+ 'وصف': 'شاحنة نقل حمولة 20 طن',
+ 'فئة': 'معدات نقل',
+ 'إنتاجية_يومية': {
+ 'نقل مواد': 80 # طن
+ }
+ }
+ }
+
+ # محاولة تحميل البيانات من ملف إذا كان متاحًا
+ try:
+ file_path = os.path.join(config.DATA_DIR, 'equipment_rates.json')
+ if os.path.exists(file_path):
+ with open(file_path, 'r', encoding='utf-8') as f:
+ loaded_data = json.load(f)
+ equipment_rates.update(loaded_data)
+ except Exception as e:
+ print(f"خطأ في تحميل بيانات أسعار المعدات: {str(e)}")
+
+ return equipment_rates
+
+ def calculate_item_cost(self, item_data: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ حساب تكلفة بند محدد بكافة مكوناته
+
+ المعلمات:
+ item_data (dict): بيانات البند، تتضمن:
+ - وصف_البند (str): وصف البند
+ - الكمية (float): كمية البند
+ - الوحدة (str): وحدة القياس
+ - المواد (list): قائمة المواد المستخدمة وكمياتها
+ - العمالة (list): قائمة العمالة المستخدمة وعددها
+ - المعدات (list): قائمة المعدات المستخدمة وساعات عملها
+ - المصاريف_الإدارية (float, optional): نسبة المصاريف الإدارية (افتراضياً 5%)
+ - هامش_الربح (float, optional): نسبة هامش الربح (افتراضياً 10%)
+ - عوامل_التعديل (dict, optional): عوامل تعديل التكلفة
+
+ العوائد:
+ dict: تفاصيل تكلفة البند بكافة عناصرها
+ """
+ # استخراج البيانات الأساسية للبند
+ item_description = item_data.get('وصف_البند', 'بند غير محدد')
+ quantity = item_data.get('الكمية', 0.0)
+ unit = item_data.get('الوحدة', 'وحدة')
+
+ # حساب تكلفة المواد
+ materials_cost = self._calculate_materials_cost(item_data.get('المواد', []))
+
+ # حساب تكلفة العمالة
+ labor_cost = self._calculate_labor_cost(item_data.get('العمالة', []))
+
+ # حساب تكلفة المعدات
+ equipment_cost = self._calculate_equipment_cost(item_data.get('المعدات', []))
+
+ # حساب التكلفة المباشرة الإجمالية
+ direct_cost = materials_cost['الإجمالي'] + labor_cost['الإجمالي'] + equipment_cost['الإجمالي']
+
+ # حساب المصاريف الإدارية
+ admin_percentage = item_data.get('المصاريف_الإدارية', self.default_admin_expenses_percentage)
+ admin_cost = direct_cost * admin_percentage
+
+ # حساب هامش الربح
+ profit_percentage = item_data.get('هامش_الربح', self.default_profit_margin_percentage)
+ profit_margin = (direct_cost + admin_cost) * profit_percentage
+
+ # حساب التكلفة الإجمالية
+ total_cost = direct_cost + admin_cost + profit_margin
+
+ # حساب سعر الوحدة
+ unit_price = total_cost / quantity if quantity > 0 else 0.0
+
+ # تطبيق عوامل التعديل إذا وجدت
+ adjustment_factors = item_data.get('عوامل_التعديل', self.default_adjustment_factors)
+ adjustment_factor = self._calculate_adjustment_factor(adjustment_factors)
+
+ adjusted_unit_price = unit_price * adjustment_factor
+ adjusted_total_cost = total_cost * adjustment_factor
+
+ # إعداد النتائج
+ result = {
+ 'وصف_البند': item_description,
+ 'الكمية': quantity,
+ 'الوحدة': unit,
+ 'تكاليف_مباشرة': {
+ 'المواد': materials_cost,
+ 'العمالة': labor_cost,
+ 'المعدات': equipment_cost,
+ 'إجمالي_تكاليف_مباشرة': direct_cost
+ },
+ 'مصاريف_إدارية': {
+ 'نسبة': admin_percentage * 100,
+ 'قيمة': admin_cost
+ },
+ 'هامش_ربح': {
+ 'نسبة': profit_percentage * 100,
+ 'قيمة': profit_margin
+ },
+ 'التكلفة_الإجمالية': total_cost,
+ 'سعر_الوحدة': unit_price,
+ 'عوامل_التعديل': {
+ 'المعامل_الإجمالي': adjustment_factor,
+ 'التفاصيل': adjustment_factors
+ },
+ 'السعر_المعدل': {
+ 'سعر_الوحدة': adjusted_unit_price,
+ 'إجمالي': adjusted_total_cost
+ }
+ }
+
+ return result
+
+ def _calculate_materials_cost(self, materials: List[Dict[str, Any]]) -> Dict[str, Any]:
+ """
+ حساب تكلفة المواد
+
+ المعلمات:
+ materials (list): قائمة المواد المستخدمة وكمياتها
+ - الاسم (str): اسم المادة
+ - الكمية (float): الكمية المستخدمة
+ - الوحدة (str, optional): وحدة القياس
+ - سعر_الوحدة (float, optional): سعر الوحدة (يستخدم السعر من البيانات المرجعية إذا لم يتم تحديده)
+
+ العوائد:
+ dict: تفاصيل تكلفة المواد
+ """
+ materials_details = []
+ total_cost = 0.0
+
+ for material in materials:
+ material_name = material.get('الاسم', '')
+ quantity = material.get('الكمية', 0.0)
+
+ # البحث عن سعر المادة من البيانات المرجعية إذا لم يتم تحديده
+ if 'سعر_الوحدة' in material:
+ unit_price = material.get('سعر_الوحدة', 0.0)
+ unit = material.get('الوحدة', 'وحدة')
+ elif material_name in self.material_rates:
+ ref_material = self.material_rates[material_name]
+ unit_price = ref_material.get('سعر_الوحدة', 0.0)
+ unit = ref_material.get('وحدة', 'وحدة')
+ else:
+ unit_price = 0.0
+ unit = material.get('الوحدة', 'وحدة')
+
+ # حساب التكلفة
+ cost = quantity * unit_price
+ total_cost += cost
+
+ # إضافة التفاصيل
+ materials_details.append({
+ 'الاسم': material_name,
+ 'الكمية': quantity,
+ 'الوحدة': unit,
+ 'سعر_الوحدة': unit_price,
+ 'التكلفة': cost
+ })
+
+ return {
+ 'التفاصيل': materials_details,
+ 'الإجمالي': total_cost
+ }
+
+ def _calculate_labor_cost(self, labor: List[Dict[str, Any]]) -> Dict[str, Any]:
+ """
+ حساب تكلفة العمالة
+
+ المعلمات:
+ labor (list): قائمة العمالة المستخدمة وعددها
+ - النوع (str): نوع العامل
+ - العدد (int): عدد العمال
+ - المدة (float): مدة العمل بالأيام
+ - سعر_اليوم (float, optional): أجر اليوم (يستخدم السعر من البيانات المرجعية إذا لم يتم تحديده)
+
+ العوائد:
+ dict: تفاصيل تكلفة العمالة
+ """
+ labor_details = []
+ total_cost = 0.0
+
+ for worker in labor:
+ worker_type = worker.get('النوع', '')
+ count = worker.get('العدد', 0)
+ duration = worker.get('المدة', 0.0)
+
+ # البحث عن سعر العامل من البيانات المرجعية إذا لم يتم تحديده
+ if 'سعر_اليوم' in worker:
+ daily_rate = worker.get('سعر_اليوم', 0.0)
+ elif worker_type in self.labor_rates:
+ daily_rate = self.labor_rates[worker_type].get('سعر_الوحدة', 0.0)
+ else:
+ daily_rate = 0.0
+
+ # حساب التكلفة
+ cost = count * duration * daily_rate
+ total_cost += cost
+
+ # إضافة التفاصيل
+ labor_details.append({
+ 'النوع': worker_type,
+ 'العدد': count,
+ 'المدة': duration,
+ 'سعر_اليوم': daily_rate,
+ 'التكلفة': cost
+ })
+
+ return {
+ 'التفاصيل': labor_details,
+ 'الإجمالي': total_cost
+ }
+
+ def _calculate_equipment_cost(self, equipment: List[Dict[str, Any]]) -> Dict[str, Any]:
+ """
+ حساب تكلفة المعدات
+
+ المعلمات:
+ equipment (list): قائمة المعدات المستخدمة وساعات عملها
+ - النوع (str): نوع المعدة
+ - العدد (int): عدد المعدات
+ - المدة (float): مدة الاستخدام بالأيام
+ - سعر_اليوم (float, optional): أجر اليوم (يستخدم السعر من البيانات المرجعية إذا لم يتم تحديده)
+
+ العوائد:
+ dict: تفاصيل تكلفة المعدات
+ """
+ equipment_details = []
+ total_cost = 0.0
+
+ for equip in equipment:
+ equip_type = equip.get('النوع', '')
+ count = equip.get('العدد', 0)
+ duration = equip.get('المدة', 0.0)
+
+ # البحث عن سعر المعدة من البيانات المرجعية إذا لم يتم تحديده
+ if 'سعر_اليوم' in equip:
+ daily_rate = equip.get('سعر_اليوم', 0.0)
+ elif equip_type in self.equipment_rates:
+ daily_rate = self.equipment_rates[equip_type].get('سعر_الوحدة', 0.0)
+ else:
+ daily_rate = 0.0
+
+ # حساب التكلفة
+ cost = count * duration * daily_rate
+ total_cost += cost
+
+ # إضافة التفاصيل
+ equipment_details.append({
+ 'النوع': equip_type,
+ 'العدد': count,
+ 'المدة': duration,
+ 'سعر_اليوم': daily_rate,
+ 'التكلفة': cost
+ })
+
+ return {
+ 'التفاصيل': equipment_details,
+ 'الإجمالي': total_cost
+ }
+
+ def _calculate_adjustment_factor(self, factors: Dict[str, float]) -> float:
+ """
+ حساب المعامل الإجمالي لتعديل التكلفة
+
+ المعلمات:
+ factors (dict): عوامل التعديل
+
+ العوائد:
+ float: المعامل الإجمالي
+ """
+ # دمج العوامل المحددة مع العوامل الافتراضية
+ effective_factors = self.default_adjustment_factors.copy()
+ effective_factors.update(factors)
+
+ # حساب المعامل الإجمالي
+ total_factor = 1.0
+ for factor in effective_factors.values():
+ total_factor *= factor
+
+ return total_factor
+
+ def calculate_project_cost(self, project_data: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ حساب التكلفة الإجمالية لمشروع بناء كامل
+
+ المعلمات:
+ project_data (dict): بيانات المشروع، تتضمن:
+ - اسم_المشروع (str): اسم المشروع
+ - وصف_المشروع (str): وصف المشروع
+ - البنود (list): قائمة بنود المشروع
+ - المصاريف_الإدارية (float, optional): نسبة المصاريف الإدارية الإجمالية (افتراضياً 5%)
+ - هامش_الربح (float, optional): نسبة هامش الربح الإجمالي (افتراضياً 10%)
+ - عوامل_التعديل (dict, optional): عوامل تعديل التكلفة للمشروع
+
+ العوائد:
+ dict: تفاصيل تكلفة المشروع بكافة عناصرها
+ """
+ # استخراج البيانات الأساسية للمشروع
+ project_name = project_data.get('اسم_المشروع', 'مشروع غير محدد')
+ project_description = project_data.get('وصف_المشروع', '')
+ items = project_data.get('البنود', [])
+
+ # استخراج النسب الإجمالية
+ admin_percentage = project_data.get('المصاريف_الإدارية', self.default_admin_expenses_percentage)
+ profit_percentage = project_data.get('هامش_الربح', self.default_profit_margin_percentage)
+
+ # حساب تكلفة كل بند
+ items_costs = []
+ total_direct_cost = 0.0
+ total_materials_cost = 0.0
+ total_labor_cost = 0.0
+ total_equipment_cost = 0.0
+
+ for item_data in items:
+ # تحديث نسب المصاريف والربح للبند إذا لم تكن محددة
+ if 'المصاريف_الإدارية' not in item_data:
+ item_data['المصاريف_الإدارية'] = admin_percentage
+
+ if 'هامش_الربح' not in item_data:
+ item_data['هامش_الربح'] = profit_percentage
+
+ # حساب تكلفة البند
+ item_cost = self.calculate_item_cost(item_data)
+ items_costs.append(item_cost)
+
+ # تحديث الإجماليات
+ total_materials_cost += item_cost['تكاليف_مباشرة']['المواد']['الإجمالي']
+ total_labor_cost += item_cost['تكاليف_مباشرة']['العمالة']['الإجمالي']
+ total_equipment_cost += item_cost['تكاليف_مباشرة']['المعدات']['الإجمالي']
+ total_direct_cost += item_cost['تكاليف_مباشرة']['إجمالي_تكاليف_مباشرة']
+
+ # حساب المصاريف الإدارية
+ admin_cost = total_direct_cost * admin_percentage
+
+ # حساب هامش الربح
+ profit_margin = (total_direct_cost + admin_cost) * profit_percentage
+
+ # حساب التكلفة الإجمالية
+ total_cost = total_direct_cost + admin_cost + profit_margin
+
+ # تطبيق عوامل التعديل إذا وجدت
+ adjustment_factors = project_data.get('عوامل_التعديل', self.default_adjustment_factors)
+ adjustment_factor = self._calculate_adjustment_factor(adjustment_factors)
+
+ adjusted_total_cost = total_cost * adjustment_factor
+
+ # إعداد النتائج
+ result = {
+ 'اسم_المشروع': project_name,
+ 'وصف_المشروع': project_description,
+ 'تكاليف_مباشرة': {
+ 'المواد': {
+ 'الإجمالي': total_materials_cost,
+ 'النسبة_المئوية': (total_materials_cost / total_direct_cost * 100) if total_direct_cost > 0 else 0
+ },
+ 'العمالة': {
+ 'الإجمالي': total_labor_cost,
+ 'النسبة_المئوية': (total_labor_cost / total_direct_cost * 100) if total_direct_cost > 0 else 0
+ },
+ 'المعدات': {
+ 'الإجمالي': total_equipment_cost,
+ 'النسبة_المئوية': (total_equipment_cost / total_direct_cost * 100) if total_direct_cost > 0 else 0
+ },
+ 'إجمالي_تكاليف_مباشرة': total_direct_cost
+ },
+ 'مصاريف_إدارية': {
+ 'نسبة': admin_percentage * 100,
+ 'قيمة': admin_cost
+ },
+ 'هامش_ربح': {
+ 'نسبة': profit_percentage * 100,
+ 'قيمة': profit_margin
+ },
+ 'التكلفة_الإجمالية': total_cost,
+ 'عوامل_التعديل': {
+ 'المعامل_الإجمالي': adjustment_factor,
+ 'التفاصيل': adjustment_factors
+ },
+ 'التكلفة_النهائية_المعدلة': adjusted_total_cost,
+ 'تفاصيل_البنود': items_costs,
+ 'عدد_البنود': len(items)
+ }
+
+ return result
+
+ def get_rate_info(self, item_type: str, item_name: str) -> Dict[str, Any]:
+ """
+ الحصول على معلومات تفصيلية عن معدل وسعر عنصر محدد (مادة، عمالة، معدة)
+
+ المعلمات:
+ item_type (str): نوع العنصر - 'مادة'، 'عمالة'، 'معدة'
+ item_name (str): اسم العنصر
+
+ العوائد:
+ dict: معلومات تفصيلية عن العنصر
+ """
+ # تحديد القاموس المناسب حسب نوع العنصر
+ if item_type == 'مادة':
+ rates_dict = self.material_rates
+ elif item_type == 'عمالة':
+ rates_dict = self.labor_rates
+ elif item_type == 'معدة':
+ rates_dict = self.equipment_rates
+ else:
+ return {'خطأ': 'نوع العنصر غير صحيح'}
+
+ # البحث عن العنصر في القاموس
+ if item_name in rates_dict:
+ return rates_dict[item_name]
+ else:
+ return {'خطأ': 'العنصر غير موجود'}
+
+ def get_all_rates(self, item_type: str = None, category: str = None) -> Dict[str, Any]:
+ """
+ الحصول على قوائم معدلات الأسعار (لجميع المواد أو العمالة أو المعدات)
+
+ المعلمات:
+ item_type (str, optional): نوع العنصر - 'مادة'، 'عمالة'، 'معدة'، أو None لجميع الأنواع
+ category (str, optional): فئة محددة للتصفية
+
+ العوائد:
+ dict: قوائم معدلات الأسعار
+ """
+ result = {}
+
+ # جمع المواد حسب الفئة
+ if item_type is None or item_type == 'مادة':
+ materials = {}
+ for name, info in self.material_rates.items():
+ if category is None or info.get('فئة') == category:
+ materials[name] = info
+ result['المواد'] = materials
+
+ # جمع العمالة حسب الفئة
+ if item_type is None or item_type == 'عمالة':
+ labor = {}
+ for name, info in self.labor_rates.items():
+ if category is None or info.get('فئة') == category:
+ labor[name] = info
+ result['العمالة'] = labor
+
+ # جمع المعدات حسب الفئة
+ if item_type is None or item_type == 'معدة':
+ equipment = {}
+ for name, info in self.equipment_rates.items():
+ if category is None or info.get('فئة') == category:
+ equipment[name] = info
+ result['المعدات'] = equipment
+
+ return result
+
+ def generate_sample_project_data(self) -> Dict[str, Any]:
+ """
+ توليد بيانات نموذجية لمشروع بناء صغير للاختبار
+
+ العوائد:
+ dict: بيانات المشروع النموذجية
+ """
+ # إنشاء بيانات المشروع
+ project_data = {
+ 'اسم_المشروع': 'مبنى سكني صغير',
+ 'وصف_المشروع': 'مبنى سكني مكون من دور أرضي بمساحة 250 متر مربع',
+ 'المصاريف_الإدارية': 0.05, # 5%
+ 'هامش_الربح': 0.10, # 10%
+ 'عوامل_التعديل': {
+ 'location_factor': 1.2, # معامل الموقع (منطقة مرتفعة التكلفة)
+ 'time_factor': 1.0, # معامل الوقت
+ 'risk_factor': 1.05, # معامل المخاطر
+ 'market_factor': 1.0 # معامل السوق
+ },
+ 'البنود': [
+ # الأساسات
+ {
+ 'وصف_البند': 'حفر الأساسات بعمق 2 متر',
+ 'الكمية': 150.0,
+ 'الوحدة': 'م3',
+ 'المواد': [],
+ 'العمالة': [
+ {'النوع': 'عامل خرسانة', 'العدد': 4, 'المدة': 3}
+ ],
+ 'المعدات': [
+ {'النوع': 'حفار متوسط', 'العدد': 1, 'المدة': 2}
+ ]
+ },
+ {
+ 'وصف_البند': 'توريد وصب خرسانة عادية للأساسات',
+ 'الكمية': 25.0,
+ 'الوحدة': 'م3',
+ 'المواد': [
+ {'الاسم': 'خرسانة جاهزة', 'الكمية': 25.0}
+ ],
+ 'العمالة': [
+ {'النوع': 'عامل خرسانة', 'العدد': 6, 'المدة': 1}
+ ],
+ 'المعدات': [
+ {'النوع': 'مضخة خرسانة', 'العدد': 1, 'المدة': 0.5}
+ ]
+ },
+ {
+ 'وصف_البند': 'توريد وتركيب حديد تسليح للأساسات',
+ 'الكمية': 3.5,
+ 'الوحدة': 'طن',
+ 'المواد': [
+ {'الاسم': 'حديد تسليح', 'الكمية': 3.5}
+ ],
+ 'العمالة': [
+ {'النوع': 'حداد مسلح', 'العدد': 4, 'المدة': 3}
+ ],
+ 'المعدات': []
+ },
+ {
+ 'وصف_البند': 'نجارة وفك شدة الأساسات',
+ 'الكمية': 120.0,
+ 'الوحدة': 'م2',
+ 'المواد': [],
+ 'العمالة': [
+ {'النوع': 'نجار مسلح', 'العدد': 4, 'المدة': 3}
+ ],
+ 'المعدات': []
+ },
+ {
+ 'وصف_البند': 'توريد وصب خرسانة مسلحة للأساسات',
+ 'الكمية': 30.0,
+ 'الوحدة': 'م3',
+ 'المواد': [
+ {'الاسم': 'خرسانة جاهزة', 'الكمية': 30.0}
+ ],
+ 'العمالة': [
+ {'النوع': 'عامل خرسانة', 'العدد': 6, 'المدة': 1}
+ ],
+ 'المعدات': [
+ {'النوع': 'مضخة خرسانة', 'العدد': 1, 'المدة': 0.5},
+ {'النوع': 'هزاز خرسانة', 'العدد': 2, 'المدة': 1}
+ ]
+ },
+
+ # الأعمدة والأسقف
+ {
+ 'وصف_البند': 'توريد وتركيب حديد تسليح للأعمدة',
+ 'الكمية': 2.8,
+ 'الوحدة': 'طن',
+ 'المواد': [
+ {'الاسم': 'حديد تسليح', 'الكمية': 2.8}
+ ],
+ 'العمالة': [
+ {'النوع': 'حداد مسلح', 'العدد': 4, 'المدة': 3}
+ ],
+ 'المعدات': []
+ },
+ {
+ 'وصف_البند': 'نجارة وفك شدة الأعمدة',
+ 'الكمية': 85.0,
+ 'الوحدة': 'م2',
+ 'المواد': [],
+ 'العمالة': [
+ {'النوع': 'نجار مسلح', 'العدد': 3, 'المدة': 3}
+ ],
+ 'المعدات': []
+ },
+ {
+ 'وصف_البند': 'توريد وصب خرسانة مسلحة للأعمدة',
+ 'الكمية': 12.0,
+ 'الوحدة': 'م3',
+ 'المواد': [
+ {'الاسم': 'خرسانة جاهزة', 'الكمية': 12.0}
+ ],
+ 'العمالة': [
+ {'النوع': 'عامل خرسانة', 'العدد': 4, 'المدة': 1}
+ ],
+ 'المعدات': [
+ {'النوع': 'مضخة خرسانة', 'العدد': 1, 'المدة': 0.5},
+ {'النوع': 'هزاز خرسانة', 'العدد': 2, 'المدة': 1}
+ ]
+ },
+
+ # أعمال البناء
+ {
+ 'وصف_البند': 'توريد وبناء حوائط من الطوب الأحمر',
+ 'الكمية': 220.0,
+ 'الوحدة': 'م2',
+ 'المواد': [
+ {'الاسم': 'طوب أحمر', 'الكمية': 16.5} # بالألف
+ ],
+ 'العمالة': [
+ {'النوع': 'بناء', 'العدد': 4, 'المدة': 8},
+ {'النوع': 'مساعد بناء', 'العدد': 4, 'المدة': 8}
+ ],
+ 'المعدات': []
+ },
+
+ # أعمال التشطيبات
+ {
+ 'وصف_البند': 'توريد وتركيب بلاط سيراميك للأرضيات',
+ 'الكمية': 250.0,
+ 'الوحدة': 'م2',
+ 'المواد': [
+ {'الاسم': 'بلاط سيراميك', 'الكمية': 250.0}
+ ],
+ 'العمالة': [
+ {'النوع': 'مبلط', 'العدد': 4, 'المدة': 7}
+ ],
+ 'المعدات': []
+ },
+ {
+ 'وصف_البند': 'توريد وتنفيذ دهانات للحوائط',
+ 'الكمية': 450.0,
+ 'الوحدة': 'م2',
+ 'المواد': [
+ {'الاسم': 'دهانات بلاستيك', 'الكمية': 90.0} # بالتر
+ ],
+ 'العمالة': [
+ {'النوع': 'نقاش', 'العدد': 3, 'المدة': 8}
+ ],
+ 'المعدات': []
+ }
+ ]
+ }
+
+ return project_data
\ No newline at end of file
diff --git a/modules/pricing/services/construction_templates.py b/modules/pricing/services/construction_templates.py
new file mode 100644
index 0000000000000000000000000000000000000000..bdbdd1586dcb54cb9d806ecb016bac7f6dfc0f91
--- /dev/null
+++ b/modules/pricing/services/construction_templates.py
@@ -0,0 +1,748 @@
+"""
+كتالوج بنود نموذجية للمقاولات
+يحتوي هذا الملف على قائمة كاملة من النماذج الجاهزة للبنود الشائعة في مشاريع المقاولات، مثل:
+- أعمال الخرسانة بأنواعها
+- المناهل وأنواع المواسير
+- التركيبات المختلفة
+- الطرق والأسفلت
+- وغيرها من أعمال المقاولات
+"""
+
+import os
+import json
+import sys
+from typing import Dict, List, Any, Optional
+from datetime import datetime
+
+# إضافة مسار النظام للوصول لملفات التكوين
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")))
+try:
+ import config
+except ImportError:
+ # إذا لم يتم العثور على ملف التكوين، نستخدم قيم افتراضية
+ class DefaultConfig:
+ DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../data"))
+ config = DefaultConfig
+
+ # إنشاء مجلد البيانات إذا لم يكن موجودًا
+ if not os.path.exists(config.DATA_DIR):
+ os.makedirs(config.DATA_DIR)
+
+
+class ConstructionTemplates:
+ """كتالوج بنود نموذجية للمقاولات"""
+
+ def __init__(self):
+ """تهيئة كتالوج البنود النموذجية"""
+ self.templates_file = os.path.join(config.DATA_DIR, 'construction_templates.json')
+ self.market_prices_file = os.path.join(config.DATA_DIR, 'saudi_market_prices.json')
+
+ # تحميل قوالب البنود النموذجية
+ self.templates = self._load_templates()
+
+ # تحميل أسعار السوق السعودي
+ self.market_prices = self._load_market_prices()
+
+ def _load_templates(self) -> Dict[str, Dict[str, Any]]:
+ """تحميل قوالب البنود النموذجية من الملف"""
+ if os.path.exists(self.templates_file):
+ try:
+ with open(self.templates_file, 'r', encoding='utf-8') as f:
+ return json.load(f)
+ except Exception as e:
+ print(f"خطأ في تحميل قوالب البنود النموذجية: {str(e)}")
+
+ # إنشاء بيانات افتراضية إذا لم يتم العثور على الملف
+ default_templates = self._create_default_templates()
+
+ # حفظ البيانات الافتراضية
+ self._save_templates(default_templates)
+
+ return default_templates
+
+ def _save_templates(self, templates: Dict[str, Dict[str, Any]]) -> None:
+ """حفظ قوالب البنود النموذجية إلى الملف"""
+ try:
+ with open(self.templates_file, 'w', encoding='utf-8') as f:
+ json.dump(templates, f, ensure_ascii=False, indent=4)
+ except Exception as e:
+ print(f"خطأ في حفظ قوالب البنود النموذجية: {str(e)}")
+
+ def _load_market_prices(self) -> Dict[str, Dict[str, Any]]:
+ """تحميل أسعار السوق السعودي من الملف"""
+ if os.path.exists(self.market_prices_file):
+ try:
+ with open(self.market_prices_file, 'r', encoding='utf-8') as f:
+ return json.load(f)
+ except Exception as e:
+ print(f"خطأ في تحميل أسعار السوق السعودي: {str(e)}")
+
+ # إنشاء بيانات افتراضية إذا لم يتم العثور على الملف
+ default_prices = self._create_default_market_prices()
+
+ # حفظ البيانات الافتراضية
+ self._save_market_prices(default_prices)
+
+ return default_prices
+
+ def _save_market_prices(self, prices: Dict[str, Dict[str, Any]]) -> None:
+ """حفظ أسعار السوق السعودي إلى الملف"""
+ try:
+ with open(self.market_prices_file, 'w', encoding='utf-8') as f:
+ json.dump(prices, f, ensure_ascii=False, indent=4)
+ except Exception as e:
+ print(f"خطأ في حفظ أسعار السوق السعودي: {str(e)}")
+
+ def _create_default_templates(self) -> Dict[str, Dict[str, Any]]:
+ """إنشاء قوالب افتراضية للبنود النموذجية"""
+ templates = {
+ "categories": {
+ "أعمال_خرسانية": {
+ "name": "أعمال خرسانية",
+ "description": "بنود أعمال الخرسانة المسلحة والعادية",
+ "icon": "building"
+ },
+ "أعمال_صحية": {
+ "name": "أعمال صحية",
+ "description": "بنود أعمال المناهل والمواسير والتركيبات الصحية",
+ "icon": "pipe"
+ },
+ "أعمال_طرق": {
+ "name": "أعمال طرق",
+ "description": "بنود أعمال الطرق والأسفلت والرصف",
+ "icon": "road"
+ },
+ "أعمال_كهربائية": {
+ "name": "أعمال كهربائية",
+ "description": "بنود أعمال الكهرباء والإنارة",
+ "icon": "zap"
+ },
+ "أعمال_ميكانيكية": {
+ "name": "أعمال ميكانيكية",
+ "description": "بنود أعمال التكييف والتهوية والتبريد",
+ "icon": "thermometer"
+ }
+ },
+ "templates": {
+ # نماذج أعمال خرسانية
+ "خرسانة_مسلحة_أساسات": {
+ "category": "أعمال_خرسانية",
+ "name": "خرسانة مسلحة للأساسات",
+ "description": "توريد وصب خرسانة مسلحة للأساسات بقوة لا تقل عن 300 كجم/سم2",
+ "unit": "م3",
+ "components": {
+ "materials": [
+ {"الاسم": "خرسانة جاهزة", "الكمية": 1.0, "الوحدة": "م3", "سعر_الوحدة": 750.0},
+ {"الاسم": "حديد تسليح", "الكمية": 0.12, "الوحدة": "طن", "سعر_الوحدة": 5500.0}
+ ],
+ "labor": [
+ {"النوع": "عامل خرسانة", "العدد": 4, "المدة": 0.3, "سعر_اليوم": 150.0},
+ {"النوع": "نجار مسلح", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 250.0},
+ {"النوع": "حداد مسلح", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 250.0}
+ ],
+ "equipment": [
+ {"النوع": "هزاز خرسانة", "العدد": 1, "المدة": 0.3, "سعر_اليوم": 150.0}
+ ]
+ },
+ "admin_expenses": 0.05,
+ "profit_margin": 0.10,
+ "tags": ["خرسانة", "أساسات", "مسلحة"]
+ },
+ "خرسانة_مسلحة_أعمدة": {
+ "category": "أعمال_خرسانية",
+ "name": "خرسانة مسلحة للأعمدة",
+ "description": "توريد وصب خرسانة مسلحة للأعمدة بقوة لا تقل عن 350 كجم/سم2",
+ "unit": "م3",
+ "components": {
+ "materials": [
+ {"الاسم": "خرسانة جاهزة", "الكمية": 1.0, "الوحدة": "م3", "سعر_الوحدة": 750.0},
+ {"الاسم": "حديد تسليح", "الكمية": 0.18, "الوحدة": "طن", "سعر_الوحدة": 5500.0}
+ ],
+ "labor": [
+ {"النوع": "عامل خرسانة", "العدد": 4, "المدة": 0.4, "سعر_اليوم": 150.0},
+ {"النوع": "نجار مسلح", "العدد": 2, "المدة": 0.7, "سعر_اليوم": 250.0},
+ {"النوع": "حداد مسلح", "العدد": 2, "المدة": 0.7, "سعر_اليوم": 250.0}
+ ],
+ "equipment": [
+ {"النوع": "هزاز خرسانة", "العدد": 2, "المدة": 0.4, "سعر_اليوم": 150.0}
+ ]
+ },
+ "admin_expenses": 0.05,
+ "profit_margin": 0.10,
+ "tags": ["خرسانة", "أعمدة", "مسلحة"]
+ },
+ "خرسانة_مسلحة_أسقف": {
+ "category": "أعمال_خرسانية",
+ "name": "خرسانة مسلحة للأسقف",
+ "description": "توريد وصب خرسانة مسلحة للأسقف والبلاطات بقوة لا تقل عن 350 كجم/سم2",
+ "unit": "م3",
+ "components": {
+ "materials": [
+ {"الاسم": "خرسانة جاهزة", "الكمية": 1.0, "الوحدة": "م3", "سعر_الوحدة": 750.0},
+ {"الاسم": "حديد تسليح", "الكمية": 0.16, "الوحدة": "طن", "سعر_الوحدة": 5500.0}
+ ],
+ "labor": [
+ {"النوع": "عامل خرسانة", "العدد": 5, "المدة": 0.5, "سعر_اليوم": 150.0},
+ {"النوع": "نجار مسلح", "العدد": 3, "المدة": 0.8, "سعر_اليوم": 250.0},
+ {"النوع": "حداد مسلح", "العدد": 3, "المدة": 0.8, "سعر_اليوم": 250.0}
+ ],
+ "equipment": [
+ {"النوع": "هزاز خرسانة", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 150.0}
+ ]
+ },
+ "admin_expenses": 0.05,
+ "profit_margin": 0.10,
+ "tags": ["خرسانة", "أسقف", "بلاطات", "مسلحة"]
+ },
+
+ # نماذج أعمال صحية
+ "منهل_تفتيش_خرساني": {
+ "category": "أعمال_صحية",
+ "name": "منهل تفتيش خرساني",
+ "description": "توريد وتركيب منهل تفتيش خرساني قطر 1 متر وعمق 2 متر",
+ "unit": "عدد",
+ "components": {
+ "materials": [
+ {"الاسم": "خرسانة جاهزة", "الكمية": 1.5, "الوحدة": "م3", "سعر_الوحدة": 750.0},
+ {"الاسم": "حديد تسليح", "الكمية": 0.15, "الوحدة": "طن", "سعر_الوحدة": 5500.0},
+ {"الاسم": "غطاء منهل حديد", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 1500.0}
+ ],
+ "labor": [
+ {"النوع": "عامل خرسانة", "العدد": 3, "المدة": 1, "سعر_اليوم": 150.0},
+ {"النوع": "نجار مسلح", "العدد": 2, "المدة": 1, "سعر_اليوم": 250.0},
+ {"النوع": "حداد مسلح", "العدد": 1, "المدة": 1, "سعر_اليوم": 250.0},
+ {"النوع": "سباك", "العدد": 2, "المدة": 1, "سعر_اليوم": 250.0}
+ ],
+ "equipment": [
+ {"النوع": "حفار صغير", "العدد": 1, "المدة": 0.5, "سعر_اليوم": 1200.0}
+ ]
+ },
+ "admin_expenses": 0.05,
+ "profit_margin": 0.12,
+ "tags": ["صرف صحي", "منهل", "تفتيش"]
+ },
+ "مواسير_بلاستيك_قطر_200_مم": {
+ "category": "أعمال_صحية",
+ "name": "مواسير بلاستيك قطر 200 مم",
+ "description": "توريد وتركيب مواسير بلاستيك UPVC قطر 200 مم لشبكات الصرف الصحي",
+ "unit": "م.ط",
+ "components": {
+ "materials": [
+ {"الاسم": "مواسير بلاستيك UPVC قطر 200 مم", "الكمية": 1.05, "الوحدة": "م.ط", "سعر_الوحدة": 180.0},
+ {"الاسم": "وصلات ومثبتات", "الكمية": 1, "الوحدة": "مجموعة", "سعر_الوحدة": 35.0},
+ {"الاسم": "مواد لاصقة", "الكمية": 0.1, "الوحدة": "لتر", "سعر_الوحدة": 120.0}
+ ],
+ "labor": [
+ {"النوع": "سباك", "العدد": 2, "المدة": 0.2, "سعر_اليوم": 250.0},
+ {"النوع": "مساعد سباك", "العدد": 2, "المدة": 0.2, "سعر_اليوم": 120.0}
+ ],
+ "equipment": [
+ {"النوع": "حفار صغير", "العدد": 1, "المدة": 0.1, "سعر_اليوم": 1200.0}
+ ]
+ },
+ "admin_expenses": 0.05,
+ "profit_margin": 0.12,
+ "tags": ["صرف صحي", "مواسير", "بلاستيك"]
+ },
+
+ # نماذج أعمال طرق
+ "طبقة_أساس_للطرق": {
+ "category": "أعمال_طرق",
+ "name": "طبقة أساس للطرق",
+ "description": "توريد وفرد ودمك طبقة أساس للطرق سمك 20 سم، درجة دمك 98%",
+ "unit": "م3",
+ "components": {
+ "materials": [
+ {"الاسم": "مواد طبقة أساس", "الكمية": 1.25, "الوحدة": "م3", "سعر_الوحدة": 90.0},
+ {"الاسم": "مياه للدمك", "الكمية": 0.2, "الوحدة": "م3", "سعر_الوحدة": 10.0}
+ ],
+ "labor": [
+ {"النوع": "عامل طرق", "العدد": 4, "المدة": 0.05, "سعر_اليوم": 150.0},
+ {"النوع": "مراقب فني", "العدد": 1, "المدة": 0.05, "سعر_اليوم": 300.0}
+ ],
+ "equipment": [
+ {"النوع": "جريدر", "العدد": 1, "المدة": 0.05, "سعر_اليوم": 2200.0},
+ {"النوع": "رصاصة دمك", "العدد": 1, "المدة": 0.05, "سعر_اليوم": 1800.0},
+ {"النوع": "شاحنة نقل", "العدد": 2, "المدة": 0.05, "سعر_اليوم": 1200.0}
+ ]
+ },
+ "admin_expenses": 0.05,
+ "profit_margin": 0.12,
+ "tags": ["طرق", "أساس", "دمك"]
+ },
+ "طبقة_إسفلت_سطحية": {
+ "category": "أعمال_طرق",
+ "name": "طبقة إسفلت سطحية",
+ "description": "توريد وفرد ودمك طبقة إسفلت سطحية سمك 5 سم",
+ "unit": "م2",
+ "components": {
+ "materials": [
+ {"الاسم": "خلطة إسفلتية ساخنة", "الكمية": 0.125, "الوحدة": "طن", "سعر_الوحدة": 400.0},
+ {"الاسم": "مواد رش تأسيسي", "الكمية": 0.5, "الوحدة": "لتر", "سعر_الوحدة": 8.0}
+ ],
+ "labor": [
+ {"النوع": "عامل طرق", "العدد": 6, "المدة": 0.01, "سعر_اليوم": 150.0},
+ {"النوع": "مراقب فني", "العدد": 1, "المدة": 0.01, "سعر_اليوم": 300.0}
+ ],
+ "equipment": [
+ {"النوع": "فرادة إسفلت", "العدد": 1, "المدة": 0.01, "سعر_اليوم": 4000.0},
+ {"النوع": "رصاصة دمك", "العدد": 2, "المدة": 0.01, "سعر_اليوم": 1800.0},
+ {"النوع": "سيارة رش إسفلت", "العدد": 1, "المدة": 0.01, "سعر_اليوم": 2000.0},
+ {"النوع": "شاحنة نقل", "العدد": 4, "المدة": 0.01, "سعر_اليوم": 1200.0}
+ ]
+ },
+ "admin_expenses": 0.05,
+ "profit_margin": 0.12,
+ "tags": ["طرق", "إسفلت", "سطحية"]
+ },
+
+ # نماذج أعمال كهربائية
+ "عمود_إنارة_10_متر": {
+ "category": "أعمال_كهربائية",
+ "name": "عمود إنارة 10 متر",
+ "description": "توريد وتركيب عمود إنارة جلفانيزي بارتفاع 10 متر مع ذراع مفردة وكشاف LED بقدرة 150 واط",
+ "unit": "عدد",
+ "components": {
+ "materials": [
+ {"الاسم": "عمود إنارة جلفانيزي 10 متر", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 3500.0},
+ {"الاسم": "ذراع إنارة مفردة", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 450.0},
+ {"الاسم": "كشاف LED 150 واط", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 850.0},
+ {"الاسم": "كابل كهرباء 3×4 مم²", "الكمية": 15, "الوحدة": "م.ط", "سعر_الوحدة": 32.0},
+ {"الاسم": "قاعدة خرسانية مسلحة", "الكمية": 0.25, "الوحدة": "م3", "سعر_الوحدة": 750.0}
+ ],
+ "labor": [
+ {"النوع": "كهربائي", "العدد": 2, "المدة": 1, "سعر_اليوم": 270.0},
+ {"النوع": "مساعد كهربائي", "العدد": 2, "المدة": 1, "سعر_اليوم": 120.0},
+ {"النوع": "عامل خرسانة", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 150.0}
+ ],
+ "equipment": [
+ {"النوع": "ونش شوكة", "العدد": 1, "المدة": 0.5, "سعر_اليوم": 1500.0},
+ {"النوع": "حفار صغير", "العدد": 1, "المدة": 0.2, "سعر_اليوم": 1200.0}
+ ]
+ },
+ "admin_expenses": 0.05,
+ "profit_margin": 0.12,
+ "tags": ["كهرباء", "إنارة", "LED"]
+ }
+ }
+ }
+
+ return templates
+
+ def _create_default_market_prices(self) -> Dict[str, Dict[str, Any]]:
+ """إنشاء بيانات افتراضية لأسعار السوق السعودي"""
+ current_date = datetime.now().strftime("%Y-%m-%d")
+
+ prices = {
+ "metadata": {
+ "last_update": current_date,
+ "source": "أسعار السوق السعودي الافتراضية",
+ "disclaimer": "هذه الأسعار تقريبية وقد تختلف حسب المنطقة والكميات والموردين"
+ },
+ "materials": {
+ # مواد الخرسانة
+ "خرسانة_جاهزة": {
+ "name": "خرسانة جاهزة",
+ "unit": "م3",
+ "current_price": 750.0,
+ "previous_price": 730.0,
+ "price_trend": "up",
+ "category": "أعمال خرسانية",
+ "specifications": "خرسانة جاهزة بقوة 350 كجم/سم2",
+ "note": "السعر يشمل توريد فقط، الضخ بتكلفة إضافية",
+ "price_history": [
+ {"date": "2023-06-01", "price": 700.0},
+ {"date": "2023-09-01", "price": 715.0},
+ {"date": "2023-12-01", "price": 730.0},
+ {"date": current_date, "price": 750.0}
+ ]
+ },
+ "حديد_تسليح": {
+ "name": "حديد تسليح",
+ "unit": "طن",
+ "current_price": 5500.0,
+ "previous_price": 5200.0,
+ "price_trend": "up",
+ "category": "أعمال خرسانية",
+ "specifications": "حديد تسليح قطر 8-32 مم، انتاج سابك",
+ "note": "السعر يتغير بشكل دوري حسب أسعار الحديد العالمية",
+ "price_history": [
+ {"date": "2023-06-01", "price": 4800.0},
+ {"date": "2023-09-01", "price": 5000.0},
+ {"date": "2023-12-01", "price": 5200.0},
+ {"date": current_date, "price": 5500.0}
+ ]
+ },
+ "أسمنت": {
+ "name": "أسمنت",
+ "unit": "كيس",
+ "current_price": 30.0,
+ "previous_price": 28.0,
+ "price_trend": "up",
+ "category": "أعمال خرسانية",
+ "specifications": "أسمنت بورتلاندي عادي، كيس 50 كجم",
+ "note": "السعر للكميات الكبيرة",
+ "price_history": [
+ {"date": "2023-06-01", "price": 25.0},
+ {"date": "2023-09-01", "price": 27.0},
+ {"date": "2023-12-01", "price": 28.0},
+ {"date": current_date, "price": 30.0}
+ ]
+ },
+
+ # مواد الطرق والإسفلت
+ "خلطة_إسفلتية_ساخنة": {
+ "name": "خلطة إسفلتية ساخنة",
+ "unit": "طن",
+ "current_price": 400.0,
+ "previous_price": 380.0,
+ "price_trend": "up",
+ "category": "أعمال طرق",
+ "specifications": "خلطة إسفلتية ساخنة للطبقة السطحية",
+ "note": "السعر يشمل التوريد من المصنع، النقل بتكلفة إضافية",
+ "price_history": [
+ {"date": "2023-06-01", "price": 350.0},
+ {"date": "2023-09-01", "price": 370.0},
+ {"date": "2023-12-01", "price": 380.0},
+ {"date": current_date, "price": 400.0}
+ ]
+ },
+
+ # مواد صحية
+ "مواسير_بلاستيك_UPVC": {
+ "name": "مواسير بلاستيك UPVC قطر 200 مم",
+ "unit": "م.ط",
+ "current_price": 180.0,
+ "previous_price": 165.0,
+ "price_trend": "up",
+ "category": "أعمال صحية",
+ "specifications": "مواسير بلاستيك UPVC قطر 200 مم لشبكات الصرف الصحي",
+ "note": "السعر للكميات الكبيرة",
+ "price_history": [
+ {"date": "2023-06-01", "price": 150.0},
+ {"date": "2023-09-01", "price": 160.0},
+ {"date": "2023-12-01", "price": 165.0},
+ {"date": current_date, "price": 180.0}
+ ]
+ },
+
+ # مواد كهربائية
+ "كشاف_LED": {
+ "name": "كشاف LED 150 واط",
+ "unit": "عدد",
+ "current_price": 850.0,
+ "previous_price": 820.0,
+ "price_trend": "up",
+ "category": "أعمال كهربائية",
+ "specifications": "كشاف إنارة LED بقدرة 150 واط للاستخدام الخارجي، IP65",
+ "note": "السعر شامل الضريبة",
+ "price_history": [
+ {"date": "2023-06-01", "price": 780.0},
+ {"date": "2023-09-01", "price": 800.0},
+ {"date": "2023-12-01", "price": 820.0},
+ {"date": current_date, "price": 850.0}
+ ]
+ }
+ },
+ "labor": {
+ "عامل_خرسانة": {
+ "name": "عامل خرسانة",
+ "unit": "يوم",
+ "current_price": 150.0,
+ "previous_price": 140.0,
+ "price_trend": "up",
+ "category": "عمالة",
+ "specifications": "عامل لصب وتسوية الخرسانة",
+ "note": "أجرة اليوم الواحد لا تشمل السكن والمواصلات",
+ "price_history": [
+ {"date": "2023-06-01", "price": 130.0},
+ {"date": "2023-09-01", "price": 135.0},
+ {"date": "2023-12-01", "price": 140.0},
+ {"date": current_date, "price": 150.0}
+ ]
+ },
+ "مهندس_موقع": {
+ "name": "مهندس موقع",
+ "unit": "يوم",
+ "current_price": 500.0,
+ "previous_price": 480.0,
+ "price_trend": "up",
+ "category": "إشراف",
+ "specifications": "مهندس إشراف موقع",
+ "note": "أجرة اليوم الواحد لا تشمل السكن والمواصلات",
+ "price_history": [
+ {"date": "2023-06-01", "price": 450.0},
+ {"date": "2023-09-01", "price": 470.0},
+ {"date": "2023-12-01", "price": 480.0},
+ {"date": current_date, "price": 500.0}
+ ]
+ }
+ },
+ "equipment": {
+ "حفار_صغير": {
+ "name": "حفار صغير",
+ "unit": "يوم",
+ "current_price": 1200.0,
+ "previous_price": 1150.0,
+ "price_trend": "up",
+ "category": "معدات حفر",
+ "specifications": "حفار صغير (بوبكات) بقدرة 70 حصان",
+ "note": "السعر يشمل المشغل والوقود",
+ "price_history": [
+ {"date": "2023-06-01", "price": 1100.0},
+ {"date": "2023-09-01", "price": 1120.0},
+ {"date": "2023-12-01", "price": 1150.0},
+ {"date": current_date, "price": 1200.0}
+ ]
+ },
+ "فرادة_إسفلت": {
+ "name": "فرادة إسفلت",
+ "unit": "يوم",
+ "current_price": 4000.0,
+ "previous_price": 3800.0,
+ "price_trend": "up",
+ "category": "معدات طرق",
+ "specifications": "فرادة إسفلت بعرض 3 متر",
+ "note": "السعر يشمل المشغل والوقود",
+ "price_history": [
+ {"date": "2023-06-01", "price": 3500.0},
+ {"date": "2023-09-01", "price": 3650.0},
+ {"date": "2023-12-01", "price": 3800.0},
+ {"date": current_date, "price": 4000.0}
+ ]
+ }
+ }
+ }
+
+ return prices
+
+ def get_all_templates(self) -> Dict[str, Dict[str, Any]]:
+ """الحصول على جميع القوالب النموذجية"""
+ return self.templates
+
+ def get_templates_by_category(self, category_id: str) -> List[Dict[str, Any]]:
+ """الحصول على القوالب النموذجية حسب الفئة"""
+ result = []
+
+ # التحقق من وجود الفئة
+ if category_id not in self.templates["categories"]:
+ return result
+
+ # جمع القوالب التي تنتمي إلى الفئة المحددة
+ for template_id, template in self.templates["templates"].items():
+ if template["category"] == category_id:
+ template_copy = template.copy()
+ template_copy["id"] = template_id
+ result.append(template_copy)
+
+ return result
+
+ def get_template_by_id(self, template_id: str) -> Optional[Dict[str, Any]]:
+ """الحصول على قالب نموذجي بواسطة المعرف"""
+ if template_id in self.templates["templates"]:
+ template = self.templates["templates"][template_id].copy()
+ template["id"] = template_id
+ return template
+
+ return None
+
+ def add_template(self, template_data: Dict[str, Any]) -> str:
+ """إضافة قالب نموذجي جديد"""
+ # إنشاء معرف فريد للقالب
+ template_name = template_data.get("name", "").strip()
+ if not template_name:
+ raise ValueError("يجب تحديد اسم القالب")
+
+ # تحويل الاسم إلى معرف (باستبدال المسافات بالشرطات السفلية وإزالة الأحرف الخاصة)
+ import re
+ template_id = re.sub(r'[^\w\s]', '', template_name)
+ template_id = template_id.replace(" ", "_")
+
+ # إضافة رقم عشوائي لتجنب التكرار
+ import random
+ if template_id in self.templates["templates"]:
+ template_id = f"{template_id}_{random.randint(1000, 9999)}"
+
+ # إضافة القالب إلى القائمة
+ self.templates["templates"][template_id] = template_data
+
+ # حفظ التغييرات
+ self._save_templates(self.templates)
+
+ return template_id
+
+ def update_template(self, template_id: str, template_data: Dict[str, Any]) -> bool:
+ """تحديث قالب نموذجي موجود"""
+ if template_id not in self.templates["templates"]:
+ return False
+
+ # تحديث القالب
+ self.templates["templates"][template_id] = template_data
+
+ # حفظ التغييرات
+ self._save_templates(self.templates)
+
+ return True
+
+ def delete_template(self, template_id: str) -> bool:
+ """حذف قالب نموذجي"""
+ if template_id not in self.templates["templates"]:
+ return False
+
+ # حذف القالب
+ del self.templates["templates"][template_id]
+
+ # حفظ التغييرات
+ self._save_templates(self.templates)
+
+ return True
+
+ def get_market_prices(self, category: Optional[str] = None, item_type: Optional[str] = None) -> Dict[str, Any]:
+ """الحصول على أسعار السوق السعودي"""
+ result = {
+ "metadata": self.market_prices["metadata"]
+ }
+
+ # تحديد نوع العناصر المطلوبة
+ sections = []
+ if item_type:
+ if item_type in ["materials", "المواد"]:
+ sections = ["materials"]
+ elif item_type in ["labor", "العمالة"]:
+ sections = ["labor"]
+ elif item_type in ["equipment", "المعدات"]:
+ sections = ["equipment"]
+ else:
+ sections = ["materials", "labor", "equipment"]
+
+ # جمع العناصر
+ for section in sections:
+ result[section] = {}
+ for item_id, item_data in self.market_prices[section].items():
+ if not category or (item_data.get("category", "") == category):
+ result[section][item_id] = item_data
+
+ return result
+
+ def update_market_price(self, item_type: str, item_id: str, new_price: float) -> bool:
+ """تحديث سعر في قائمة أسعار السوق"""
+ section = ""
+ if item_type in ["materials", "المواد"]:
+ section = "materials"
+ elif item_type in ["labor", "العمالة"]:
+ section = "labor"
+ elif item_type in ["equipment", "المعدات"]:
+ section = "equipment"
+ else:
+ return False
+
+ if item_id not in self.market_prices[section]:
+ return False
+
+ # تحديث السعر
+ current_price = self.market_prices[section][item_id]["current_price"]
+ self.market_prices[section][item_id]["previous_price"] = current_price
+ self.market_prices[section][item_id]["current_price"] = new_price
+
+ # تحديد اتجاه السعر
+ if new_price > current_price:
+ self.market_prices[section][item_id]["price_trend"] = "up"
+ elif new_price < current_price:
+ self.market_prices[section][item_id]["price_trend"] = "down"
+ else:
+ self.market_prices[section][item_id]["price_trend"] = "stable"
+
+ # إضافة السعر الجديد إلى تاريخ الأسعار
+ current_date = datetime.now().strftime("%Y-%m-%d")
+ self.market_prices[section][item_id]["price_history"].append({
+ "date": current_date,
+ "price": new_price
+ })
+
+ # تحديث تاريخ آخر تحديث
+ self.market_prices["metadata"]["last_update"] = current_date
+
+ # حفظ التغييرات
+ self._save_market_prices(self.market_prices)
+
+ return True
+
+ def add_market_price_item(self, item_type: str, item_data: Dict[str, Any]) -> str:
+ """إضافة عنصر جديد إلى قائمة أسعار السوق"""
+ section = ""
+ if item_type in ["materials", "المواد"]:
+ section = "materials"
+ elif item_type in ["labor", "العمالة"]:
+ section = "labor"
+ elif item_type in ["equipment", "المعدات"]:
+ section = "equipment"
+ else:
+ raise ValueError("نوع العنصر غير صحيح")
+
+ # التحقق من البيانات الأساسية
+ if "name" not in item_data or "current_price" not in item_data or "unit" not in item_data:
+ raise ValueError("يجب تحديد الاسم والسعر الحالي والوحدة")
+
+ # إنشاء معرف فريد للعنصر
+ item_name = item_data["name"].strip()
+ import re
+ item_id = re.sub(r'[^\w\s]', '', item_name)
+ item_id = item_id.replace(" ", "_")
+
+ # إضافة رقم عشوائي لتجنب التكرار
+ import random
+ if item_id in self.market_prices[section]:
+ item_id = f"{item_id}_{random.randint(1000, 9999)}"
+
+ # إعداد بيانات العنصر
+ current_date = datetime.now().strftime("%Y-%m-%d")
+ new_item = {
+ "name": item_name,
+ "unit": item_data["unit"],
+ "current_price": item_data["current_price"],
+ "previous_price": item_data.get("previous_price", item_data["current_price"]),
+ "price_trend": "stable",
+ "category": item_data.get("category", ""),
+ "specifications": item_data.get("specifications", ""),
+ "note": item_data.get("note", ""),
+ "price_history": [
+ {"date": current_date, "price": item_data["current_price"]}
+ ]
+ }
+
+ # إضافة العنصر إلى القائمة
+ self.market_prices[section][item_id] = new_item
+
+ # تحديث تاريخ آخر تحديث
+ self.market_prices["metadata"]["last_update"] = current_date
+
+ # حفظ التغييرات
+ self._save_market_prices(self.market_prices)
+
+ return item_id
+
+ def convert_template_to_item(self, template_id: str) -> Dict[str, Any]:
+ """تحويل قالب نموذجي إلى بند للاستخدام في حاسبة تكاليف البناء"""
+ template = self.get_template_by_id(template_id)
+ if not template:
+ raise ValueError("القالب غير موجود")
+
+ # تحويل القالب إلى صيغة بند
+ item = {
+ "وصف_البند": template["description"],
+ "الكمية": 1.0,
+ "الوحدة": template["unit"],
+ "المواد": template["components"]["materials"],
+ "العمالة": template["components"]["labor"],
+ "المعدات": template["components"]["equipment"],
+ "المصاريف_الإدارية": template["admin_expenses"],
+ "هامش_الربح": template["profit_margin"],
+ "عوامل_التعديل": {
+ "location_factor": 1.0,
+ "time_factor": 1.0,
+ "risk_factor": 1.0,
+ "market_factor": 1.0
+ }
+ }
+
+ return item
\ No newline at end of file
diff --git a/modules/pricing/services/local_content_calculator.py b/modules/pricing/services/local_content_calculator.py
new file mode 100644
index 0000000000000000000000000000000000000000..859c3d004512fc247e2a801608da335bc4c07f29
--- /dev/null
+++ b/modules/pricing/services/local_content_calculator.py
@@ -0,0 +1,577 @@
+"""
+خدمة حساب المحتوى المحلي
+"""
+import pandas as pd
+import numpy as np
+from datetime import datetime
+import os
+import config
+
+class LocalContentCalculator:
+ """خدمة حساب وتحسين المحتوى المحلي"""
+
+ def __init__(self):
+ """تهيئة خدمة حساب المحتوى المحلي"""
+ # تحميل بيانات المواد المحلية ونسب المحتوى المحلي
+ self.local_products = self._load_local_products()
+ self.local_services = self._load_local_services()
+ self.local_labor = self._load_local_labor()
+
+ # تحديد الأوزان النسبية لمكونات المحتوى المحلي
+ self.component_weights = {
+ 'القوى العاملة': 0.3, # 30% من وزن المحتوى المحلي
+ 'المنتجات': 0.5, # 50% من وزن المحتوى المحلي
+ 'الخدمات': 0.2 # 20% من وزن المحتوى المحلي
+ }
+
+ # تحديد المستهدفات (متطلبات المحتوى المحلي)
+ self.targets = {
+ 'القوى العاملة': 0.8, # 80% محتوى محلي للقوى العاملة
+ 'المنتجات': 0.7, # 70% محتوى محلي للمنتجات
+ 'الخدمات': 0.6 # 60% محتوى محلي للخدمات
+ }
+
+ def _load_local_products(self):
+ """تحميل بيانات المنتجات المحلية ونسب المحتوى المحلي"""
+ # محاكاة تحميل البيانات من مصدر بيانات
+ local_products = {
+ 'خرسانة': {
+ 'نسبة_المحتوى_المحلي': 0.95, # 95% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي',
+ 'ملاحظات': 'منتج محلي بالكامل'
+ },
+ 'حديد تسليح': {
+ 'نسبة_المحتوى_المحلي': 0.70, # 70% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي/مستورد',
+ 'ملاحظات': 'متوفر من مصانع محلية ومستورد'
+ },
+ 'عزل مائي': {
+ 'نسبة_المحتوى_المحلي': 0.60, # 60% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي/مستورد',
+ 'ملاحظات': 'منتج محلي متوفر بجودة معقولة'
+ },
+ 'بلوك خرساني': {
+ 'نسبة_المحتوى_المحلي': 0.98, # 98% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي',
+ 'ملاحظات': 'منتج محلي بالكامل'
+ },
+ 'رخام': {
+ 'نسبة_المحتوى_المحلي': 0.80, # 80% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي',
+ 'ملاحظات': 'متوفر من محاجر محلية'
+ },
+ 'أثاث مكتبي': {
+ 'نسبة_المحتوى_المحلي': 0.75, # 75% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي',
+ 'ملاحظات': 'يُصنع محليًا ويستخدم بعض المكونات المستوردة'
+ },
+ 'أجهزة تكييف': {
+ 'نسبة_المحتوى_المحلي': 0.40, # 40% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي/مستورد',
+ 'ملاحظات': 'تجميع محلي مع مكونات مستوردة'
+ },
+ 'أنظمة إضاءة': {
+ 'نسبة_المحتوى_المحلي': 0.55, # 55% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي/مستورد',
+ 'ملاحظات': 'متوفر محليًا وبجودة متفاوتة'
+ },
+ 'زجاج': {
+ 'نسبة_المحتوى_المحلي': 0.65, # 65% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي/مستورد',
+ 'ملاحظات': 'إنتاج محلي بمواصفات جيدة'
+ },
+ 'أسلاك كهربائية': {
+ 'نسبة_المحتوى_المحلي': 0.85, # 85% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي',
+ 'ملاحظات': 'تصنيع محلي بجودة عالية'
+ }
+ }
+
+ # محاولة تحميل البيانات من ملف إذا كان متاحًا
+ try:
+ file_path = os.path.join(config.DATA_DIR, 'local_products.csv')
+ if os.path.exists(file_path):
+ df = pd.read_csv(file_path, encoding='utf-8')
+ local_products = {}
+ for _, row in df.iterrows():
+ local_products[row['اسم_المنتج']] = {
+ 'نسبة_المحتوى_المحلي': row['نسبة_المحتوى_المحلي'],
+ 'بديل_محلي': row['بديل_محلي'],
+ 'مصدر': row['مصدر'],
+ 'ملاحظات': row['ملاحظات']
+ }
+ except Exception as e:
+ print(f"خطأ في تحميل بيانات المنتجات المحلية: {str(e)}")
+
+ return local_products
+
+ def _load_local_services(self):
+ """تحميل بيانات الخدمات المحلية ونسب المحتوى المحلي"""
+ # محاكاة تحميل البيانات من مصدر بيانات
+ local_services = {
+ 'تصميم معماري': {
+ 'نسبة_المحتوى_المحلي': 0.90, # 90% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي',
+ 'ملاحظات': 'متوفرة من مكاتب استشارية محلية'
+ },
+ 'إشراف هندسي': {
+ 'نسبة_المحتوى_المحلي': 0.85, # 85% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي',
+ 'ملاحظات': 'متوفر من شركات محلية'
+ },
+ 'خدمات تنسيق المواقع': {
+ 'نسبة_المحتوى_المحلي': 0.80, # 80% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي',
+ 'ملاحظات': 'شركات محلية متخصصة'
+ },
+ 'خدمات أمن وسلامة': {
+ 'نسبة_المحتوى_المحلي': 0.95, # 95% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي',
+ 'ملاحظات': 'شركات محلية متخصصة'
+ },
+ 'استشارات بيئية': {
+ 'نسبة_المحتوى_المحلي': 0.65, # 65% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي/دولي',
+ 'ملاحظات': 'متوفرة محليًا مع بعض الخبرات الأجنبية'
+ },
+ 'دراسات جدوى': {
+ 'نسبة_المحتوى_المحلي': 0.70, # 70% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي/دولي',
+ 'ملاحظات': 'متوفرة من مكاتب استشارية محلية'
+ },
+ 'خدمات نقل': {
+ 'نسبة_المحتوى_المحلي': 0.90, # 90% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي',
+ 'ملاحظات': 'شركات نقل محلية متعددة'
+ },
+ 'صيانة ونظافة': {
+ 'نسبة_المحتوى_المحلي': 0.95, # 95% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي',
+ 'ملاحظات': 'شركات محلية متخصصة'
+ }
+ }
+
+ # محاولة تحميل البيانات من ملف إذا كان متاحًا
+ try:
+ file_path = os.path.join(config.DATA_DIR, 'local_services.csv')
+ if os.path.exists(file_path):
+ df = pd.read_csv(file_path, encoding='utf-8')
+ local_services = {}
+ for _, row in df.iterrows():
+ local_services[row['اسم_الخدمة']] = {
+ 'نسبة_المحتوى_المحلي': row['نسبة_المحتوى_المحلي'],
+ 'بديل_محلي': row['بديل_محلي'],
+ 'مصدر': row['مصدر'],
+ 'ملاحظات': row['ملاحظات']
+ }
+ except Exception as e:
+ print(f"خطأ في تحميل بيانات الخدمات المحلية: {str(e)}")
+
+ return local_services
+
+ def _load_local_labor(self):
+ """تحميل بيانات القوى العاملة المحلية ونسب المحتوى المحلي"""
+ # محاكاة تحميل البيانات من مصدر بيانات
+ local_labor = {
+ 'عمال بناء': {
+ 'نسبة_المحتوى_المحلي': 0.60, # 60% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي/أجنبي',
+ 'ملاحظات': 'متوفر محليًا مع نسبة من العمالة الأجنبية'
+ },
+ 'مهندسون': {
+ 'نسبة_المحتوى_المحلي': 0.75, # 75% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي/أجنبي',
+ 'ملاحظات': 'كفاءات محلية متوفرة'
+ },
+ 'فنيون': {
+ 'نسبة_المحتوى_المحلي': 0.65, # 65% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي/أجنبي',
+ 'ملاحظات': 'متوفر محليًا بنسب متفاوتة'
+ },
+ 'إداريون': {
+ 'نسبة_المحتوى_المحلي': 0.90, # 90% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي',
+ 'ملاحظات': 'معظمهم من الكوادر المحلية'
+ },
+ 'مشرفون': {
+ 'نسبة_المحتوى_المحلي': 0.80, # 80% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي',
+ 'ملاحظات': 'معظمهم من الكوادر المحلية'
+ },
+ 'مصممون': {
+ 'نسبة_المحتوى_المحلي': 0.70, # 70% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي/أجنبي',
+ 'ملاحظات': 'كفاءات محلية مع بعض الخبرات الأجنبية'
+ },
+ 'عمال مهرة': {
+ 'نسبة_المحتوى_المحلي': 0.55, # 55% محتوى محلي
+ 'بديل_محلي': True,
+ 'مصدر': 'محلي/أجنبي',
+ 'ملاحظات': 'نسبة من العمالة الأجنبية ذات الخبرة'
+ }
+ }
+
+ # محاولة تحميل البيانات من ملف إذا كان متاحًا
+ try:
+ file_path = os.path.join(config.DATA_DIR, 'local_labor.csv')
+ if os.path.exists(file_path):
+ df = pd.read_csv(file_path, encoding='utf-8')
+ local_labor = {}
+ for _, row in df.iterrows():
+ local_labor[row['فئة_العمالة']] = {
+ 'نسبة_المحتوى_المحلي': row['نسبة_المحتوى_المحلي'],
+ 'بديل_محلي': row['بديل_محلي'],
+ 'مصدر': row['مصدر'],
+ 'ملاحظات': row['ملاحظات']
+ }
+ except Exception as e:
+ print(f"خطأ في تحميل بيانات القوى العاملة المحلية: {str(e)}")
+
+ return local_labor
+
+ def calculate_project_local_content(self, project_data):
+ """
+ حساب نسبة المحتوى المحلي للمشروع
+
+ المعلمات:
+ project_data: بيانات المشروع، تتضمن مكونات المنتجات والخدمات والقوى العاملة
+
+ إرجاع:
+ نسبة المحتوى المحلي الإجمالية، وتفاصيل حسب كل مكون
+ """
+ # تهيئة نتائج الحساب
+ results = {
+ 'نسبة_المحتوى_المحلي_الإجمالية': 0,
+ 'تفاصيل_المكونات': {
+ 'المنتجات': {'نسبة': 0, 'تفاصيل': {}},
+ 'الخدمات': {'نسبة': 0, 'تفاصيل': {}},
+ 'القوى العاملة': {'نسبة': 0, 'تفاصيل': {}}
+ },
+ 'ملخص_المحتوى_المحلي': {},
+ 'توصيات_التحسين': []
+ }
+
+ # حساب نسبة المحتوى المحلي للمنتجات
+ if 'المنتجات' in project_data:
+ products_local_content = self._calculate_products_local_content(project_data['المنتجات'])
+ results['تفاصيل_المكونات']['المنتجات'] = products_local_content
+
+ # حساب نسبة المحتوى المحلي للخدمات
+ if 'الخدمات' in project_data:
+ services_local_content = self._calculate_services_local_content(project_data['الخدمات'])
+ results['تفاصيل_المكونات']['الخدمات'] = services_local_content
+
+ # حساب نسبة المحتوى المحلي للقوى العاملة
+ if 'القوى العاملة' in project_data:
+ labor_local_content = self._calculate_labor_local_content(project_data['القوى العاملة'])
+ results['تفاصيل_المكونات']['القوى العاملة'] = labor_local_content
+
+ # حساب النسبة الإجمالية للمحتوى المحلي بناءً على الأوزان النسبية
+ total_local_content = 0
+ for component, weight in self.component_weights.items():
+ if component in results['تفاصيل_المكونات']:
+ component_percentage = results['تفاصيل_المكونات'][component]['نسبة']
+ total_local_content += component_percentage * weight
+
+ results['نسبة_المحتوى_المحلي_الإجمالية'] = total_local_content
+
+ # تحديد ملخص المحتوى المحلي ومقارنته بالمستهدف
+ for component, target in self.targets.items():
+ if component in results['تفاصيل_المكونات']:
+ actual = results['تفاصيل_المكونات'][component]['نسبة']
+ status = 'مطابق' if actual >= target else 'غير مطابق'
+ gap = round((target - actual) * 100, 2) if actual < target else 0
+
+ results['ملخص_المحتوى_المحلي'][component] = {
+ 'المستهدف': target * 100,
+ 'الفعلي': round(actual * 100, 2),
+ 'الحالة': status,
+ 'الفجوة (%)': gap
+ }
+
+ # توليد توصيات لتحسين نسبة المحتوى المحلي
+ results['توصيات_التحسين'] = self._generate_improvement_recommendations(results)
+
+ return results
+
+ def _calculate_products_local_content(self, products_data):
+ """
+ حساب نسبة المحتوى المحلي للمنتجات
+
+ المعلمات:
+ products_data: بيانات المنتجات المستخدمة في المشروع
+
+ إرجاع:
+ تفاصيل نسبة المحتوى المحلي للمنتجات
+ """
+ total_cost = 0
+ local_content_value = 0
+ details = {}
+
+ for product_name, product_info in products_data.items():
+ quantity = product_info.get('الكمية', 0)
+ unit_price = product_info.get('سعر_الوحدة', 0)
+ total_product_cost = quantity * unit_price
+
+ # البحث عن نسبة المحتوى المحلي للمنتج
+ local_content_percentage = 0
+ if product_name in self.local_products:
+ local_content_percentage = self.local_products[product_name]['نسبة_المحتوى_المحلي']
+
+ # حساب قيمة المحتوى المحلي للمنتج
+ product_local_content_value = total_product_cost * local_content_percentage
+
+ # تحديث الإجماليات
+ total_cost += total_product_cost
+ local_content_value += product_local_content_value
+
+ # تسجيل التفاصيل
+ details[product_name] = {
+ 'الكمية': quantity,
+ 'سعر_الوحدة': unit_price,
+ 'التكلفة_الإجمالية': total_product_cost,
+ 'نسبة_المحتوى_المحلي': local_content_percentage,
+ 'قيمة_المحتوى_المحلي': product_local_content_value,
+ 'مصدر': self.local_products.get(product_name, {}).get('مصدر', 'غير معروف'),
+ 'ملاحظات': self.local_products.get(product_name, {}).get('ملاحظات', '')
+ }
+
+ # حساب النسبة الإجمالية للمحتوى المحلي للمنتجات
+ local_content_percentage = local_content_value / total_cost if total_cost > 0 else 0
+
+ return {
+ 'نسبة': local_content_percentage,
+ 'إجمالي_التكلفة': total_cost,
+ 'قيمة_المحتوى_المحلي': local_content_value,
+ 'تفاصيل': details
+ }
+
+ def _calculate_services_local_content(self, services_data):
+ """
+ حساب نسبة المحتوى المحلي للخدمات
+
+ المعلمات:
+ services_data: بيانات الخدمات المستخدمة في المشروع
+
+ إرجاع:
+ تفاصيل نسبة المحتوى المحلي للخدمات
+ """
+ total_cost = 0
+ local_content_value = 0
+ details = {}
+
+ for service_name, service_info in services_data.items():
+ cost = service_info.get('التكلفة', 0)
+
+ # البحث عن نسبة المحتوى المحلي للخدمة
+ local_content_percentage = 0
+ if service_name in self.local_services:
+ local_content_percentage = self.local_services[service_name]['نسبة_المحتوى_المحلي']
+
+ # حساب قيمة المحتوى المحلي للخدمة
+ service_local_content_value = cost * local_content_percentage
+
+ # تحديث الإجماليات
+ total_cost += cost
+ local_content_value += service_local_content_value
+
+ # تسجيل التفاصيل
+ details[service_name] = {
+ 'التكلفة': cost,
+ 'نسبة_المحتوى_المحلي': local_content_percentage,
+ 'قيمة_المحتوى_المحلي': service_local_content_value,
+ 'مصدر': self.local_services.get(service_name, {}).get('مصدر', 'غير معروف'),
+ 'ملاحظات': self.local_services.get(service_name, {}).get('ملاحظات', '')
+ }
+
+ # حساب النسبة الإجمالية للمحتوى المحلي للخدمات
+ local_content_percentage = local_content_value / total_cost if total_cost > 0 else 0
+
+ return {
+ 'نسبة': local_content_percentage,
+ 'إجمالي_التكلفة': total_cost,
+ 'قيمة_المحتوى_المحلي': local_content_value,
+ 'تفاصيل': details
+ }
+
+ def _calculate_labor_local_content(self, labor_data):
+ """
+ حساب نسبة المحتوى المحلي للقوى العاملة
+
+ المعلمات:
+ labor_data: بيانات القوى العاملة المستخدمة في المشروع
+
+ إرجاع:
+ تفاصيل نسبة المحتوى المحلي للقوى العاملة
+ """
+ total_cost = 0
+ local_content_value = 0
+ details = {}
+
+ for labor_type, labor_info in labor_data.items():
+ count = labor_info.get('العدد', 0)
+ monthly_salary = labor_info.get('الراتب_الشهري', 0)
+ duration_months = labor_info.get('المدة_بالأشهر', 0)
+
+ total_labor_cost = count * monthly_salary * duration_months
+
+ # البحث عن نسبة المحتوى المحلي للقوى العاملة
+ local_content_percentage = 0
+ if labor_type in self.local_labor:
+ local_content_percentage = self.local_labor[labor_type]['نسبة_المحتوى_المحلي']
+
+ # حساب قيمة المحتوى المحلي للقوى العاملة
+ labor_local_content_value = total_labor_cost * local_content_percentage
+
+ # تحديث الإجماليات
+ total_cost += total_labor_cost
+ local_content_value += labor_local_content_value
+
+ # تسجيل التفاصيل
+ details[labor_type] = {
+ 'العدد': count,
+ 'الراتب_الشهري': monthly_salary,
+ 'المدة_بالأشهر': duration_months,
+ 'التكلفة_الإجمالية': total_labor_cost,
+ 'نسبة_المحتوى_المحلي': local_content_percentage,
+ 'قيمة_المحتوى_المحلي': labor_local_content_value,
+ 'مصدر': self.local_labor.get(labor_type, {}).get('مصدر', 'غير معروف'),
+ 'ملاحظات': self.local_labor.get(labor_type, {}).get('ملاحظات', '')
+ }
+
+ # حساب النسبة الإجمالية للمحتوى المحلي للقوى العاملة
+ local_content_percentage = local_content_value / total_cost if total_cost > 0 else 0
+
+ return {
+ 'نسبة': local_content_percentage,
+ 'إجمالي_التكلفة': total_cost,
+ 'قيمة_المحتوى_المحلي': local_content_value,
+ 'تفاصيل': details
+ }
+
+ def _generate_improvement_recommendations(self, results):
+ """
+ توليد توصيات لتحسين نسبة المحتوى المحلي
+
+ المعلمات:
+ results: نتائج حساب المحتوى المحلي
+
+ إرجاع:
+ قائمة بالتوصيات لتحسين نسبة المحتوى المحلي
+ """
+ recommendations = []
+
+ # تحليل المكونات التي تحتاج إلى تحسين
+ for component, summary in results['ملخص_المحتوى_المحلي'].items():
+ if summary['الحالة'] == 'غير مطابق':
+ if component == 'المنتجات':
+ # تحديد المنتجات ذات المحتوى المحلي المنخفض
+ low_content_products = []
+ for product, details in results['تفاصيل_المكونات']['المنتجات']['تفاصيل'].items():
+ if details['نسبة_المحتوى_المحلي'] < 0.5: # أقل من 50%
+ low_content_products.append({
+ 'اسم': product,
+ 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'],
+ 'التكلفة_الإجمالية': details['التكلفة_الإجمالية']
+ })
+
+ elif component == 'الخدمات':
+ # تحديد البنود ذات المحتوى المحلي المنخفض
+ low_content_services = []
+ for service, details in results['تفاصيل_المكونات']['الخدمات']['تفاصيل'].items():
+ if details['نسبة_المحتوى_المحلي'] < 0.5: # أقل من 50%
+ low_content_services.append({
+ 'اسم': service,
+ 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'],
+ 'التكلفة': details['التكلفة']
+ })
+
+ elif component == 'القوى العاملة':
+ # تحديد فئات العمالة ذات المحتوى المحلي المنخفض
+ low_content_labor = []
+ for labor_type, details in results['تفاصيل_المكونات']['القوى العاملة']['تفاصيل'].items():
+ if details['نسبة_المحتوى_المحلي'] < 0.5: # أقل من 50%
+ low_content_labor.append({
+ 'اسم': labor_type,
+ 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'],
+ 'التكلفة_الإجمالية': details['التكلفة_الإجمالية']
+ })
+
+ # إنشاء توصيات لتحسين المحتوى المحلي
+ # توصيات للمنتجات
+ if 'المنتجات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['المنتجات']['الحالة'] == 'غير مطابق':
+ low_content_products = []
+ for product, details in results['تفاصيل_المكونات']['المنتجات']['تفاصيل'].items():
+ if details['نسبة_المحتوى_المحلي'] < 0.5:
+ low_content_products.append({
+ 'اسم': product,
+ 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي']
+ })
+
+ if low_content_products:
+ recommendations.append(f"استبدال المنتجات ذات المحتوى المحلي المنخفض: {', '.join([p['اسم'] for p in low_content_products[:3]])}")
+ recommendations.append("البحث عن موردين محليين للمنتجات ذات الأولوية العالية")
+
+ # توصيات للخدمات
+ if 'الخدمات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['الخدمات']['الحالة'] == 'غير مطابق':
+ low_content_services = []
+ for service, details in results['تفاصيل_المكونات']['الخدمات']['تفاصيل'].items():
+ if details['نسبة_المحتوى_المحلي'] < 0.5:
+ low_content_services.append({
+ 'اسم': service,
+ 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي']
+ })
+
+ if low_content_services:
+ recommendations.append(f"تحسين نسبة المحتوى المحلي للخدمات: {', '.join([s['اسم'] for s in low_content_services[:3]])}")
+ recommendations.append("التعاقد مع شركات خدمية محلية")
+
+ # توصيات للقوى العاملة
+ if 'القوى العاملة' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['القوى العاملة']['الحالة'] == 'غير مطابق':
+ low_content_labor = []
+ for labor_type, details in results['تفاصيل_المكونات']['القوى العاملة']['تفاصيل'].items():
+ if details['نسبة_المحتوى_المحلي'] < 0.5:
+ low_content_labor.append({
+ 'اسم': labor_type,
+ 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي']
+ })
+
+ if low_content_labor:
+ recommendations.append(f"زيادة توظيف العمالة المحلية في الفئات: {', '.join([l['اسم'] for l in low_content_labor[:3]])}")
+ recommendations.append("الاستثمار في برامج تدريب وتأهيل الكوادر المحلية")
+
+ # توصيات عامة
+ if 'المنتجات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['المنتجات'].get('الفجوة (%)', 0) > 10:
+ recommendations.append(f"خطة تطوير المحتوى المحلي للمنتجات لتقليل الفجوة البالغة {results['ملخص_المحتوى_المحلي']['المنتجات']['الفجوة (%)']}%")
+
+ if 'الخدمات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['الخدمات'].get('الفجوة (%)', 0) > 10:
+ recommendations.append(f"خطة تطوير المحتوى المحلي للخدمات لتقليل الفجوة البالغة {results['ملخص_المحتوى_المحلي']['الخدمات']['الفجوة (%)']}%")
+
+ if 'القوى العاملة' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['القوى العاملة'].get('الفجوة (%)', 0) > 10:
+ recommendations.append(f"خطة تطوير المحتوى المحلي للقوى العاملة لتقليل الفجوة البالغة {results['ملخص_المحتوى_المحلي']['القوى العاملة']['الفجوة (%)']}%")
+
+ return recommendations
\ No newline at end of file
diff --git a/modules/pricing/services/price_prediction.py b/modules/pricing/services/price_prediction.py
new file mode 100644
index 0000000000000000000000000000000000000000..8579c10d5cb8f388dff85c08a6131a2e0dd44afe
--- /dev/null
+++ b/modules/pricing/services/price_prediction.py
@@ -0,0 +1,444 @@
+"""
+خدمة التنبؤ بالأسعار
+"""
+
+import pandas as pd
+import numpy as np
+import joblib
+import os
+from datetime import datetime, timedelta
+from sklearn.ensemble import RandomForestRegressor
+from sklearn.model_selection import train_test_split
+from sklearn.preprocessing import StandardScaler
+from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
+
+import config
+
+
+class PricePrediction:
+ """خدمة التنبؤ بالأسعار باستخدام التعلم الآلي"""
+
+ def __init__(self):
+ """تهيئة خدمة التنبؤ بالأسعار"""
+ self.model_path = config.PRICE_PREDICTION_MODEL
+ self.model = self._load_model()
+ self.scaler = None
+ self.materials_data = self._load_materials_data()
+ self.market_indices = self._load_market_indices()
+
+ def _load_model(self):
+ """تحميل نموذج التنبؤ المدرب مسبقاً"""
+ try:
+ if os.path.exists(self.model_path):
+ model = joblib.load(self.model_path)
+ return model
+ else:
+ # إذا لم يكن النموذج موجوداً، قم بإنشاء نموذج جديد
+ model = RandomForestRegressor(
+ n_estimators=100,
+ max_depth=15,
+ min_samples_split=5,
+ min_samples_leaf=2,
+ random_state=42
+ )
+ return model
+ except Exception as e:
+ print(f"خطأ في تحميل نموذج التنبؤ: {str(e)}")
+ return RandomForestRegressor(random_state=42)
+
+ def _load_materials_data(self):
+ """تحميل بيانات المواد وأسعارها التاريخية"""
+ # محاكاة تحميل البيانات من مصدر بيانات
+ materials_data = {
+ 'خرسانة': {
+ 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
+ 'سعر': [750, 740, 735, 730, 720, 715, 710, 700, 695, 690, 685, 680],
+ 'وحدة': 'م3'
+ },
+ 'حديد تسليح': {
+ 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
+ 'سعر': [5500, 5450, 5400, 5350, 5300, 5250, 5200, 5150, 5100, 5050, 5000, 4950],
+ 'وحدة': 'طن'
+ },
+ 'إسمنت': {
+ 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
+ 'سعر': [25, 25, 24.5, 24.5, 24, 24, 23.5, 23.5, 23, 23, 22.5, 22.5],
+ 'وحدة': 'كيس'
+ },
+ 'رمل': {
+ 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
+ 'سعر': [140, 140, 135, 135, 130, 130, 125, 125, 120, 120, 115, 115],
+ 'وحدة': 'م3'
+ },
+ 'بلوك خرساني': {
+ 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
+ 'سعر': [11, 11, 10.5, 10.5, 10, 10, 9.5, 9.5, 9, 9, 8.5, 8.5],
+ 'وحدة': 'قطعة'
+ }
+ }
+ return materials_data
+
+ def _load_market_indices(self):
+ """تحميل مؤشرات السوق المؤثرة على الأسعار"""
+ # محاكاة تحميل البيانات من مصدر بيانات
+ market_indices = {
+ 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
+ 'مؤشر_البناء': [105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94],
+ 'مؤشر_النفط': [80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69],
+ 'مؤشر_سعر_الصرف': [3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75],
+ 'مؤشر_التضخم': [2.5, 2.4, 2.3, 2.2, 2.1, 2.0, 1.9, 1.8, 1.7, 1.6, 1.5, 1.4]
+ }
+ return market_indices
+
+ def train(self, training_data=None):
+ """
+ تدريب نموذج التنبؤ بالأسعار
+
+ المعلمات:
+ training_data: بيانات التدريب (اختياري)، إذا لم يتم توفيرها سيتم استخدام البيانات المتاحة
+
+ إرجاع:
+ مؤشرات أداء النموذج
+ """
+ # تجهيز بيانات التدريب
+ if training_data is None:
+ # استخدام البيانات المتاحة لتوليد مجموعة تدريب
+ X, y = self._prepare_training_data()
+ else:
+ X, y = self._extract_features_target(training_data)
+
+ # تقسيم البيانات إلى تدريب واختبار
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
+
+ # تطبيع البيانات
+ self.scaler = StandardScaler()
+ X_train_scaled = self.scaler.fit_transform(X_train)
+ X_test_scaled = self.scaler.transform(X_test)
+
+ # تدريب النموذج
+ self.model.fit(X_train_scaled, y_train)
+
+ # تقييم النموذج
+ y_pred = self.model.predict(X_test_scaled)
+
+ # حساب مؤشرات الأداء
+ mae = mean_absolute_error(y_test, y_pred)
+ rmse = np.sqrt(mean_squared_error(y_test, y_pred))
+ r2 = r2_score(y_test, y_pred)
+
+ # حفظ النموذج
+ try:
+ joblib.dump(self.model, self.model_path)
+ joblib.dump(self.scaler, os.path.join(os.path.dirname(self.model_path), 'price_scaler.pkl'))
+ except Exception as e:
+ print(f"خطأ في حفظ النموذج: {str(e)}")
+
+ return {
+ 'mae': mae,
+ 'rmse': rmse,
+ 'r2': r2
+ }
+
+ def _prepare_training_data(self):
+ """تجهيز بيانات التدريب من البيانات المتاحة"""
+ # توليد بيانات تدريب افتراضية
+ data = []
+ target = []
+
+ # استخدام بيانات المواد وأسعارها التاريخية
+ for material_name, material_info in self.materials_data.items():
+ for i in range(len(material_info['تاريخ'])):
+ # استخراج المؤشرات في التاريخ المقابل
+ date_index = self.market_indices['تاريخ'].index(material_info['تاريخ'][i]) if material_info['تاريخ'][i] in self.market_indices['تاريخ'] else 0
+
+ # تكوين ميزات التدريب (المؤشرات السوقية والشهر)
+ features = [
+ material_info['تاريخ'][i].month, # الشهر
+ self.market_indices['مؤشر_البناء'][date_index],
+ self.market_indices['مؤشر_النفط'][date_index],
+ self.market_indices['مؤشر_سعر_الصرف'][date_index],
+ self.market_indices['مؤشر_التضخم'][date_index]
+ ]
+
+ # إضافة معرّف للمادة (تمثيل رقمي)
+ material_id = list(self.materials_data.keys()).index(material_name)
+ features.append(material_id)
+
+ data.append(features)
+ target.append(material_info['سعر'][i])
+
+ # إضافة ضوضاء عشوائية لزيادة حجم البيانات
+ for _ in range(5):
+ noisy_features = features.copy()
+ for j in range(1, 5): # إضافة ضوضاء للمؤشرات فقط
+ noisy_features[j] += np.random.normal(0, 0.5)
+
+ noisy_price = material_info['سعر'][i] * (1 + np.random.normal(0, 0.02)) # ضوضاء 2%
+
+ data.append(noisy_features)
+ target.append(noisy_price)
+
+ return np.array(data), np.array(target)
+
+ def _extract_features_target(self, training_data):
+ """استخراج الميزات والأهداف من بيانات التدريب"""
+ # استخراج الميزات والأهداف من البيانات المقدمة
+ features = []
+ target = []
+
+ for item in training_data:
+ features.append([
+ item['date'].month, # الشهر
+ item['building_index'],
+ item['oil_index'],
+ item['exchange_rate'],
+ item['inflation_rate'],
+ item['material_id']
+ ])
+ target.append(item['price'])
+
+ return np.array(features), np.array(target)
+
+ def predict_prices(self, materials, prediction_date=None, market_conditions=None):
+ """
+ التنبؤ بأسعار المواد
+
+ المعلمات:
+ materials: قائمة المواد المطلوب التنبؤ بأسعارها
+ prediction_date: تاريخ التنبؤ (اختياري)، إذا لم يتم توفيره سيتم استخدام التاريخ الحالي
+ market_conditions: ظروف السوق (اختياري)، إذا لم يتم توفيرها سيتم استخدام آخر قيم متاحة
+
+ إرجاع:
+ قاموس بأسعار المواد المتنبأ بها
+ """
+ if prediction_date is None:
+ prediction_date = datetime.now()
+
+ if market_conditions is None:
+ # استخدام آخر قيم متاحة للمؤشرات
+ market_conditions = {
+ 'مؤشر_البناء': self.market_indices['مؤشر_البناء'][0],
+ 'مؤشر_النفط': self.market_indices['مؤشر_النفط'][0],
+ 'مؤشر_سعر_الصرف': self.market_indices['مؤشر_سعر_الصرف'][0],
+ 'مؤشر_التضخم': self.market_indices['مؤشر_التضخم'][0]
+ }
+
+ # التحقق من وجود المواد في البيانات
+ material_names = list(self.materials_data.keys())
+ valid_materials = [m for m in materials if m in material_names]
+
+ if not valid_materials:
+ return {}
+
+ # تحميل المعايير إذا كانت متوفرة
+ scaler_path = os.path.join(os.path.dirname(self.model_path), 'price_scaler.pkl')
+ if self.scaler is None and os.path.exists(scaler_path):
+ try:
+ self.scaler = joblib.load(scaler_path)
+ except Exception as e:
+ print(f"خطأ في تحميل المعايير: {str(e)}")
+ # إنشاء معايير جديدة
+ X, _ = self._prepare_training_data()
+ self.scaler = StandardScaler()
+ self.scaler.fit(X)
+
+ # إعداد ميزات التنبؤ
+ features = []
+ for material in valid_materials:
+ material_id = material_names.index(material)
+
+ material_features = [
+ prediction_date.month, # الشهر
+ market_conditions['مؤشر_البناء'],
+ market_conditions['مؤشر_النفط'],
+ market_conditions['مؤشر_سعر_الصرف'],
+ market_conditions['مؤشر_التضخم'],
+ material_id
+ ]
+
+ features.append(material_features)
+
+ # تطبيع الميزات
+ if self.scaler is not None:
+ features_scaled = self.scaler.transform(features)
+ else:
+ features_scaled = features
+
+ # التنبؤ بالأسعار
+ predicted_prices = self.model.predict(features_scaled)
+
+ # إرجاع النتائج
+ results = {}
+ for i, material in enumerate(valid_materials):
+ # تطبيق عامل تصحيح (2% عشوائية)
+ correction_factor = 1.0 + np.random.uniform(-0.02, 0.02)
+ price = max(0, predicted_prices[i] * correction_factor)
+
+ results[material] = {
+ 'سعر': price,
+ 'وحدة': self.materials_data[material]['وحدة'],
+ 'تاريخ_التنبؤ': prediction_date.strftime('%Y-%m-%d'),
+ 'هامش_الخطأ': '±5%' # تقدير هامش الخطأ
+ }
+
+ return results
+
+ def get_price_trends(self, material, periods=6):
+ """
+ الحصول على اتجاهات الأسعار المستقبلية
+
+ المعلمات:
+ material: المادة المطلوب التنبؤ باتجاهات أسعارها
+ periods: عدد الفترات المستقبلية (الشهور)
+
+ إرجاع:
+ قائمة بالأسعار المتوقعة للفترات المستقبلية
+ """
+ if material not in self.materials_data:
+ return []
+
+ # الحصول على التاريخ الحالي
+ current_date = datetime.now()
+
+ # التنبؤ بالأسعار للفترات المستقبلية
+ price_trends = []
+
+ for i in range(periods):
+ prediction_date = current_date + timedelta(days=30 * (i + 1))
+
+ # افتراض تغيرات طفيفة في المؤشرات مع مرور الوقت
+ market_conditions = {
+ 'مؤشر_البناء': self.market_indices['مؤشر_البناء'][0] * (1 + 0.01 * i), # زيادة 1% شهرياً
+ 'مؤشر_النفط': self.market_indices['مؤشر_النفط'][0] * (1 + 0.005 * i), # زيادة 0.5% شهرياً
+ 'مؤشر_سعر_الصرف': self.market_indices['مؤشر_سعر_الصرف'][0], # ثابت
+ 'مؤشر_التضخم': self.market_indices['مؤشر_التضخم'][0] * (1 + 0.01 * i) # زيادة 1% شهرياً
+ }
+
+ # التنبؤ بالسعر
+ predicted_price = self.predict_prices([material], prediction_date, market_conditions)
+
+ price_trends.append({
+ 'تاريخ': prediction_date.strftime('%Y-%m'),
+ 'سعر': predicted_price[material]['سعر'] if material in predicted_price else 0
+ })
+
+ return price_trends
+
+ def analyze_factors(self, material):
+ """
+ تحليل العوامل المؤثرة على سعر المادة
+
+ المعلمات:
+ material: المادة المطلوب تحليلها
+
+ إرجاع:
+ قاموس بالعوامل المؤثرة وأهميتها النسبية
+ """
+ if material not in self.materials_data or not hasattr(self.model, 'feature_importances_'):
+ return {}
+
+ # الحصول على أهمية الميزات من النموذج
+ feature_importances = self.model.feature_importances_
+
+ # أسماء الميزات
+ feature_names = ['الشهر', 'مؤشر البناء', 'مؤشر النفط', 'سعر الصرف', 'معدل التضخم', 'نوع المادة']
+
+ # ترتيب الميزات حسب الأهمية
+ importance_pairs = [(name, importance) for name, importance in zip(feature_names, feature_importances)]
+ importance_pairs.sort(key=lambda x: x[1], reverse=True)
+
+ # إرجاع العوامل المؤثرة وأهميتها
+ factors = {}
+ for name, importance in importance_pairs:
+ factors[name] = round(importance * 100, 2) # تحويل إلى نسبة مئوية
+
+ return {
+ 'العوامل_المؤثرة': factors,
+ 'المادة': material,
+ 'وحدة': self.materials_data[material]['وحدة'],
+ 'سعر_حالي': self.materials_data[material]['سعر'][0],
+ 'اتجاه_السعر': self._get_price_trend(material)
+ }
+
+ def _get_price_trend(self, material):
+ """تحديد اتجاه سعر المادة بناءً على البيانات التاريخية"""
+ if material not in self.materials_data:
+ return "غير معروف"
+
+ prices = self.materials_data[material]['سعر']
+ if len(prices) < 2:
+ return "غير معروف"
+
+ # حساب متوسط التغير الشهري
+ price_changes = [(prices[i] - prices[i+1]) / prices[i+1] * 100 for i in range(len(prices)-1)]
+ avg_monthly_change = sum(price_changes) / len(price_changes)
+
+ if avg_monthly_change > 1:
+ return "ارتفاع حاد"
+ elif avg_monthly_change > 0.2:
+ return "ارتفاع معتدل"
+ elif avg_monthly_change > -0.2:
+ return "استقرار"
+ elif avg_monthly_change > -1:
+ return "انخفاض معتدل"
+ else:
+ return "انخفاض حاد"
+
+ def export_price_forecast(self, materials, periods=6, output_file=None):
+ """
+ تصدير توقعات الأسعار إلى ملف
+
+ المعلمات:
+ materials: قائمة المواد المطلوب التنبؤ بأسعارها
+ periods: عدد الفترات المستقبلية (الشهور)
+ output_file: مسار ملف الإخراج (اختياري)
+
+ إرجاع:
+ مسار الملف المصدر أو البيانات مباشرة إذا لم يتم تحديد ملف
+ """
+ # التحقق من وجود المواد في البيانات
+ valid_materials = [m for m in materials if m in self.materials_data]
+
+ if not valid_materials:
+ return None
+
+ # إعداد بيانات التوقعات
+ forecast_data = []
+
+ for material in valid_materials:
+ # الحصول على اتجاهات الأسعار
+ price_trends = self.get_price_trends(material, periods)
+
+ for trend in price_trends:
+ forecast_data.append({
+ 'المادة': material,
+ 'الوحدة': self.materials_data[material]['وحدة'],
+ 'التاريخ': trend['تاريخ'],
+ 'السعر المتوقع': trend['سعر'],
+ 'هامش الخطأ': '±5%'
+ })
+
+ # تحويل البيانات إلى DataFrame
+ forecast_df = pd.DataFrame(forecast_data)
+
+ # تصدير البيانات إلى ملف إذا تم تحديده
+ if output_file:
+ try:
+ ext = os.path.splitext(output_file)[1].lower()
+
+ if ext == '.csv':
+ forecast_df.to_csv(output_file, index=False, encoding='utf-8-sig')
+ elif ext in ['.xlsx', '.xls']:
+ forecast_df.to_excel(output_file, index=False)
+ elif ext == '.json':
+ forecast_df.to_json(output_file, orient='records', force_ascii=False)
+ else:
+ print(f"تنسيق غير مدعوم: {ext}")
+ return None
+
+ return output_file
+ except Exception as e:
+ print(f"خطأ في تصدير توقعات الأسعار: {str(e)}")
+ return None
+
+ return forecast_df
\ No newline at end of file
diff --git a/modules/pricing/services/standard_pricing.py b/modules/pricing/services/standard_pricing.py
new file mode 100644
index 0000000000000000000000000000000000000000..ec89e61a101f89432b815718b8b615c5c00ecf45
--- /dev/null
+++ b/modules/pricing/services/standard_pricing.py
@@ -0,0 +1,232 @@
+"""
+خدمة التسعير القياسي
+"""
+
+import pandas as pd
+import numpy as np
+from datetime import datetime
+import os
+import config
+
+
+class StandardPricing:
+ """خدمة التسعير القياسي للبنود"""
+
+ def __init__(self):
+ """تهيئة خدمة التسعير القياسي"""
+ # تحميل بيانات المواد والأسعار المرجعية
+ self.material_prices = self._load_material_prices()
+ self.labor_rates = self._load_labor_rates()
+ self.equipment_rates = self._load_equipment_rates()
+
+ def _load_material_prices(self):
+ """تحميل أسعار المواد"""
+ # محاكاة تحميل البيانات من مصدر بيانات
+ material_prices = {
+ 'خرسانة': {
+ 'م3': 750.0, # سعر المتر المكعب بالريال
+ 'وحدة_قياسية': 'م3',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'حديد تسليح': {
+ 'طن': 5500.0, # سعر الطن بالريال
+ 'وحدة_قياسية': 'طن',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'عزل مائي': {
+ 'م2': 80.0, # سعر المتر المربع بالريال
+ 'وحدة_قياسية': 'م2',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'بلوك خرساني': {
+ '20سم': 11.0, # سعر البلكة بالريال
+ 'وحدة_قياسية': 'عدد',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'رمل': {
+ 'م3': 140.0, # سعر المتر المكعب بالريال
+ 'وحدة_قياسية': 'م3',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'اسمنت': {
+ 'كيس': 25.0, # سعر الكيس بالريال
+ 'وحدة_قياسية': 'كيس',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ }
+ }
+ return material_prices
+
+ def _load_labor_rates(self):
+ """تحميل معدلات أجور العمالة"""
+ # محاكاة تحميل البيانات من مصدر بيانات
+ labor_rates = {
+ 'عامل': {
+ 'يومي': 150.0, # الأجر اليومي بالريال
+ 'وحدة_قياسية': 'يوم',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'نجار': {
+ 'يومي': 250.0, # الأجر اليومي بالريال
+ 'وحدة_قياسية': 'يوم',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'حداد': {
+ 'يومي': 250.0, # الأجر اليومي بالريال
+ 'وحدة_قياسية': 'يوم',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'سباك': {
+ 'يومي': 300.0, # الأجر اليومي بالريال
+ 'وحدة_قياسية': 'يوم',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'كهربائي': {
+ 'يومي': 300.0, # الأجر اليومي بالريال
+ 'وحدة_قياسية': 'يوم',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'مراقب': {
+ 'يومي': 400.0, # الأجر اليومي بالريال
+ 'وحدة_قياسية': 'يوم',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ }
+ }
+ return labor_rates
+
+ def _load_equipment_rates(self):
+ """تحميل معدلات تأجير المعدات"""
+ # محاكاة تحميل البيانات من مصدر بيانات
+ equipment_rates = {
+ 'خلاطة خرسانة': {
+ 'يومي': 800.0, # الإيجار اليومي بالريال
+ 'وحدة_قياسية': 'يوم',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'هزاز خرسانة': {
+ 'يومي': 150.0, # الإيجار اليومي بالريال
+ 'وحدة_قياسية': 'يوم',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'حفارة': {
+ 'يومي': 1500.0, # الإيجار اليومي بالريال
+ 'وحدة_قياسية': 'يوم',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'لودر': {
+ 'يومي': 1200.0, # الإيجار اليومي بالريال
+ 'وحدة_قياسية': 'يوم',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'رافعة': {
+ 'يومي': 2000.0, # الإيجار اليومي بالريال
+ 'وحدة_قياسية': 'يوم',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ },
+ 'شاحنة نقل': {
+ 'يومي': 900.0, # الإيجار اليومي بالريال
+ 'وحدة_قياسية': 'يوم',
+ 'آخر_تحديث': datetime(2025, 3, 1)
+ }
+ }
+ return equipment_rates
+
+ def calculate_prices(self, items_df):
+ """حساب الأسعار للبنود باستخدام التسعير القياسي"""
+ # نسخة من البيانات المدخلة للعمل عليها
+ df = items_df.copy()
+
+ # التأكد من وجود العمود المطلوب
+ if 'سعر الوحدة' not in df.columns:
+ df['سعر الوحدة'] = 0.0
+
+ if 'الإجمالي' not in df.columns:
+ df['الإجمالي'] = 0.0
+
+ # حساب أسعار الوحدات لكل بند
+ for idx, row in df.iterrows():
+ # حساب سعر الوحدة بناءً على وصف البند
+ unit_price = self._estimate_unit_price(row['وصف البند'], row['الوحدة'])
+ df.at[idx, 'سعر الوحدة'] = unit_price
+
+ # حساب الإجمالي لكل بند
+ df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة']
+
+ return df
+
+ def _estimate_unit_price(self, description, unit):
+ """تقدير سعر الوحدة بناءً على وصف البند ووحدة القياس"""
+ description = description.lower()
+
+ # تقدير سعر الوحدة بناءً على وصف البند
+ if 'خرسان' in description:
+ if 'أساسات' in description:
+ return 1200.0 if unit == 'م3' else 0.0
+ elif 'أعمدة' in description:
+ return 1800.0 if unit == 'م3' else 0.0
+ elif 'سقف' in description:
+ return 1500.0 if unit == 'م3' else 0.0
+ else:
+ return 1400.0 if unit == 'م3' else 0.0
+
+ elif 'حديد' in description and 'تسليح' in description:
+ if 'أساسات' in description:
+ return 6000.0 if unit == 'طن' else 0.0
+ elif 'أعمدة' in description or 'سقف' in description:
+ return 6500.0 if unit == 'طن' else 0.0
+ else:
+ return 6200.0 if unit == 'طن' else 0.0
+
+ elif 'عزل' in description:
+ if 'مائي' in description:
+ return 120.0 if unit == 'م2' else 0.0
+ elif 'حراري' in description:
+ return 90.0 if unit == 'م2' else 0.0
+ else:
+ return 100.0 if unit == 'م2' else 0.0
+
+ elif 'ردم' in description or 'حفر' in description:
+ return 75.0 if unit == 'م3' else 0.0
+
+ elif 'بلوك' in description or 'طوب' in description:
+ return 250.0 if unit == 'م2' else 0.0
+
+ elif 'لياسة' in description or 'بياض' in description:
+ return 80.0 if unit == 'م2' else 0.0
+
+ elif 'دهان' in description or 'طلاء' in description:
+ return 65.0 if unit == 'م2' else 0.0
+
+ elif 'سيراميك' in description or 'بلاط' in description:
+ return 180.0 if unit == 'م2' else 0.0
+
+ elif 'كهرباء' in description:
+ return 150.0 if unit == 'نقطة' else 500.0
+
+ # قيمة افتراضية إذا لم تتطابق مع أي وصف
+ return 100.0
+
+ def adjust_prices_for_factors(self, items_df, factors=None):
+ """تعديل الأسعار بناءً على عوامل مؤثرة"""
+ # نسخة من البيانات المدخلة للعمل عليها
+ df = items_df.copy()
+
+ # إذا لم يتم تحديد عوامل، استخدم العوامل الافتراضية
+ if factors is None:
+ factors = {
+ 'location_factor': 1.0, # معامل الموقع
+ 'time_factor': 1.0, # معامل الوقت
+ 'risk_factor': 1.1, # معامل المخاطر
+ 'market_factor': 1.05 # معامل السوق
+ }
+
+ # حساب المعامل الإجمالي
+ total_factor = (factors['location_factor'] * factors['time_factor'] *
+ factors['risk_factor'] * factors['market_factor'])
+
+ # تعديل سعر الوحدة بناءً على المعامل الإجمالي
+ df['سعر الوحدة'] = df['سعر الوحدة'] * total_factor
+
+ # حساب الإجمالي بعد التعديل
+ df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة']
+
+ return df
\ No newline at end of file
diff --git a/modules/pricing/services/templates_catalog/__init__.py b/modules/pricing/services/templates_catalog/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c7a39ff16d8fa482e8d44f777bee35f84728795
--- /dev/null
+++ b/modules/pricing/services/templates_catalog/__init__.py
@@ -0,0 +1,3 @@
+from .templates_catalog import TemplatesCatalog
+
+__all__ = ['TemplatesCatalog']
\ No newline at end of file
diff --git a/modules/pricing/services/templates_catalog/templates_catalog.py b/modules/pricing/services/templates_catalog/templates_catalog.py
new file mode 100644
index 0000000000000000000000000000000000000000..23d6006f8f7781a90a22203f309372e16bcc84f6
--- /dev/null
+++ b/modules/pricing/services/templates_catalog/templates_catalog.py
@@ -0,0 +1,949 @@
+"""
+كتالوج قوالب البناء والمقاولات
+واجهة مستخدم متكاملة لعرض واستخدام نماذج بنود البناء الجاهزة
+"""
+
+import os
+import sys
+import json
+import pandas as pd
+import streamlit as st
+from typing import Dict, List, Any, Optional
+
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../..")))
+
+# استيراد مكونات واجهة المستخدم
+from utils.components.header import render_header
+# استيراد الدالة مباشرة من الملف
+from utils.components.credits import display_credits
+# استخدام display_credits كبديل لـ render_credits
+render_credits = display_credits
+from utils.helpers import format_number, format_currency, styled_button, filter_dataframe
+
+# النموذج المستخدم عند إضافة بند نموذجي جديد من صفحة التسعير
+class NewTemplateForm:
+ """نموذج إضافة بند نموذجي جديد من صفحة التسعير"""
+ pass
+
+class TemplatesCatalog:
+ """كتالوج قوالب البناء والمقاولات"""
+
+ def __init__(self, construction_templates):
+ """تهيئة كتالوج قوالب البناء والمقاولات"""
+ self.construction_templates = construction_templates
+
+ # تهيئة حالة الجلسة للطلبات والعروض
+ if 'material_requests' not in st.session_state:
+ st.session_state.material_requests = []
+
+ if 'equipment_requests' not in st.session_state:
+ st.session_state.equipment_requests = []
+
+ if 'material_offers' not in st.session_state:
+ st.session_state.material_offers = []
+
+ if 'equipment_offers' not in st.session_state:
+ st.session_state.equipment_offers = []
+
+ # تهيئة قوائم الأسعار المرجعية
+ if 'reference_price_list' not in st.session_state:
+ st.session_state.reference_price_list = []
+
+ # تعبئة قوائم الأسعار المرجعية من كتالوج البنود النموذجية
+ self._populate_reference_price_list()
+
+ def render(self):
+ """عرض واجهة كتالوج القوالب"""
+ # عرض الشعار والعنوان الرئيسي
+ render_header("كتالوج بنود المقاولات النموذجية")
+
+ # تبويبات الكتالوج
+ tabs = st.tabs(["تصفح البنود النموذجية", "طلب تسعير مواد جديدة", "طلب تسعير معدات جديدة", "قوائم الأسعار المرجعية"])
+
+ with tabs[0]:
+ self._render_templates_browser()
+
+ with tabs[1]:
+ self._render_material_request_form()
+
+ with tabs[2]:
+ self._render_equipment_request_form()
+
+ with tabs[3]:
+ self._render_reference_price_list()
+
+ def _render_templates_browser(self):
+ """عرض واجهة تصفح كتالوج القوالب"""
+ st.markdown("""
+
+
🗃️ نماذج بنود المقاولات الجاهزة للاستخدام
+
يمكنك الاختيار من بين مجموعة متنوعة من نماذج البنود المعرفة مسبقًا والجاهزة للاستخدام في مشاريعك وعروض أسعارك.
+
تشمل النماذج تفاصيل كاملة عن:
+
+
المواد المستخدمة وكمياتها
+
العمالة المطلوبة ومدة العمل
+
المعدات اللازمة وتكلفتها
+
المصروفات الإدارية وهامش الربح
+
+
+ """, unsafe_allow_html=True)
+
+ # قسم البحث والتصفية
+ st.markdown("### تصفية البنود النموذجية")
+
+ # الحصول على الفئات من الكتالوج
+ templates = self.construction_templates.get_all_templates()
+ categories = templates.get("categories", {})
+
+ # إنشاء قائمة الفئات
+ category_list = [{"id": cat_id, "name": cat_data.get("name", cat_id)} for cat_id, cat_data in categories.items()]
+ category_names = ["الكل"] + [cat["name"] for cat in category_list]
+
+ # تصفية حسب الفئة
+ col1, col2 = st.columns(2)
+ with col1:
+ selected_category_name = st.selectbox("فئة البنود", category_names, index=0)
+
+ with col2:
+ search_query = st.text_input("بحث في النماذج", placeholder="اكتب كلمة للبحث...")
+
+ # تحديد الفئة المختارة
+ selected_category_id = None
+ if selected_category_name != "الكل":
+ for cat in category_list:
+ if cat["name"] == selected_category_name:
+ selected_category_id = cat["id"]
+ break
+
+ # الحصول على النماذج المصفاة
+ filtered_templates = []
+ all_templates = templates.get("templates", {})
+
+ for template_id, template in all_templates.items():
+ # التصفية حسب الفئة إذا تم اختيار فئة محددة
+ if selected_category_id and template.get("category") != selected_category_id:
+ continue
+
+ # التصفية حسب البحث إذا تم إدخال نص للبحث
+ if search_query:
+ template_name = template.get("name", "")
+ template_desc = template.get("description", "")
+ searchable_text = f"{template_name} {template_desc}"
+ if search_query.lower() not in searchable_text.lower():
+ continue
+
+ # إضافة النموذج إلى القائمة المصفاة
+ template_copy = template.copy()
+ template_copy["id"] = template_id
+ filtered_templates.append(template_copy)
+
+ # عرض النماذج المصفاة
+ self._render_templates_list(filtered_templates)
+
+ # عرض نموذج إضافة قالب جديد
+ with st.expander("إضافة قالب نموذجي جديد"):
+ self._render_new_template_form()
+
+ # عرض الحقوق
+ render_credits()
+
+ def _render_templates_list(self, templates: List[Dict[str, Any]]):
+ """عرض قائمة النماذج المصفاة"""
+
+ if not templates:
+ st.warning("لا توجد نماذج بنود متاحة تطابق معايير البحث.")
+ return
+
+ # تحويل النماذج إلى DataFrame
+ templates_data = []
+ for template in templates:
+ # حساب التكلفة التقديرية للنموذج
+ estimated_cost = self._calculate_template_cost(template)
+
+ templates_data.append({
+ "الرقم التعريفي": template["id"],
+ "اسم النموذج": template.get("name", ""),
+ "الوصف": template.get("description", "")[:50] + "..." if len(template.get("description", "")) > 50 else template.get("description", ""),
+ "التكلفة التقديرية": estimated_cost,
+ "الوحدة": template.get("unit", ""),
+ "الفئة": template.get("category", "")
+ })
+
+ templates_df = pd.DataFrame(templates_data)
+
+ # تنسيق الجدول
+ def highlight_row(row):
+ """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
+ color = '#F0F8FF' if row.name % 2 == 0 else 'white'
+ return ['background-color: %s' % color] * len(row)
+
+ styled_df = templates_df.style.apply(highlight_row, axis=1)
+ styled_df = styled_df.format({
+ "التكلفة التقديرية": "{:,.2f} ريال"
+ })
+
+ # عرض الجدول
+ st.markdown("### قائمة النماذج المتاحة")
+ st.dataframe(styled_df, use_container_width=True, hide_index=True)
+
+ # عرض تفاصيل النموذج المحدد
+ selected_template_id = st.selectbox(
+ "اختر نموذجًا لعرض التفاصيل",
+ options=[t["الرقم التعريفي"] for t in templates_data],
+ format_func=lambda x: next((t["اسم النموذج"] for t in templates_data if t["الرقم التعريفي"] == x), x)
+ )
+
+ if selected_template_id:
+ # الحصول على النموذج المحدد
+ selected_template = next((t for t in templates if t["id"] == selected_template_id), None)
+ if selected_template:
+ self._render_template_details(selected_template)
+
+ def _render_template_details(self, template: Dict[str, Any]):
+ """عرض تفاصيل النموذج المحدد"""
+ st.markdown(f"### تفاصيل نموذج: {template.get('name', '')}")
+
+ # معلومات النموذج الأساسية
+ col1, col2, col3 = st.columns(3)
+ with col1:
+ st.markdown(f"**الوصف:** {template.get('description', '')}")
+ with col2:
+ st.markdown(f"**وحدة القياس:** {template.get('unit', '')}")
+ with col3:
+ # حساب التكلفة التقديرية للنموذج
+ estimated_cost = self._calculate_template_cost(template)
+ st.markdown(f"**التكلفة التقديرية:** {estimated_cost:,.2f} ريال")
+
+ # تبويبات لعرض مكونات النموذج
+ tabs = st.tabs(["المواد", "العمالة", "المعدات", "التكاليف"])
+
+ # تبويب المواد
+ with tabs[0]:
+ self._render_materials_tab(template)
+
+ # تبويب العمالة
+ with tabs[1]:
+ self._render_labor_tab(template)
+
+ # تبويب المعدات
+ with tabs[2]:
+ self._render_equipment_tab(template)
+
+ # تبويب التكاليف
+ with tabs[3]:
+ self._render_costs_tab(template)
+
+ # أزرار العمليات
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ if styled_button("إضافة إلى حاسبة التكاليف", key=f"add_to_calc_{template['id']}", type="primary", icon="➕"):
+ st.session_state.selected_template_for_calculator = template["id"]
+ st.success("تم إضافة النموذج إلى حاسبة التكاليف!")
+
+ with col2:
+ if styled_button("استخدام في بند جديد", key=f"use_in_new_item_{template['id']}", type="success", icon="🔄"):
+ st.session_state.selected_template_for_new_item = template["id"]
+ st.success("تم اختيار النموذج لاستخدامه في بند جديد!")
+
+ with col3:
+ if styled_button("تحرير النموذج", key=f"edit_template_{template['id']}", type="secondary", icon="✏️"):
+ st.session_state.template_to_edit = template["id"]
+
+ def _render_materials_tab(self, template: Dict[str, Any]):
+ """عرض تبويب المواد"""
+ components = template.get("components", {})
+ materials = components.get("materials", [])
+
+ if not materials:
+ st.info("لا توجد مواد محددة لهذا النموذج.")
+ return
+
+ # تحويل المواد إلى DataFrame
+ materials_data = []
+ total_materials_cost = 0
+
+ for material in materials:
+ material_cost = material.get("الكمية", 0) * material.get("سعر_الوحدة", 0)
+ total_materials_cost += material_cost
+
+ materials_data.append({
+ "اسم المادة": material.get("الاسم", ""),
+ "الكمية": material.get("الكمية", 0),
+ "الوحدة": material.get("الوحدة", ""),
+ "سعر الوحدة": material.get("سعر_الوحدة", 0),
+ "التكلفة": material_cost
+ })
+
+ materials_df = pd.DataFrame(materials_data)
+
+ # تنسيق الجدول
+ def highlight_row(row):
+ """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
+ color = '#F0F8FF' if row.name % 2 == 0 else 'white'
+ return ['background-color: %s' % color] * len(row)
+
+ styled_df = materials_df.style.apply(highlight_row, axis=1)
+ styled_df = styled_df.format({
+ "الكمية": "{:.2f}",
+ "سعر الوحدة": "{:,.2f} ريال",
+ "التكلفة": "{:,.2f} ريال"
+ })
+
+ # عرض الجدول
+ st.dataframe(styled_df, use_container_width=True, hide_index=True)
+ st.markdown(f"**إجمالي تكلفة المواد:** {total_materials_cost:,.2f} ريال")
+
+ def _render_labor_tab(self, template: Dict[str, Any]):
+ """عرض تبويب العمالة"""
+ components = template.get("components", {})
+ labor = components.get("labor", [])
+
+ if not labor:
+ st.info("لا توجد عمالة محددة لهذا النموذج.")
+ return
+
+ # تحويل العمالة إلى DataFrame
+ labor_data = []
+ total_labor_cost = 0
+
+ for worker in labor:
+ labor_cost = worker.get("العدد", 0) * worker.get("المدة", 0) * worker.get("سعر_اليوم", 0)
+ total_labor_cost += labor_cost
+
+ labor_data.append({
+ "نوع العامل": worker.get("النوع", ""),
+ "العدد": worker.get("العدد", 0),
+ "المدة (يوم)": worker.get("المدة", 0),
+ "سعر اليوم": worker.get("سعر_اليوم", 0),
+ "التكلفة": labor_cost
+ })
+
+ labor_df = pd.DataFrame(labor_data)
+
+ # تنسيق الجدول
+ def highlight_row(row):
+ """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
+ color = '#F0F8FF' if row.name % 2 == 0 else 'white'
+ return ['background-color: %s' % color] * len(row)
+
+ styled_df = labor_df.style.apply(highlight_row, axis=1)
+ styled_df = styled_df.format({
+ "المدة (يوم)": "{:.2f}",
+ "سعر اليوم": "{:,.2f} ريال",
+ "التكلفة": "{:,.2f} ريال"
+ })
+
+ # عرض الجدول
+ st.dataframe(styled_df, use_container_width=True, hide_index=True)
+ st.markdown(f"**إجمالي تكلفة العمالة:** {total_labor_cost:,.2f} ريال")
+
+ def _render_equipment_tab(self, template: Dict[str, Any]):
+ """عرض تبويب المعدات"""
+ components = template.get("components", {})
+ equipment = components.get("equipment", [])
+
+ if not equipment:
+ st.info("لا توجد معدات محددة لهذا النموذج.")
+ return
+
+ # تحويل المعدات إلى DataFrame
+ equipment_data = []
+ total_equipment_cost = 0
+
+ for eq in equipment:
+ equipment_cost = eq.get("العدد", 0) * eq.get("المدة", 0) * eq.get("سعر_اليوم", 0)
+ total_equipment_cost += equipment_cost
+
+ equipment_data.append({
+ "نوع المعدة": eq.get("النوع", ""),
+ "العدد": eq.get("العدد", 0),
+ "المدة (يوم)": eq.get("المدة", 0),
+ "سعر اليوم": eq.get("سعر_اليوم", 0),
+ "التكلفة": equipment_cost
+ })
+
+ equipment_df = pd.DataFrame(equipment_data)
+
+ # تنسيق الجدول
+ def highlight_row(row):
+ """تنسيق الجدول مع تمييز الصفوف بالتناوب"""
+ color = '#F0F8FF' if row.name % 2 == 0 else 'white'
+ return ['background-color: %s' % color] * len(row)
+
+ styled_df = equipment_df.style.apply(highlight_row, axis=1)
+ styled_df = styled_df.format({
+ "المدة (يوم)": "{:.2f}",
+ "سعر اليوم": "{:,.2f} ريال",
+ "التكلفة": "{:,.2f} ريال"
+ })
+
+ # عرض الجدول
+ st.dataframe(styled_df, use_container_width=True, hide_index=True)
+ st.markdown(f"**إجمالي تكلفة المعدات:** {total_equipment_cost:,.2f} ريال")
+
+ def _render_costs_tab(self, template: Dict[str, Any]):
+ """عرض تبويب التكاليف"""
+ # حساب إجمالي التكاليف المباشرة
+ direct_cost = self._calculate_direct_cost(template)
+
+ # المصاريف الإدارية
+ admin_expenses_pct = template.get("admin_expenses", 0.05)
+ admin_expenses = direct_cost * admin_expenses_pct
+
+ # هامش الربح
+ profit_margin_pct = template.get("profit_margin", 0.1)
+ profit_margin = direct_cost * profit_margin_pct
+
+ # إجمالي التكلفة
+ total_cost = direct_cost + admin_expenses + profit_margin
+
+ # عرض ملخص التكاليف
+ st.markdown("#### ملخص التكاليف")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.markdown(f"**التكاليف المباشرة:** {direct_cost:,.2f} ريال")
+ st.markdown(f"**المصاريف الإدارية ({admin_expenses_pct*100:.0f}%):** {admin_expenses:,.2f} ريال")
+ st.markdown(f"**هامش الربح ({profit_margin_pct*100:.0f}%):** {profit_margin:,.2f} ريال")
+
+ with col2:
+ # رسم بياني دائري لتوزيع التكاليف
+ import plotly.express as px
+
+ # حساب تكاليف المكونات
+ components = template.get("components", {})
+
+ materials_cost = sum(
+ mat.get("الكمية", 0) * mat.get("سعر_الوحدة", 0)
+ for mat in components.get("materials", [])
+ )
+
+ labor_cost = sum(
+ worker.get("العدد", 0) * worker.get("المدة", 0) * worker.get("سعر_اليوم", 0)
+ for worker in components.get("labor", [])
+ )
+
+ equipment_cost = sum(
+ eq.get("العدد", 0) * eq.get("المدة", 0) * eq.get("سعر_اليوم", 0)
+ for eq in components.get("equipment", [])
+ )
+
+ # إنشاء البيانات للرسم البياني
+ cost_distribution = [
+ {"النوع": "المواد", "القيمة": materials_cost},
+ {"النوع": "العمالة", "القيمة": labor_cost},
+ {"النوع": "المعدات", "القيمة": equipment_cost},
+ {"النوع": "المصاريف الإدارية", "القيمة": admin_expenses},
+ {"النوع": "هامش الربح", "القيمة": profit_margin}
+ ]
+
+ cost_df = pd.DataFrame(cost_distribution)
+
+ fig = px.pie(
+ cost_df,
+ values="القيمة",
+ names="النوع",
+ title="توزيع التكاليف",
+ color_discrete_sequence=px.colors.sequential.Teal,
+ hole=0.4
+ )
+
+ fig.update_traces(textposition='inside', textinfo='percent+label')
+
+ fig.update_layout(
+ annotations=[dict(text=f"{total_cost:,.0f} ريال", x=0.5, y=0.5, font_size=14, showarrow=False)],
+ font=dict(family="Almarai, Arial", size=12),
+ margin=dict(t=30, b=30, l=10, r=10)
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ st.markdown(f"**السعر النهائي:** {total_cost:,.2f} ريال لكل {template.get('unit', 'وحدة')}")
+
+ def _render_new_template_form(self):
+ """عرض نموذج إضافة قالب جديد"""
+ st.markdown("### إضافة قالب نموذجي جديد")
+
+ # معلومات القالب الأساسية
+ col1, col2 = st.columns(2)
+
+ with col1:
+ template_name = st.text_input("اسم القالب", key="new_template_name")
+ template_description = st.text_area("وصف القالب", key="new_template_description")
+
+ with col2:
+ # الحصول على الفئات من الكتالوج
+ templates = self.construction_templates.get_all_templates()
+ categories = templates.get("categories", {})
+
+ # إنشاء قائمة الفئات
+ category_list = [{"id": cat_id, "name": cat_data.get("name", cat_id)} for cat_id, cat_data in categories.items()]
+ category_options = [cat["id"] for cat in category_list]
+ category_labels = [cat["name"] for cat in category_list]
+
+ category_index = 0
+ if category_options:
+ template_category = st.selectbox(
+ "فئة القالب",
+ options=category_options,
+ format_func=lambda x: next((cat["name"] for cat in category_list if cat["id"] == x), x),
+ key="new_template_category"
+ )
+ else:
+ template_category = st.text_input("فئة القالب (لا توجد فئات محددة)", key="new_template_category_text")
+
+ template_unit = st.selectbox(
+ "وحدة القياس",
+ options=["م²", "م³", "م.ط", "عدد", "طن", "كجم", "لتر", "يوم", "ساعة", "مقطوعية"],
+ index=0,
+ key="new_template_unit"
+ )
+
+ # إضافة المواد
+ st.markdown("#### المواد المستخدمة")
+
+ # إنشاء مصفوفة لتخزين المواد
+ if "new_template_materials" not in st.session_state:
+ st.session_state.new_template_materials = []
+
+ # عرض المواد المضافة حاليًا
+ if st.session_state.new_template_materials:
+ materials_df = pd.DataFrame(st.session_state.new_template_materials)
+ st.dataframe(materials_df, hide_index=True)
+
+ # إضافة مادة جديدة
+ st.markdown("##### 🧱 إضافة مادة جديدة")
+ st.markdown('
', unsafe_allow_html=True)
+
+ if styled_button("إضافة إلى القائمة المرجعية", key="add_manual_reference_item", type="success", icon="➕"):
+ if not item_name:
+ st.error("الرجاء إدخال اسم العنصر")
+ elif not item_unit:
+ st.error("الرجاء إدخال وحدة القياس")
+ elif item_price <= 0:
+ st.error("الرجاء إدخال سعر صحيح")
+ else:
+ # إنشاء معرف فريد للعنصر
+ item_id = f"MAN-{item_type[:1]}-{len(st.session_state.reference_price_list) + 1:04d}"
+
+ # إنشاء العنصر
+ new_item = {
+ "id": item_id,
+ "name": item_name,
+ "description": item_desc,
+ "unit": item_unit,
+ "estimated_cost": item_price,
+ "category": "أخرى",
+ "type": item_type,
+ "source": "إدخال يدوي",
+ "date_added": pd.Timestamp.now().strftime("%Y-%m-%d"),
+ "is_active": True
+ }
+
+ # إضافة العنصر إلى القائمة
+ st.session_state.reference_price_list.append(new_item)
+ st.success(f"تم إضافة العنصر {item_name} إلى القائمة المرجعية بنجاح!")
+
+ def _calculate_direct_cost(self, template: Dict[str, Any]) -> float:
+ """حساب التكاليف المباشرة للنموذج"""
+ components = template.get("components", {})
+
+ # حساب تكلفة المواد
+ materials_cost = sum(
+ mat.get("الكمية", 0) * mat.get("سعر_الوحدة", 0)
+ for mat in components.get("materials", [])
+ )
+
+ # حساب تكلفة العمالة
+ labor_cost = sum(
+ worker.get("العدد", 0) * worker.get("المدة", 0) * worker.get("سعر_اليوم", 0)
+ for worker in components.get("labor", [])
+ )
+
+ # حساب تكلفة المعدات
+ equipment_cost = sum(
+ eq.get("العدد", 0) * eq.get("المدة", 0) * eq.get("سعر_اليوم", 0)
+ for eq in components.get("equipment", [])
+ )
+ direct_cost = materials_cost + labor_cost + equipment_cost
+
+ return direct_cost
+
+
+# دالة لتشغيل الكتالوج مباشرة في حالة تنفيذ الملف بشكل مستقل
+def main():
+ """تشغيل كتالوج القوالب بشكل مستقل"""
+ from modules.pricing.services.construction_templates import ConstructionTemplates
+
+ # تهيئة الواجهة
+ st.set_page_config(
+ page_title="كتالوج بنود المقاولات النموذجية",
+ page_icon="🗃️",
+ layout="wide",
+ initial_sidebar_state="collapsed",
+ menu_items={
+ 'Get Help': 'mailto:support@wahbi-ai.com',
+ 'Report a bug': 'mailto:support@wahbi-ai.com',
+ 'About': 'كتالوج بنود المقاولات النموذجية - جزء من نظام WAHBi AI لتحليل المناقصات'
+ }
+ )
+
+ # تهيئة كائن الكتالوج
+ construction_templates = ConstructionTemplates()
+ templates_catalog = TemplatesCatalog(construction_templates)
+
+ # عرض الكتالوج
+ templates_catalog.render()
+
+# تشغيل الكتالوج مباشرة عند تنفيذ الملف
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/modules/pricing/services/unbalanced_pricing.py b/modules/pricing/services/unbalanced_pricing.py
new file mode 100644
index 0000000000000000000000000000000000000000..77df2b857fe88bb48c307b3b14c5217c425efad2
--- /dev/null
+++ b/modules/pricing/services/unbalanced_pricing.py
@@ -0,0 +1,213 @@
+"""
+خدمة التسعير غير المتزن
+"""
+
+import pandas as pd
+import numpy as np
+from datetime import datetime
+import os
+import config
+
+
+class UnbalancedPricing:
+ """خدمة التسعير غير المتزن للبنود"""
+
+ def __init__(self):
+ """تهيئة خدمة التسعير غير المتزن"""
+ self.strategies = {
+ 'front_loading': self.apply_front_loading,
+ 'back_loading': self.apply_back_loading,
+ 'confirmed_items': self.apply_confirmed_items_loading,
+ 'variable_items': self.apply_variable_items_discount
+ }
+
+ def apply_strategy(self, items_df, strategy, params=None):
+ """تطبيق استراتيجية تسعير غير متزن على البنود"""
+ # نسخة من البيانات المدخلة للعمل عليها
+ df = items_df.copy()
+
+ # إضافة عمود إستراتيجية التسعير إذا لم يكن موجوداً
+ if 'إستراتيجية التسعير' not in df.columns:
+ df['إستراتيجية التسعير'] = 'متوازن'
+
+ # تطبيق الإستراتيجية المطلوبة
+ if strategy in self.strategies:
+ df = self.strategies[strategy](df, params)
+ else:
+ # إذا كانت الإستراتيجية غير معروفة، أعد البيانات بدون تغيير
+ pass
+
+ # حساب الإجمالي بعد التعديل
+ df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة']
+
+ return df
+
+ def apply_front_loading(self, items_df, params=None):
+ """تطبيق استراتيجية التحميل الأمامي (Front Loading)"""
+ df = items_df.copy()
+
+ # استخراج المعلمات الافتراضية إذا لم يتم تحديدها
+ if params is None:
+ params = {
+ 'early_increase': 1.3, # زيادة 30% للبنود المبكرة
+ 'late_decrease': 0.7, # تخفيض 30% للبنود المتأخرة
+ 'early_percentage': 0.33, # نسبة البنود المبكرة 33%
+ 'late_percentage': 0.33 # نسبة البنود المتأخرة 33%
+ }
+
+ # تحديد البنود المبكرة والمتأخرة والمتوسطة
+ items_count = len(df)
+ early_count = int(items_count * params['early_percentage'])
+ late_count = int(items_count * params['late_percentage'])
+
+ early_items = df.iloc[:early_count].index
+ middle_items = df.iloc[early_count:items_count-late_count].index
+ late_items = df.iloc[items_count-late_count:].index
+
+ # تطبيق الزيادة والنقصان
+ for idx in early_items:
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['early_increase']
+ df.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
+
+ for idx in middle_items:
+ df.at[idx, 'إستراتيجية التسعير'] = 'متوازن'
+
+ for idx in late_items:
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['late_decrease']
+ df.at[idx, 'إستراتيجية التسعير'] = 'نقص'
+
+ return df
+
+ def apply_back_loading(self, items_df, params=None):
+ """تطبيق استراتيجية التحميل الخلفي (Back Loading)"""
+ df = items_df.copy()
+
+ # استخراج المعلمات الافتراضية إذا لم يتم تحديدها
+ if params is None:
+ params = {
+ 'early_decrease': 0.7, # تخفيض 30% للبنود المبكرة
+ 'late_increase': 1.3, # زيادة 30% للبنود المتأخرة
+ 'early_percentage': 0.33, # نسبة البنود المبكرة 33%
+ 'late_percentage': 0.33 # نسبة البنود المتأخرة 33%
+ }
+
+ # تحديد البنود المبكرة والمتأخرة والمتوسطة
+ items_count = len(df)
+ early_count = int(items_count * params['early_percentage'])
+ late_count = int(items_count * params['late_percentage'])
+
+ early_items = df.iloc[:early_count].index
+ middle_items = df.iloc[early_count:items_count-late_count].index
+ late_items = df.iloc[items_count-late_count:].index
+
+ # تطبيق الزيادة والنقصان
+ for idx in early_items:
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['early_decrease']
+ df.at[idx, 'إستراتيجية التسعير'] = 'نقص'
+
+ for idx in middle_items:
+ df.at[idx, 'إستراتيجية التسعير'] = 'متوازن'
+
+ for idx in late_items:
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['late_increase']
+ df.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
+
+ return df
+
+ def apply_confirmed_items_loading(self, items_df, params=None):
+ """تطبيق استراتيجية تحميل البنود المؤكدة"""
+ df = items_df.copy()
+
+ # استخراج المعلمات الافتراضية إذا لم يتم تحديدها
+ if params is None:
+ params = {
+ 'confirmed_increase': 1.25, # زيادة 25% للبنود المؤكدة
+ 'others_decrease': 0.85, # تخفيض 15% للبنود الأخرى
+ 'confirmed_items_indices': [] # قائمة مؤشرات البنود المؤكدة
+ }
+
+ # إذا لم يتم تحديد البنود المؤكدة، استخدم قواعد اختيار افتراضية
+ if not params['confirmed_items_indices']:
+ # البنود التي تحتوي على كلمات مثل "أساسات" أو "هيكل" عادة ما تكون مؤكدة
+ confirmed_items = []
+ for idx, row in df.iterrows():
+ description = row['وصف البند'].lower()
+ if any(term in description for term in ['أساس', 'خرسان', 'هيكل', 'إنشائي']):
+ confirmed_items.append(idx)
+ else:
+ confirmed_items = params['confirmed_items_indices']
+
+ # تحديد البنود غير المؤكدة
+ all_indices = set(range(len(df)))
+ confirmed_indices = set(confirmed_items)
+ variable_indices = list(all_indices - confirmed_indices)
+
+ # تطبيق الزيادة والنقصان
+ for idx in confirmed_items:
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['confirmed_increase']
+ df.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
+
+ for idx in variable_indices:
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['others_decrease']
+ df.at[idx, 'إستراتيجية التسعير'] = 'نقص'
+
+ return df
+
+ def apply_variable_items_discount(self, items_df, params=None):
+ """تطبيق استراتيجية تخفيض البنود المحتمل زيادتها"""
+ df = items_df.copy()
+
+ # استخراج المعلمات الافتراضية إذا لم يتم تحديدها
+ if params is None:
+ params = {
+ 'variable_decrease': 0.7, # تخفيض 30% للبنود المحتمل زيادتها
+ 'others_increase': 1.15, # زيادة 15% للبنود الأخرى
+ 'variable_items_indices': [] # قائمة مؤشرات البنود المحتمل زيادتها
+ }
+
+ # إذا لم يتم تحديد البنود المحتمل زيادتها، استخدم قواعد اختيار افتراضية
+ if not params['variable_items_indices']:
+ # البنود التي تحتوي على كلمات مثل "حفر" أو "ردم" عادة ما تكون محتمل زيادتها
+ variable_items = []
+ for idx, row in df.iterrows():
+ description = row['وصف البند'].lower()
+ if any(term in description for term in ['حفر', 'ردم', 'تمديد', 'صرف', 'مياه']):
+ variable_items.append(idx)
+ else:
+ variable_items = params['variable_items_indices']
+
+ # تحديد البنود الأخرى
+ all_indices = set(range(len(df)))
+ variable_indices = set(variable_items)
+ other_indices = list(all_indices - variable_indices)
+
+ # تطبيق الزيادة والنقصان
+ for idx in variable_items:
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['variable_decrease']
+ df.at[idx, 'إستراتيجية التسعير'] = 'نقص'
+
+ for idx in other_indices:
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['others_increase']
+ df.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
+
+ return df
+
+ def calibrate_prices(self, original_df, unbalanced_df):
+ """معايرة الأسعار للحفاظ على إجمالي التسعير الأصلي"""
+ # حساب الإجماليات
+ original_total = original_df['الإجمالي'].sum()
+ unbalanced_total = unbalanced_df['الإجمالي'].sum()
+
+ # نسخة من البيانات المدخلة للعمل عليها
+ df = unbalanced_df.copy()
+
+ # حساب معامل التعديل
+ adjustment_factor = original_total / unbalanced_total if unbalanced_total > 0 else 1.0
+
+ # تعديل الأسعار
+ df['سعر الوحدة'] = df['سعر الوحدة'] * adjustment_factor
+
+ # حساب الإجمالي بعد التعديل
+ df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة']
+
+ return df
\ No newline at end of file
diff --git a/modules/pricing/specs_analyzer.py b/modules/pricing/specs_analyzer.py
new file mode 100644
index 0000000000000000000000000000000000000000..9db1b7f586f7317f57fcf81089a4f79d5bd0d09b
--- /dev/null
+++ b/modules/pricing/specs_analyzer.py
@@ -0,0 +1,527 @@
+"""
+تطبيق وحدة التسعير المتكاملة
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import plotly.express as px
+import plotly.graph_objects as go
+from datetime import datetime
+import random
+import os
+import time
+import io
+
+from modules.pricing.services.standard_pricing import StandardPricing
+from modules.pricing.services.unbalanced_pricing import UnbalancedPricing
+from modules.pricing.services.local_content import LocalContentCalculator
+from modules.pricing.services.price_prediction import PricePrediction
+from utils.excel_handler import export_to_excel
+from utils.helpers import format_number, format_currency
+
+
+class PricingApp:
+ """وحدة التسعير المتكاملة"""
+
+ def __init__(self):
+ self.pricing_methods = [
+ "التسعير القياسي",
+ "التسعير غير المتزن",
+ "التسعير التنافسي",
+ "التسعير الموجه بالربحية"
+ ]
+
+ # تهيئة خدمات التسعير
+ self.standard_pricing = StandardPricing()
+ self.unbalanced_pricing = UnbalancedPricing()
+ self.local_content = LocalContentCalculator()
+ self.price_prediction = PricePrediction()
+
+ def render(self):
+ """عرض واجهة وحدة التسعير"""
+
+ st.markdown("
وحدة التسعير المتكاملة
", unsafe_allow_html=True)
+
+ tabs = st.tabs([
+ "إنشاء تسعير جديد",
+ "نموذج التسعير الشامل",
+ "التسعير غير المتزن",
+ "المحتوى المحلي"
+ ])
+
+ with tabs[0]:
+ self._render_new_pricing_tab()
+
+ with tabs[1]:
+ self._render_comprehensive_pricing_tab()
+
+ with tabs[2]:
+ self._render_unbalanced_pricing_tab()
+
+ with tabs[3]:
+ self._render_local_content_tab()
+
+ def _render_new_pricing_tab(self):
+ """عرض تبويب إنشاء تسعير جديد"""
+
+ st.markdown("### إنشاء تسعير جديد")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ tender_name = st.text_input("اسم المناقصة")
+ client = st.text_input("الجهة المالكة")
+ pricing_method = st.selectbox("طريقة التسعير", self.pricing_methods)
+
+ with col2:
+ tender_number = st.text_input("رقم المناقصة")
+ location = st.text_input("الموقع")
+ submission_date = st.date_input("تاريخ التقديم")
+
+ # خيارات بيانات البنود
+ st.markdown("### بيانات البنود")
+
+ data_source = st.radio(
+ "مصدر بيانات البنود",
+ ["إدخال يدوي", "استيراد من Excel", "استيراد من وحدة تحليل المستندات"]
+ )
+
+ if data_source == "إدخال يدوي":
+ # إنشاء بيانات افتراضية
+ if 'manual_items' not in st.session_state:
+ st.session_state.manual_items = pd.DataFrame({
+ 'رقم البند': [f"A{i}" for i in range(1, 6)],
+ 'وصف البند': [
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
+ "توريد وتركيب حديد التسليح للأساسات",
+ "أعمال العزل المائي للأساسات",
+ "أعمال الردم والدك للأساسات",
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة"
+ ],
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3"],
+ 'الكمية': [250, 25, 500, 300, 120],
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0],
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0]
+ })
+
+ # عرض جدول البنود مع إمكانية التعديل
+ edited_items = st.data_editor(
+ st.session_state.manual_items,
+ use_container_width=True,
+ hide_index=True,
+ num_rows="dynamic"
+ )
+ st.session_state.manual_items = edited_items
+
+ elif data_source == "استيراد من Excel":
+ uploaded_file = st.file_uploader("رفع ملف Excel", type=["xlsx", "xls"])
+
+ if uploaded_file is not None:
+ st.success("تم رفع الملف بنجاح")
+ # محاكاة قراءة الملف
+ st.markdown("### معاينة البيانات المستوردة")
+
+ # إنشاء بيانات افتراضية
+ import_items = pd.DataFrame({
+ 'رقم البند': [f"A{i}" for i in range(1, 8)],
+ 'وصف البند': [
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
+ "توريد وتركيب حديد التسليح للأساسات",
+ "أعمال العزل المائي للأساسات",
+ "أعمال الردم والدك للأساسات",
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة",
+ "توريد وتركيب حديد التسليح للأعمدة",
+ "أعمال البلوك للجدران"
+ ],
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"],
+ 'الكمية': [250, 25, 500, 300, 120, 10, 400],
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+ })
+
+ st.dataframe(import_items)
+
+ if st.button("استيراد البيانات"):
+ st.session_state.manual_items = import_items.copy()
+ st.session_state.manual_items_modified = True
+ st.success("تم استيراد البيانات بنجاح!")
+
+ else: # استيراد من وحدة تحليل المستندات
+ available_documents = [
+ "كراسة شروط مشروع توسعة مستشفى الملك فهد",
+ "جدول كميات صيانة محطات المياه",
+ "مخططات إنشاء مدرسة ثانوية"
+ ]
+
+ selected_doc = st.selectbox("اختر المستند", available_documents)
+
+ if st.button("استيراد البيانات من تحليل المستند"):
+ # محاكاة استيراد البيانات
+ with st.spinner("جاري استيراد البيانات..."):
+ time.sleep(2)
+
+ # إنشاء بيانات افتراضية
+ doc_items = pd.DataFrame({
+ 'رقم البند': [f"A{i}" for i in range(1, 8)],
+ 'وصف البند': [
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
+ "توريد وتركيب حديد التسليح للأساسات",
+ "أعمال العزل المائي للأساسات",
+ "أعمال الردم والدك للأساسات",
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة",
+ "توريد وتركيب حديد التسليح للأعمدة",
+ "أعمال البلوك للجدران"
+ ],
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"],
+ 'الكمية': [250, 25, 500, 300, 120, 10, 400],
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+ })
+
+ st.session_state.manual_items = doc_items.copy()
+ st.success("تم استيراد البيانات من تحليل المستند بنجاح!")
+ st.dataframe(doc_items)
+
+ # زر بدء التسعير
+ if st.button("بدء التسعير"):
+ # تحقق من صحة البيانات
+ if 'manual_items' in st.session_state and not st.session_state.manual_items.empty:
+ # حفظ بيانات التسعير الحالي
+ st.session_state.current_pricing = {
+ 'name': tender_name,
+ 'number': tender_number,
+ 'client': client,
+ 'location': location,
+ 'method': pricing_method,
+ 'submission_date': submission_date,
+ 'items': st.session_state.manual_items.copy(),
+ 'status': 'جديد',
+ 'created_at': datetime.now()
+ }
+
+ # الانتقال إلى تبويب نموذج التسعير الشامل
+ st.success("تم إنشاء التسعير بنجاح! يمكنك الانتقال إلى نموذج التسعير الشامل.")
+ else:
+ st.error("يرجى إدخال بيانات البنود أولاً.")
+
+ def _render_comprehensive_pricing_tab(self):
+ """عرض تبويب نموذج التسعير الشامل"""
+
+ st.markdown("### نموذج التسعير الشامل")
+
+ # التحقق من وجود تسعير حالي
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
+ return
+
+ # عرض معلومات التسعير الحالي
+ pricing = st.session_state.current_pricing
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric("اسم المناقصة", pricing['name'])
+ st.metric("الجهة المالكة", pricing['client'])
+
+ with col2:
+ st.metric("رقم المناقصة", pricing['number'])
+ st.metric("تاريخ التقديم", pricing['submission_date'].strftime("%Y-%m-%d"))
+
+ with col3:
+ st.metric("طريقة التسعير", pricing['method'])
+ st.metric("الموقع", pricing['location'])
+
+ # عرض البنود والتسعير
+ st.markdown("### بنود التسعير")
+
+ items = pricing['items'].copy()
+
+ # إضافة أسعار الوحدة للمحاكاة
+ if 'سعر الوحدة' in items.columns and (items['سعر الوحدة'] == 0).all():
+ items['سعر الوحدة'] = [
+ round(random.uniform(1000, 3000), 2), # الخرسانة
+ round(random.uniform(5000, 7000), 2), # الحديد
+ round(random.uniform(100, 200), 2), # العزل
+ round(random.uniform(50, 100), 2), # الردم
+ round(random.uniform(1200, 3500), 2), # الخرسانة للأعمدة
+ ]
+
+ if len(items) > 5:
+ for i in range(5, len(items)):
+ items.at[i, 'سعر الوحدة'] = round(random.uniform(500, 5000), 2)
+
+ # حساب الإجمالي
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
+
+ # عرض الجدول مع إمكانية التعديل
+ edited_items = st.data_editor(
+ items,
+ use_container_width=True,
+ hide_index=True,
+ disabled=('رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'الإجمالي')
+ )
+
+ # حساب الإجمالي بعد التعديل
+ edited_items['الإجمالي'] = edited_items['الكمية'] * edited_items['سعر الوحدة']
+ st.session_state.current_pricing['items'] = edited_items
+
+ # حساب وعرض إجماليات التسعير
+ total_price = edited_items['الإجمالي'].sum()
+
+ st.markdown("### إجماليات التسعير")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric("إجمالي التكاليف المباشرة", f"{total_price:,.2f} ريال")
+
+ with col2:
+ overhead_percentage = st.slider("نسبة المصاريف العامة والأرباح (%)", 5, 30, 15)
+ overhead_value = total_price * overhead_percentage / 100
+ st.metric("المصاريف العامة والأرباح", f"{overhead_value:,.2f} ريال")
+
+ with col3:
+ grand_total = total_price + overhead_value
+ st.metric("الإجمالي النهائي", f"{grand_total:,.2f} ريال")
+
+ # رسم بياني لتوزيع التكاليف
+ st.markdown("### تحليل التكاليف")
+
+ # حساب النسب المئوية لكل بند
+ pie_data = edited_items.copy()
+ pie_data['نسبة من إجمالي التكاليف'] = pie_data['الإجمالي'] / total_price * 100
+
+ fig = px.pie(
+ pie_data,
+ values='نسبة من إجمالي التكاليف',
+ names='وصف البند',
+ title='توزيع التكاليف حسب البنود',
+ hole=0.4
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # أزرار العمليات
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ if st.button("حفظ التسعير"):
+ st.success("تم حفظ التسعير بنجاح!")
+
+ with col2:
+ if st.button("تصدير إلى Excel"):
+ st.success("تم تصدير التسعير إلى Excel بنجاح!")
+
+ with col3:
+ if st.button("تحليل المخاطر المالية"):
+ st.success("تم إرسال الطلب إلى وحدة تحليل المخاطر!")
+
+ def _render_unbalanced_pricing_tab(self):
+ """عرض تبويب التسعير غير المتزن"""
+
+ st.markdown("### التسعير غير المتزن")
+
+ # التحقق من وجود تسعير حالي
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
+ return
+
+ # شرح التسعير غير المتزن
+ with st.expander("ما هو التسعير غير المتزن؟", expanded=False):
+ st.markdown("""
+ **التسعير غير المتزن** هو استراتيجية تسعير تقوم على توزيع التكاليف بين بنود المناقصة بشكل غير متساوٍ، مع الحفاظ على إجمالي قيمة العطاء.
+
+ ### استراتيجيات التسعير غير المتزن:
+
+ 1. **التحميل الأمامي (Front Loading)**: زيادة أسعار البنود المبكرة في المشروع للحصول على تدفق نقدي أفضل في بداية المشروع.
+ 2. **التحميل الخلفي (Back Loading)**: زيادة أسعار البنود المتأخرة في المشروع.
+ 3. **تحميل البنود المؤكدة**: زيادة أسعار البنود التي من المؤكد تنفيذها بالكميات المحددة.
+ 4. **تخفيض أسعار البنود المحتملة**: تخفيض أسعار البنود التي قد تزيد كمياتها أثناء التنفيذ.
+
+ ### مزايا التسعير غير المتزن:
+
+ - تحسين التدفق النقدي للمشروع.
+ - تعظيم الربحية في حالة التغييرات والأوامر التغييرية.
+ - زيادة فرص الفوز بالمناقصة.
+
+ ### مخاطر التسعير غير المتزن:
+
+ - قد يتم رفض العطاء إذا كان عدم التوازن واضحاً.
+ - قد تتأثر السمعة سلباً إذا تم استخدامه بشكل مفرط.
+ - قد يؤدي إلى خسائر إذا لم يتم تنفيذ البنود ذات الأسعار العالية.
+ """)
+
+ # عرض بنود التسعير الحالي
+ items = st.session_state.current_pricing['items'].copy()
+
+ # إضافة عمود إستراتيجية التسعير
+ if 'إستراتيجية التسعير' not in items.columns:
+ items['إستراتيجية التسعير'] = 'متوازن'
+
+ st.markdown("### إستراتيجية التسعير غير المتزن")
+
+ # اختيار الإستراتيجية
+ strategy = st.selectbox(
+ "اختر إستراتيجية التسعير",
+ [
+ "تحميل أمامي (Front Loading)",
+ "تحميل البنود المؤكدة",
+ "تخفيض البنود المحتمل زيادتها",
+ "إستراتيجية مخصصة"
+ ]
+ )
+
+ # تطبيق الإستراتيجية المختارة
+ if strategy == "تحميل أمامي (Front Loading)":
+ # محاكاة تحميل أمامي
+ items_count = len(items)
+ early_items = items.iloc[:items_count//3].index
+ middle_items = items.iloc[items_count//3:2*items_count//3].index
+ late_items = items.iloc[2*items_count//3:].index
+
+ # تطبيق الزيادة والنقصان
+ for idx in early_items:
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.3 # زيادة 30%
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
+
+ for idx in middle_items:
+ items.at[idx, 'إستراتيجية التسعير'] = 'متوازن'
+
+ for idx in late_items:
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30%
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
+
+ elif strategy == "تحميل البنود المؤكدة":
+ # محاكاة - اعتبار بعض البنود مؤكدة
+ confirmed_items = [0, 2, 4] # الأصفار-مستندة
+ variable_items = [idx for idx in range(len(items)) if idx not in confirmed_items]
+
+ # تطبيق الزيادة والنقصان
+ for idx in confirmed_items:
+ if idx < len(items):
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.25 # زيادة 25%
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
+
+ for idx in variable_items:
+ if idx < len(items):
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.85 # نقص 15%
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
+
+ elif strategy == "تخفيض البنود المحتمل زيادتها":
+ # محاكاة - اعتبار بعض البنود محتمل زيادتها
+ variable_items = [1, 3] # الأصفار-مستندة
+ other_items = [idx for idx in range(len(items)) if idx not in variable_items]
+
+ # تطبيق الزيادة والنقصان
+ for idx in variable_items:
+ if idx < len(items):
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30%
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
+
+ for idx in other_items:
+ if idx < len(items):
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.15 # زيادة 15%
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
+
+ else: # إستراتيجية مخصصة
+ st.markdown("### تعديل أسعار البنود يدوياً")
+ st.markdown("قم بتعديل أسعار البنود وإستراتيجية التسعير يدوياً.")
+
+ # حساب الإجمالي بعد التعديل
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
+
+ # تعيين ألوان للإستراتيجيات
+ def highlight_strategy(val):
+ if val == 'زيادة':
+ return 'background-color: #a8e6cf'
+ elif val == 'نقص':
+ return 'background-color: #ff9aa2'
+ return ''
+
+ # عرض الجدول مع تنسيق
+ st.markdown("### بنود التسعير غير المتزن")
+ styled_items = items.style.applymap(highlight_strategy, subset=['إستراتيجية التسعير'])
+ st.dataframe(styled_items, use_container_width=True)
+
+ # المقارنة بين التسعير المتوازن وغير المتوازن
+ st.markdown("### مقارنة التسعير المتوازن وغير المتوازن")
+
+ original_items = st.session_state.current_pricing['items'].copy()
+ original_total = original_items['الإجمالي'].sum()
+ unbalanced_total = items['الإجمالي'].sum()
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric("إجمالي التسعير المتوازن", f"{original_total:,.2f} ريال")
+
+ with col2:
+ st.metric("إجمالي التسعير غير المتوازن", f"{unbalanced_total:,.2f} ريال")
+
+ with col3:
+ diff = unbalanced_total - original_total
+ st.metric("الفرق", f"{diff:,.2f} ريال", delta=f"{diff/original_total*100:.1f}%")
+
+ # المعايرة للحفاظ على إجمالي التسعير
+ if abs(diff) > 1: # إذا كان هناك فرق كبير
+ if st.button("معايرة الأسعار للحفاظ على إجمالي التسعير"):
+ # تعديل الأسعار للحفاظ على إجمالي التكلفة
+ adjustment_factor = original_total / unbalanced_total
+ items['سعر الوحدة'] = items['سعر الوحدة'] * adjustment_factor
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
+
+ st.success(f"تم تعديل الأسعار للحفاظ على إجمالي التسعير الأصلي ({original_total:,.2f} ريال)")
+ st.dataframe(items, use_container_width=True)
+
+ # رسم بياني للمقارنة
+ st.markdown("### تحليل بصري للتسعير غير المتوازن")
+
+ # إعداد البيانات للرسم البياني
+ chart_data = pd.DataFrame({
+ 'وصف البند': original_items['وصف البند'],
+ 'التسعير المتوازن': original_items['الإجمالي'],
+ 'التسعير غير المتوازن': items['الإجمالي']
+ })
+
+ # رسم بياني شريطي للمقارنة
+ fig = go.Figure()
+
+ fig.add_trace(go.Bar(
+ x=chart_data['وصف البند'],
+ y=chart_data['التسعير المتوازن'],
+ name='التسعير المتوازن',
+ marker_color='rgb(55, 83, 109)'
+ ))
+
+ fig.add_trace(go.Bar(
+ x=chart_data['وصف البند'],
+ y=chart_data['التسعير غير المتوازن'],
+ name='التسعير غير المتوازن',
+ marker_color='rgb(26, 118, 255)'
+ ))
+
+ fig.update_layout(
+ title='مقارنة بين التسعير المتوازن وغير المتوازن',
+ xaxis_tickfont_size=14,
+ yaxis=dict(
+ title='الإجمالي (ريال)',
+ titlefont_size=16,
+ tickfont_size=14,
+ ),
+ legend=dict(
+ x=0,
+ y=1.0,
+ bgcolor='rgba(255, 255, 255, 0)',
+ bordercolor='rgba(255, 255, 255, 0)'
+ ),
+ barmode='group',
+ bargap=0.15,
+ bargroupgap=0.1
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # زر حفظ التسعير غير المتوازن
+ if st.button("
\ No newline at end of file
diff --git a/modules/project_tracker/__init__.py b/modules/project_tracker/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3d9b980b1d848af2732c586ff297deb2fdf8995e
--- /dev/null
+++ b/modules/project_tracker/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+"""
+وحدة متتبع حالة المشروع المتحرك مع تصور التقدم
+"""
\ No newline at end of file
diff --git a/modules/project_tracker/status_tracker.py b/modules/project_tracker/status_tracker.py
new file mode 100644
index 0000000000000000000000000000000000000000..8a304fdc87c1ca5a30ff7a9dc736adaa764da7ed
--- /dev/null
+++ b/modules/project_tracker/status_tracker.py
@@ -0,0 +1,1740 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+وحدة متتبع حالة المشروع المتحرك مع تصور التقدم
+"""
+
+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, # مؤشر أداء الجدول الزمني (SPI)
+ 'cpi': 0.98, # مؤشر أداء التكلفة (CPI)
+ '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']:
+ # إضافة القيمة الجديدة للاتجاه وحذف أقدم قيمة إذا تجاوز العدد 5
+ 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("
متتبع حالة المشروع المتحرك
", unsafe_allow_html=True)
+
+ st.markdown("""
+
+ متتبع حالة المشروع المتحرك يوفر عرضاً تفاعلياً ومرئياً لحالة المشروع ومراحله المختلفة،
+ مع إمكانية متابعة مؤشرات الأداء الرئيسية وتتبع التقدم بشكل مباشر.
+
+ """, 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("
", unsafe_allow_html=True)
+
+ if not self.kpi_data['issues']:
+ st.info("لا توجد مشكلات مسجلة للمشروع.")
+ return
+
+ # تحويل المشكلات إلى DataFrame
+ issues_data = pd.DataFrame(self.kpi_data['issues'])
+
+ # تنسيق الجدول
+ st.markdown("""
+
+ """, 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"""
+
+
+
#{issue['id']} - {issue['description']}
+
+ {issue['severity']}
+ {issue['status']}
+
+
+
+
+
تاريخ الإنشاء: {issue['created_date']}
+
المسؤول: {issue['responsible']}
+
+
+
خطة المعالجة: {issue['resolution']}
+
+
+
+ """, 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("
تحديث حالة المشروع
", 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):
+ """عرض واجهة متتبع حالة المشروع"""
+ # إضافة CSS مخصص
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+ # عرض لوحة تحكم حالة المشروع
+ self.render_project_status_dashboard()
\ No newline at end of file
diff --git a/modules/project_tracker/tracker_app.py b/modules/project_tracker/tracker_app.py
new file mode 100644
index 0000000000000000000000000000000000000000..3fa249317afc63249666dbecd7802b8e6f902e94
--- /dev/null
+++ b/modules/project_tracker/tracker_app.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+وحدة تطبيق متتبع حالة المشروع المتحرك مع تصور التقدم
+"""
+
+import os
+import sys
+import streamlit as st
+import pandas as pd
+import numpy as np
+from datetime import datetime, timedelta
+
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
+
+# استيراد مكونات متتبع حالة المشروع
+from modules.project_tracker.status_tracker import ProjectStatusTracker
+
+
+class TrackerApp:
+ """وحدة تطبيق متتبع حالة المشروع المتحرك"""
+
+ def __init__(self, project_id=None, user_id=None):
+ """تهيئة وحدة تطبيق متتبع حالة المشروع المتحرك"""
+ self.project_tracker = ProjectStatusTracker(project_id, user_id)
+
+ def render(self):
+ """عرض واجهة وحدة تطبيق متتبع حالة المشروع المتحرك"""
+ self.project_tracker.render()
+
+
+# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
+if __name__ == "__main__":
+ st.set_page_config(
+ page_title="متتبع حالة المشروع المتحرك | WAHBi AI",
+ page_icon="📊",
+ layout="wide",
+ initial_sidebar_state="expanded"
+ )
+
+ app = TrackerApp()
+ app.render()
\ No newline at end of file
diff --git a/modules/projects/__init__.py b/modules/projects/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a6c17a73333f06321271be498c9a2e2a175542f
--- /dev/null
+++ b/modules/projects/__init__.py
@@ -0,0 +1 @@
+# ملف تهيئة حزمة إدارة المشاريع
\ No newline at end of file
diff --git a/modules/projects/projects_app.py b/modules/projects/projects_app.py
new file mode 100644
index 0000000000000000000000000000000000000000..a2996048f89249e786f202765bc753d054e48c3e
--- /dev/null
+++ b/modules/projects/projects_app.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+وحدة إدارة المشاريع لنظام تحليل العقود والمناقصات
+"""
+
+import os
+import sys
+import streamlit as st
+import pandas as pd
+import numpy as np
+
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
+
+# استيراد مكونات إدارة المشاريع
+from modules.projects.projects_management import ProjectsManagement
+
+
+class ProjectsApp:
+ """وحدة إدارة المشاريع الرئيسية"""
+
+ def __init__(self):
+ """تهيئة وحدة إدارة المشاريع"""
+ self.projects_management = ProjectsManagement()
+
+ def render(self):
+ """عرض واجهة وحدة إدارة المشاريع"""
+ st.markdown("
وحدة إدارة المشاريع
", unsafe_allow_html=True)
+
+ st.markdown("""
+
+ تمكنك وحدة إدارة المشاريع من إنشاء وتتبع وإدارة المشاريع بكفاءة، مع ميزات متقدمة لمراقبة المواعيد النهائية والموارد.
+ يمكنك إضافة معلومات تفصيلية للمشاريع، بما في ذلك معلومات الموقع، مرئيات المدير، والمخاطر والمميزات.
+
+ """, unsafe_allow_html=True)
+
+ # عرض نموذج إدارة المشاريع
+ self.projects_management.render()
+
+
+# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
+if __name__ == "__main__":
+ st.set_page_config(
+ page_title="إدارة المشاريع | WAHBi AI",
+ page_icon="🏗️",
+ layout="wide",
+ initial_sidebar_state="expanded"
+ )
+
+ app = ProjectsApp()
+ app.render()
\ No newline at end of file
diff --git a/modules/projects/projects_management.py b/modules/projects/projects_management.py
new file mode 100644
index 0000000000000000000000000000000000000000..812d74bb3eac0b29d54ef2ff3b3210c749983096
--- /dev/null
+++ b/modules/projects/projects_management.py
@@ -0,0 +1,942 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+نموذج إدارة المشاريع لنظام WAHBi-AI
+يتضمن ميزات إضافة وإدارة المشاريع الجديدة مع معلومات موسعة
+"""
+
+import os
+import sys
+import streamlit as st
+import pandas as pd
+import numpy as np
+import datetime
+import tempfile
+from pathlib import Path
+import plotly.express as px
+import plotly.graph_objects as go
+import json
+import time
+from datetime import datetime, timedelta
+
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
+
+# استيراد مكونات واجهة المستخدم
+from utils.components.header import render_header
+from utils.components.credits import render_credits
+from utils.helpers import format_number, format_currency, styled_button
+
+class ProjectsManagement:
+ """نموذج إدارة المشاريع"""
+
+ def __init__(self):
+ """تهيئة نموذج إدارة المشاريع"""
+ # تهيئة حالة الجلسة
+ if 'projects' not in st.session_state:
+ st.session_state.projects = []
+
+ if 'project_files' not in st.session_state:
+ st.session_state.project_files = {}
+
+ if 'project_inquiries' not in st.session_state:
+ st.session_state.project_inquiries = {}
+
+ # ضمان وجود مجلد لحفظ ملفات المشاريع
+ self.projects_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/projects"))
+ os.makedirs(self.projects_dir, exist_ok=True)
+
+ def render(self):
+ """عرض واجهة إدارة المشاريع"""
+ # عرض الشعار والعنوان الرئيسي
+ render_header("إدارة المشاريع")
+
+ # تبويبات إدارة المشاريع
+ tabs = st.tabs(["المشاريع الحالية", "إضافة مشروع جديد", "أرشيف المشاريع", "التقارير"])
+
+ with tabs[0]:
+ self._render_current_projects()
+
+ with tabs[1]:
+ self._render_new_project_form()
+
+ with tabs[2]:
+ self._render_archived_projects()
+
+ with tabs[3]:
+ self._render_projects_reports()
+
+ # عرض الحقوق
+ render_credits()
+
+ def _render_current_projects(self):
+ """عرض قائمة المشاريع الحالية"""
+ st.markdown("""
+
+
🏗️ المشاريع الحالية
+
قائمة المشاريع النشطة التي يتم العمل عليها حالياً.
+
+ """, unsafe_allow_html=True)
+
+ # تصفية المشاريع النشطة
+ active_projects = [p for p in st.session_state.projects if p.get("status") != "archived"]
+
+ if not active_projects:
+ st.info("لا توجد مشاريع نشطة حالياً. يمكنك إضافة مشروع جديد من تبويب 'إضافة مشروع جديد'.")
+ return
+
+ # عرض المشاريع النشطة
+ for idx, project in enumerate(active_projects):
+ with st.expander(f"{project.get('name')} - {project.get('client')}"):
+ self._render_project_details(project, idx)
+
+ def _render_project_details(self, project, idx):
+ """عرض تفاصيل مشروع محدد"""
+ # معلومات أساسية
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.markdown(f"**اسم المشروع:** {project.get('name')}")
+ st.markdown(f"**رقم المشروع:** {project.get('number')}")
+
+ with col2:
+ st.markdown(f"**العميل:** {project.get('client')}")
+ st.markdown(f"**الموقع:** {project.get('location')}")
+
+ with col3:
+ st.markdown(f"**تاريخ البدء:** {project.get('start_date')}")
+ st.markdown(f"**تاريخ التقديم:** {project.get('submission_date')}")
+
+ # تبويبات تفاصيل المشروع
+ project_tabs = st.tabs([
+ "معلومات المشروع",
+ "مرئيات مدير المنطقة",
+ "صور وفيديوهات الموقع",
+ "مميزات ومخاطر المشروع",
+ "استفسارات المالك",
+ "معلومات الموقع"
+ ])
+
+ # تبويب معلومات المشروع
+ with project_tabs[0]:
+ # المعلومات الأساسية
+ st.markdown("### معلومات المشروع الأساسية")
+ st.markdown(f"**نوع المشروع:** {project.get('type')}")
+ st.markdown(f"**القيمة التقديرية:** {format_currency(project.get('estimated_value', 0))} ريال")
+ st.markdown(f"**المدة المتوقعة:** {project.get('duration')} يوم")
+ st.markdown(f"**حالة المشروع:** {project.get('status')}")
+
+ # جدول زمني للمشروع
+ st.markdown("### الجدول الزمني للمشروع")
+
+ # حساب الأيام المتبقية للتقديم
+ if project.get('submission_date'):
+ try:
+ submission_date = datetime.strptime(project.get('submission_date'), "%Y-%m-%d")
+ today = datetime.now()
+ days_remaining = (submission_date - today).days
+
+ # عرض شريط التقدم للوقت المتبقي
+ if days_remaining > 0:
+ st.markdown(f"**الوقت المتبقي للتقديم:** {days_remaining} يوم")
+ progress_pct = min(1.0, max(0.0, days_remaining / 30.0)) # افتراض 30 يوم كمدة قياسية
+ st.progress(progress_pct)
+ else:
+ st.error(f"**انتهت مدة التقديم منذ:** {abs(days_remaining)} يوم")
+ except:
+ st.warning("تعذر حساب الأيام المتبقية. يرجى التأكد من صحة التاريخ.")
+
+ # أزرار العمليات
+ col1, col2 = st.columns(2)
+ with col1:
+ if styled_button("تحديث حالة المشروع", key=f"update_project_{idx}", type="primary", icon="🔄"):
+ st.session_state.project_to_update = idx
+
+ with col2:
+ if styled_button("تصدير معلومات المشروع", key=f"export_project_{idx}", type="success", icon="📤"):
+ st.session_state.project_to_export = idx
+
+ # تبويب مرئيات مدير المنطقة
+ with project_tabs[1]:
+ st.markdown("### مرئيات مدير المنطقة")
+
+ # عرض المرئيات الموجودة
+ manager_insights = project.get('manager_insights', [])
+
+ if manager_insights:
+ for i, insight in enumerate(manager_insights):
+ with st.expander(f"مرئية #{i+1} - {insight.get('date')}"):
+ st.markdown(f"**العنوان:** {insight.get('title')}")
+ st.markdown(f"**التاريخ:** {insight.get('date')}")
+ st.markdown(f"**المحتوى:**\n{insight.get('content')}")
+
+ # عرض المرفقات إن وجدت
+ attachments = insight.get('attachments', [])
+ if attachments:
+ st.markdown("**المرفقات:**")
+ for att in attachments:
+ st.markdown(f"- {att}")
+ else:
+ st.info("لا توجد مرئيات مضافة لمدير المنطقة.")
+
+ # إضافة مرئية جديدة
+ st.markdown("### إضافة مرئية جديدة")
+
+ insight_title = st.text_input("عنوان المرئية", key=f"new_insight_title_{idx}")
+ insight_content = st.text_area("محتوى المرئية", key=f"new_insight_content_{idx}")
+ insight_file = st.file_uploader("إرفاق ملف (اختياري)", key=f"new_insight_file_{idx}")
+
+ if styled_button("إضافة مرئية", key=f"add_insight_{idx}", type="primary", icon="➕"):
+ if not insight_title or not insight_content:
+ st.error("يرجى تعبئة عنوان ومحتوى المرئية.")
+ else:
+ # إنشاء مرئية جديدة
+ new_insight = {
+ "title": insight_title,
+ "date": datetime.now().strftime("%Y-%m-%d"),
+ "content": insight_content,
+ "attachments": []
+ }
+
+ # حفظ الملف المرفق إن وجد
+ if insight_file:
+ file_path = self._save_project_file(project.get('number'), "insights", insight_file)
+ if file_path:
+ new_insight["attachments"].append(file_path)
+
+ # إضافة المرئية للمشروع
+ if 'manager_insights' not in project:
+ project['manager_insights'] = []
+
+ project['manager_insights'].append(new_insight)
+ st.success("تمت إضافة المرئية بنجاح!")
+ st.rerun()
+
+ # تبويب صور وفيديوهات الموقع
+ with project_tabs[2]:
+ st.markdown("### صور وفيديوهات الموقع")
+
+ # عرض الصور والفيديوهات الموجودة
+ site_media = project.get('site_media', [])
+
+ if site_media:
+ media_tabs = st.tabs(["الصور", "الفيديوهات", "مرفقات أخرى"])
+
+ # عرض الصور
+ with media_tabs[0]:
+ images = [m for m in site_media if m.get('type') == 'image']
+ if images:
+ for img in images:
+ st.markdown(f"**{img.get('title')}** - {img.get('date')}")
+ if 'file_path' in img:
+ try:
+ # يمكن تنفيذ عرض الصورة هنا إذا كانت متاحة
+ st.markdown(f"*مسار الملف:* {img.get('file_path')}")
+ except:
+ st.warning("تعذر عرض الصورة.")
+ st.markdown(f"*الوصف:* {img.get('description', '')}")
+ st.markdown("---")
+ else:
+ st.info("لا توجد صور مضافة للموقع.")
+
+ # عرض الفيديوهات
+ with media_tabs[1]:
+ videos = [m for m in site_media if m.get('type') == 'video']
+ if videos:
+ for vid in videos:
+ st.markdown(f"**{vid.get('title')}** - {vid.get('date')}")
+ if 'file_path' in vid:
+ st.markdown(f"*مسار الملف:* {vid.get('file_path')}")
+ st.markdown(f"*الوصف:* {vid.get('description', '')}")
+ st.markdown("---")
+ else:
+ st.info("لا توجد فيديوهات مضافة للموقع.")
+
+ # عرض مرفقات أخرى
+ with media_tabs[2]:
+ other_files = [m for m in site_media if m.get('type') not in ['image', 'video']]
+ if other_files:
+ for f in other_files:
+ st.markdown(f"**{f.get('title')}** - {f.get('date')}")
+ if 'file_path' in f:
+ st.markdown(f"*مسار الملف:* {f.get('file_path')}")
+ st.markdown(f"*الوصف:* {f.get('description', '')}")
+ st.markdown("---")
+ else:
+ st.info("لا توجد مرفقات أخرى للموقع.")
+ else:
+ st.info("لا توجد صور أو فيديوهات مضافة للموقع.")
+
+ # إضافة وسائط جديدة
+ st.markdown("### إضافة صور أو فيديوهات جديدة")
+
+ media_type = st.selectbox(
+ "نوع الملف",
+ options=["صورة", "فيديو", "ملف آخر"],
+ key=f"new_media_type_{idx}"
+ )
+
+ media_title = st.text_input("عنوان الملف", key=f"new_media_title_{idx}")
+ media_desc = st.text_area("وصف الملف", key=f"new_media_desc_{idx}")
+ media_file = st.file_uploader(
+ "اختر الملف",
+ type=["jpg", "jpeg", "png", "mp4", "avi", "mov", "pdf", "docx"] if media_type == "ملف آخر" else
+ ["mp4", "avi", "mov"] if media_type == "فيديو" else
+ ["jpg", "jpeg", "png"],
+ key=f"new_media_file_{idx}"
+ )
+
+ if styled_button("إضافة للموقع", key=f"add_media_{idx}", type="primary", icon="➕"):
+ if not media_title or not media_file:
+ st.error("يرجى تعبئة عنوان الملف واختيار الملف.")
+ else:
+ # تحديد نوع الملف لحفظه
+ file_type = "images" if media_type == "صورة" else "videos" if media_type == "فيديو" else "others"
+
+ # حفظ الملف
+ file_path = self._save_project_file(project.get('number'), file_type, media_file)
+
+ if file_path:
+ # إنشاء كائن الوسائط
+ new_media = {
+ "title": media_title,
+ "date": datetime.now().strftime("%Y-%m-%d"),
+ "description": media_desc,
+ "type": "image" if media_type == "صورة" else "video" if media_type == "فيديو" else "other",
+ "file_path": file_path
+ }
+
+ # إضافة الوسائط للمشروع
+ if 'site_media' not in project:
+ project['site_media'] = []
+
+ project['site_media'].append(new_media)
+ st.success(f"تمت إضافة {media_type} بنجاح!")
+ st.rerun()
+
+ # تبويب مميزات ومخاطر المشروع
+ with project_tabs[3]:
+ st.markdown("### مميزات ومخاطر المشروع")
+
+ # عرض المميزات والمخاطر في تبويبات
+ advantage_risk_tabs = st.tabs(["مميزات المشروع", "مخاطر المشروع"])
+
+ # تبويب مميزات المشروع
+ with advantage_risk_tabs[0]:
+ advantages = project.get('advantages', [])
+
+ if advantages:
+ for i, adv in enumerate(advantages):
+ st.markdown(f"**{i+1}. {adv.get('title')}**")
+ st.markdown(f"*التأثير:* {adv.get('impact')}")
+ st.markdown(f"{adv.get('description')}")
+ st.markdown("---")
+ else:
+ st.info("لم يتم إضافة مميزات للمشروع.")
+
+ # إضافة ميزة جديدة
+ st.markdown("### إضافة ميزة جديدة")
+ adv_title = st.text_input("عنوان الميزة", key=f"new_adv_title_{idx}")
+ adv_impact = st.selectbox(
+ "مستوى التأثير",
+ options=["منخفض", "متوسط", "عالي"],
+ key=f"new_adv_impact_{idx}"
+ )
+ adv_desc = st.text_area("وصف الميزة", key=f"new_adv_desc_{idx}")
+
+ if styled_button("إضافة ميزة", key=f"add_adv_{idx}", type="success", icon="✨"):
+ if not adv_title or not adv_desc:
+ st.error("يرجى تعبئة عنوان ووصف الميزة.")
+ else:
+ # إنشاء ميزة جديدة
+ new_adv = {
+ "title": adv_title,
+ "impact": adv_impact,
+ "description": adv_desc,
+ "date_added": datetime.now().strftime("%Y-%m-%d")
+ }
+
+ # إضافة الميزة للمشروع
+ if 'advantages' not in project:
+ project['advantages'] = []
+
+ project['advantages'].append(new_adv)
+ st.success("تمت إضافة الميزة بنجاح!")
+ st.rerun()
+
+ # تبويب مخاطر المشروع
+ with advantage_risk_tabs[1]:
+ risks = project.get('risks', [])
+
+ if risks:
+ for i, risk in enumerate(risks):
+ risk_color = "🔴" if risk.get('severity') == "عالي" else "🟠" if risk.get('severity') == "متوسط" else "🟡"
+ st.markdown(f"{risk_color} **{i+1}. {risk.get('title')}**")
+ st.markdown(f"*الحدة:* {risk.get('severity')} | *الاحتمالية:* {risk.get('probability')}%")
+ st.markdown(f"*الوصف:* {risk.get('description')}")
+ st.markdown(f"*الإجراءات المقترحة:* {risk.get('mitigation_plan')}")
+ st.markdown("---")
+ else:
+ st.info("لم يتم إضافة مخاطر للمشروع.")
+
+ # إضافة مخاطر جديدة
+ st.markdown("### إضافة مخاطر جديدة")
+ risk_title = st.text_input("عنوان المخاطرة", key=f"new_risk_title_{idx}")
+ risk_severity = st.selectbox(
+ "حدة المخاطرة",
+ options=["منخفض", "متوسط", "عالي"],
+ key=f"new_risk_severity_{idx}"
+ )
+ risk_probability = st.slider(
+ "احتمالية الحدوث (%)",
+ min_value=0,
+ max_value=100,
+ value=50,
+ key=f"new_risk_prob_{idx}"
+ )
+ risk_desc = st.text_area("وصف المخاطرة", key=f"new_risk_desc_{idx}")
+ risk_mitigation = st.text_area("خطة التخفيف المقترحة", key=f"new_risk_mitigation_{idx}")
+
+ if styled_button("إضافة مخاطرة", key=f"add_risk_{idx}", type="warning", icon="⚠️"):
+ if not risk_title or not risk_desc:
+ st.error("يرجى تعبئة عنوان ووصف المخاطرة.")
+ else:
+ # إنشاء مخاطرة جديدة
+ new_risk = {
+ "title": risk_title,
+ "severity": risk_severity,
+ "probability": risk_probability,
+ "description": risk_desc,
+ "mitigation_plan": risk_mitigation,
+ "date_added": datetime.now().strftime("%Y-%m-%d")
+ }
+
+ # إضافة المخاطرة للمشروع
+ if 'risks' not in project:
+ project['risks'] = []
+
+ project['risks'].append(new_risk)
+ st.success("تمت إضافة المخاطرة بنجاح!")
+ st.rerun()
+
+ # تبويب استفسارات المالك
+ with project_tabs[4]:
+ st.markdown("### استفسارات المالك")
+
+ # الحصول على استفسارات المشروع
+ inquiries = project.get('inquiries', [])
+
+ if inquiries:
+ for i, inq in enumerate(inquiries):
+ with st.expander(f"استفسار #{i+1} - {inq.get('date')}"):
+ st.markdown(f"**السؤال:** {inq.get('question')}")
+
+ if inq.get('answer'):
+ st.markdown(f"**الإجابة:** {inq.get('answer')}")
+ st.markdown(f"**تاريخ الإجابة:** {inq.get('answer_date', 'غير محدد')}")
+ else:
+ st.warning("لم تتم الإجابة على هذا الاستفسار بعد.")
+
+ # نموذج للإجابة
+ answer_text = st.text_area("إجابة الاستفسار", key=f"answer_{idx}_{i}")
+
+ if styled_button("إرسال الإجابة", key=f"send_answer_{idx}_{i}", type="primary", icon="✉️"):
+ if not answer_text:
+ st.error("يرجى كتابة الإجابة.")
+ else:
+ # تحديث الاستفسار بالإجابة
+ inq['answer'] = answer_text
+ inq['answer_date'] = datetime.now().strftime("%Y-%m-%d")
+ st.success("تم إرسال الإجابة بنجاح!")
+ st.rerun()
+ else:
+ st.info("لا توجد استفسارات من المالك لهذا المشروع.")
+
+ # إضافة استفسار جديد
+ st.markdown("### إضافة استفسار جديد")
+
+ inquiry_question = st.text_area("سؤال الاستفسار", key=f"new_inquiry_{idx}")
+
+ if styled_button("إضافة استفسار", key=f"add_inquiry_{idx}", type="primary", icon="❓"):
+ if not inquiry_question:
+ st.error("يرجى كتابة السؤال.")
+ else:
+ # إنشاء استفسار جديد
+ new_inquiry = {
+ "question": inquiry_question,
+ "date": datetime.now().strftime("%Y-%m-%d"),
+ "answer": None,
+ "answer_date": None
+ }
+
+ # إضافة الاستفسار للمشروع
+ if 'inquiries' not in project:
+ project['inquiries'] = []
+
+ project['inquiries'].append(new_inquiry)
+ st.success("تمت إضافة الاستفسار بنجاح!")
+ st.rerun()
+
+ # تبويب معلومات الموقع
+ with project_tabs[5]:
+ st.markdown("### معلومات الموقع")
+
+ # عرض معلومات الموقع الحالية
+ site_info = project.get('site_info', {})
+
+ if site_info:
+ st.markdown("#### معلومات أساسية")
+ st.markdown(f"**الطبيعة الجغرافية:** {site_info.get('geography', 'غير محدد')}")
+ st.markdown(f"**إمكانية الوصول:** {site_info.get('accessibility', 'غير محدد')}")
+ st.markdown(f"**المسافة عن أقرب مدينة:** {site_info.get('distance_to_city', 'غير محدد')}")
+
+ st.markdown("#### بيانات التضاريس")
+ st.markdown(f"**نوع التربة:** {site_info.get('soil_type', 'غير محدد')}")
+ st.markdown(f"**متوسط درجة الحرارة:** {site_info.get('avg_temperature', 'غير محدد')}")
+ st.markdown(f"**موسم الأمطار:** {site_info.get('rainy_season', 'غير محدد')}")
+
+ st.markdown("#### الخدمات المتوفرة")
+ st.markdown(f"**مياه:** {'متوفر' if site_info.get('has_water', False) else 'غير متوفر'}")
+ st.markdown(f"**كهرباء:** {'متوفر' if site_info.get('has_electricity', False) else 'غير متوفر'}")
+ st.markdown(f"**اتصالات:** {'متوفر' if site_info.get('has_communications', False) else 'غير متوفر'}")
+
+ st.markdown("#### ملاحظات إضافية")
+ st.markdown(f"{site_info.get('notes', '')}")
+
+ # خريطة الموقع (يمكن إضافتها لاحقاً إذا توفرت الإحداثيات)
+ if 'latitude' in site_info and 'longitude' in site_info:
+ st.markdown("#### موقع المشروع على الخريطة")
+ # يمكن استخدام تقنيات مثل folium أو ربط Google Maps API هنا
+ else:
+ st.info("لم يتم إضافة معلومات الموقع بعد.")
+
+ # تحديث معلومات الموقع
+ st.markdown("### تحديث معلومات الموقع")
+
+ # قسم الطبيعة الجغرافية
+ st.markdown("#### الطبيعة الجغرافية والوصول")
+ geo_col1, geo_col2 = st.columns(2)
+
+ with geo_col1:
+ geography = st.selectbox(
+ "الطبيعة الجغرافية",
+ options=["صحراوية", "جبلية", "ساحلية", "زراعية", "حضرية", "أخرى"],
+ index=0 if not site_info else ["صحراوية", "جبلية", "ساحلية", "زراعية", "حضرية", "أخرى"].index(site_info.get('geography', "صحراوية")),
+ key=f"site_geography_{idx}"
+ )
+
+ accessibility = st.selectbox(
+ "إمكانية الوصول",
+ options=["سهلة", "متوسطة", "صعبة"],
+ index=0 if not site_info else ["سهلة", "متوسطة", "صعبة"].index(site_info.get('accessibility', "سهلة")),
+ key=f"site_accessibility_{idx}"
+ )
+
+ with geo_col2:
+ distance_to_city = st.text_input(
+ "المسافة عن أقرب مدينة (كم)",
+ value=site_info.get('distance_to_city', ""),
+ key=f"site_distance_{idx}"
+ )
+
+ nearest_city = st.text_input(
+ "أقرب مدينة رئيسية",
+ value=site_info.get('nearest_city', ""),
+ key=f"site_nearest_city_{idx}"
+ )
+
+ # قسم التضاريس والمناخ
+ st.markdown("#### التضاريس والمناخ")
+ terrain_col1, terrain_col2 = st.columns(2)
+
+ with terrain_col1:
+ soil_type = st.selectbox(
+ "نوع التربة",
+ options=["رملية", "صخرية", "طينية", "مختلطة", "أخرى"],
+ index=0 if not site_info else ["رملية", "صخرية", "طينية", "مختلطة", "أخرى"].index(site_info.get('soil_type', "رملية")),
+ key=f"site_soil_{idx}"
+ )
+
+ avg_temperature = st.text_input(
+ "متوسط درجة الحرارة",
+ value=site_info.get('avg_temperature', ""),
+ key=f"site_temp_{idx}"
+ )
+
+ with terrain_col2:
+ rainy_season = st.text_input(
+ "موسم الأمطار",
+ value=site_info.get('rainy_season', ""),
+ key=f"site_rainy_{idx}"
+ )
+
+ wind_info = st.text_input(
+ "معلومات الرياح",
+ value=site_info.get('wind_info', ""),
+ key=f"site_wind_{idx}"
+ )
+
+ # قسم الخدمات المتوفرة
+ st.markdown("#### الخدمات المتوفرة")
+ services_col1, services_col2, services_col3 = st.columns(3)
+
+ with services_col1:
+ has_water = st.checkbox(
+ "مياه",
+ value=site_info.get('has_water', False),
+ key=f"site_water_{idx}"
+ )
+
+ with services_col2:
+ has_electricity = st.checkbox(
+ "كهرباء",
+ value=site_info.get('has_electricity', False),
+ key=f"site_electricity_{idx}"
+ )
+
+ with services_col3:
+ has_communications = st.checkbox(
+ "اتصالات",
+ value=site_info.get('has_communications', False),
+ key=f"site_communications_{idx}"
+ )
+
+ # ملاحظات إضافية
+ site_notes = st.text_area(
+ "ملاحظات إضافية عن الموقع",
+ value=site_info.get('notes', ""),
+ key=f"site_notes_{idx}"
+ )
+
+ # حفظ معلومات الموقع
+ if styled_button("حفظ معلومات الموقع", key=f"save_site_info_{idx}", type="primary", icon="💾"):
+ # تجميع معلومات الموقع
+ updated_site_info = {
+ "geography": geography,
+ "accessibility": accessibility,
+ "distance_to_city": distance_to_city,
+ "nearest_city": nearest_city,
+ "soil_type": soil_type,
+ "avg_temperature": avg_temperature,
+ "rainy_season": rainy_season,
+ "wind_info": wind_info,
+ "has_water": has_water,
+ "has_electricity": has_electricity,
+ "has_communications": has_communications,
+ "notes": site_notes
+ }
+
+ # تحديث معلومات الموقع في المشروع
+ project['site_info'] = updated_site_info
+ st.success("تم حفظ معلومات الموقع بنجاح!")
+
+ def _render_new_project_form(self):
+ """عرض نموذج إضافة مشروع جديد"""
+ st.markdown("""
+
+
➕ إضافة مشروع جديد
+
قم بإدخال معلومات المشروع الجديد بالتفصيل.
+
+ """, unsafe_allow_html=True)
+
+ # قسم المعلومات الأساسية
+ st.markdown("### معلومات المشروع الأساسية")
+
+ # عمل تقسيم لحقول المعلومات الأساسية
+ col1, col2 = st.columns(2)
+
+ with col1:
+ project_name = st.text_input("اسم المشروع", key="new_project_name")
+ project_number = st.text_input("رقم المشروع", key="new_project_number")
+ project_client = st.text_input("العميل", key="new_project_client")
+
+ with col2:
+ project_location = st.text_input("موقع المشروع", key="new_project_location")
+ project_type = st.selectbox(
+ "نوع المشروع",
+ options=["بنية تحتية", "مباني", "طرق", "جسور", "شبكات مياه", "شبكات كهرباء", "أخرى"],
+ key="new_project_type"
+ )
+ project_estimated_value = st.number_input(
+ "القيمة التقديرية (ريال)",
+ min_value=0.0,
+ step=100000.0,
+ format="%.2f",
+ key="new_project_value"
+ )
+
+ # قسم التواريخ والجدول الزمني
+ st.markdown("### الجدول الزمني")
+
+ date_col1, date_col2 = st.columns(2)
+
+ with date_col1:
+ project_start_date = st.date_input(
+ "تاريخ البدء",
+ value=datetime.now(),
+ key="new_project_start_date"
+ )
+
+ project_submission_date = st.date_input(
+ "تاريخ التقديم",
+ value=datetime.now() + timedelta(days=30),
+ key="new_project_submission_date"
+ )
+
+ with date_col2:
+ project_duration = st.number_input(
+ "مدة المشروع (يوم)",
+ min_value=1,
+ value=180,
+ step=1,
+ key="new_project_duration"
+ )
+
+ project_status = st.selectbox(
+ "حالة المشروع",
+ options=["جديد", "قيد الدراسة", "تم التقديم", "تم الترسية", "قيد التنفيذ", "مكتمل", "ملغي"],
+ key="new_project_status"
+ )
+
+ # زر إضافة المشروع
+ if styled_button("إضافة المشروع", key="add_new_project", type="success", icon="✅"):
+ # التحقق من وجود المعلومات الأساسية
+ if not project_name or not project_number or not project_client or not project_location:
+ st.error("يرجى تعبئة جميع الحقول الأساسية (اسم المشروع، رقم المشروع، العميل، الموقع).")
+ else:
+ # إنشاء كائن المشروع الجديد
+ new_project = {
+ "name": project_name,
+ "number": project_number,
+ "client": project_client,
+ "location": project_location,
+ "type": project_type,
+ "estimated_value": project_estimated_value,
+ "start_date": project_start_date.strftime("%Y-%m-%d"),
+ "submission_date": project_submission_date.strftime("%Y-%m-%d"),
+ "duration": project_duration,
+ "status": project_status,
+ "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "site_info": {},
+ "manager_insights": [],
+ "site_media": [],
+ "advantages": [],
+ "risks": [],
+ "inquiries": []
+ }
+
+ # إضافة المشروع للقائمة
+ st.session_state.projects.append(new_project)
+
+ # إنشاء مجلد للمشروع
+ project_dir = os.path.join(self.projects_dir, project_number)
+ os.makedirs(project_dir, exist_ok=True)
+
+ # إنشاء المجلدات الفرعية
+ os.makedirs(os.path.join(project_dir, "insights"), exist_ok=True)
+ os.makedirs(os.path.join(project_dir, "images"), exist_ok=True)
+ os.makedirs(os.path.join(project_dir, "videos"), exist_ok=True)
+ os.makedirs(os.path.join(project_dir, "others"), exist_ok=True)
+
+ st.success(f"تمت إضافة المشروع '{project_name}' بنجاح!")
+ st.rerun()
+
+ def _render_archived_projects(self):
+ """عرض قائمة المشاريع المؤرشفة"""
+ st.markdown("""
+
+
📂 أرشيف المشاريع
+
قائمة المشاريع المكتملة أو المؤرشفة.
+
+ """, unsafe_allow_html=True)
+
+ # تصفية المشاريع المؤرشفة
+ archived_projects = [p for p in st.session_state.projects if p.get("status") == "archived" or p.get("status") == "مكتمل"]
+
+ if not archived_projects:
+ st.info("لا توجد مشاريع مؤرشفة حالياً.")
+ return
+
+ # عرض المشاريع المؤرشفة
+ for idx, project in enumerate(archived_projects):
+ with st.expander(f"{project.get('name')} - {project.get('client')}"):
+ self._render_project_details(project, idx + 1000) # استخدام مؤشر مختلف لتجنب التعارض
+
+ def _render_projects_reports(self):
+ """عرض تقارير المشاريع والإحصائيات"""
+ st.markdown("""
+
+
📊 تقارير المشاريع
+
عرض إحصائيات وتقارير عن المشاريع الحالية والسابقة.
+
+ """, unsafe_allow_html=True)
+
+ # إذا لم تكن هناك مشاريع
+ if not st.session_state.projects:
+ st.info("لا توجد مشاريع لعرض التقارير.")
+ return
+
+ # إحصائيات عامة
+ st.markdown("### إحصائيات عامة")
+
+ # حساب الإحصائيات
+ total_projects = len(st.session_state.projects)
+ active_projects = len([p for p in st.session_state.projects if p.get("status") not in ["archived", "مكتمل", "ملغي"]])
+ completed_projects = len([p for p in st.session_state.projects if p.get("status") in ["مكتمل"]])
+ canceled_projects = len([p for p in st.session_state.projects if p.get("status") in ["ملغي"]])
+
+ # عرض الإحصائيات في صفوف
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ st.metric("إجمالي المشاريع", total_projects)
+
+ with col2:
+ st.metric("المشاريع النشطة", active_projects)
+
+ with col3:
+ st.metric("المشاريع المكتملة", completed_projects)
+
+ with col4:
+ st.metric("المشاريع الملغاة", canceled_projects)
+
+ # تحليل المشاريع حسب النوع
+ st.markdown("### توزيع المشاريع حسب النوع")
+
+ # حساب عدد المشاريع حسب النوع
+ project_types = {}
+ for project in st.session_state.projects:
+ project_type = project.get("type", "غير محدد")
+ if project_type in project_types:
+ project_types[project_type] += 1
+ else:
+ project_types[project_type] = 1
+
+ # إنشاء بيانات للرسم البياني
+ types_df = pd.DataFrame({
+ "نوع المشروع": list(project_types.keys()),
+ "عدد المشاريع": list(project_types.values())
+ })
+
+ # رسم بياني دائري لتوزيع المشاريع حسب النوع
+ fig = px.pie(
+ types_df,
+ values="عدد المشاريع",
+ names="نوع المشروع",
+ title="توزيع المشاريع حسب النوع"
+ )
+ st.plotly_chart(fig, use_container_width=True)
+
+ # تحليل المشاريع حسب الحالة
+ st.markdown("### توزيع المشاريع حسب الحالة")
+
+ # حساب عدد المشاريع حسب الحالة
+ project_statuses = {}
+ for project in st.session_state.projects:
+ status = project.get("status", "غير محدد")
+ if status in project_statuses:
+ project_statuses[status] += 1
+ else:
+ project_statuses[status] = 1
+
+ # إنشاء بيانات للرسم البياني
+ statuses_df = pd.DataFrame({
+ "حالة المشروع": list(project_statuses.keys()),
+ "عدد المشاريع": list(project_statuses.values())
+ })
+
+ # رسم بياني شريطي لتوزيع المشاريع حسب الحالة
+ fig2 = px.bar(
+ statuses_df,
+ x="حالة المشروع",
+ y="عدد المشاريع",
+ title="توزيع المشاريع حسب الحالة",
+ color="حالة المشروع"
+ )
+ st.plotly_chart(fig2, use_container_width=True)
+
+ # عرض مخاطر المشاريع
+ st.markdown("### أهم المخاطر في المشاريع الحالية")
+
+ # تجميع المخاطر من جميع المشاريع النشطة
+ all_risks = []
+ for project in st.session_state.projects:
+ if project.get("status") not in ["archived", "مكتمل", "ملغي"]:
+ for risk in project.get("risks", []):
+ all_risks.append({
+ "مشروع": project.get("name"),
+ "مخاطرة": risk.get("title"),
+ "الحدة": risk.get("severity"),
+ "الاحتمالية": risk.get("probability", 0)
+ })
+
+ if all_risks:
+ # تحويل المخاطر إلى DataFrame
+ risks_df = pd.DataFrame(all_risks)
+
+ # ترتيب المخاطر حسب الحدة والاحتمالية
+ risks_df = risks_df.sort_values(by=["الحدة", "الاحتمالية"], ascending=[False, False])
+
+ # تلوين الجدول حسب الحدة
+ def color_severity(val):
+ if val == "عالي":
+ return 'background-color: #FFCCCC'
+ elif val == "متوسط":
+ return 'background-color: #FFFFCC'
+ else:
+ return 'background-color: #CCFFCC'
+
+ # عرض جدول المخاطر
+ st.dataframe(risks_df.style.applymap(color_severity, subset=["الحدة"]), use_container_width=True)
+ else:
+ st.info("لا توجد مخاطر مسجلة في المشاريع النشطة.")
+
+ def _save_project_file(self, project_number, file_type, uploaded_file):
+ """حفظ ملف مرفق للمشروع وإرجاع المسار"""
+ try:
+ # التأكد من وجود المجلد
+ project_dir = os.path.join(self.projects_dir, project_number)
+ type_dir = os.path.join(project_dir, file_type)
+
+ os.makedirs(type_dir, exist_ok=True)
+
+ # إنشاء اسم الملف
+ file_name = f"{int(time.time())}_{uploaded_file.name}"
+ file_path = os.path.join(type_dir, file_name)
+
+ # حفظ الملف
+ with open(file_path, "wb") as f:
+ f.write(uploaded_file.getbuffer())
+
+ return file_path
+ except Exception as e:
+ st.error(f"خطأ في حفظ الملف: {str(e)}")
+ return None
+
+
+# تشغيل النموذج مباشرة عند استدعاء الملف
+def main():
+ """تشغيل نموذج إدارة المشاريع بشكل مستقل"""
+ # تهيئة الواجهة
+ st.set_page_config(
+ page_title="إدارة المشاريع | WAHBi AI",
+ page_icon="🏗️",
+ layout="wide",
+ initial_sidebar_state="expanded",
+ menu_items={
+ 'Get Help': 'mailto:support@wahbi-ai.com',
+ 'Report a bug': 'mailto:support@wahbi-ai.com',
+ 'About': 'نظام إدارة المشاريع - جزء من نظام WAHBi AI لتحليل المناقصات'
+ }
+ )
+
+ # تهيئة نموذج إدارة المشاريع
+ projects_management = ProjectsManagement()
+
+ # عرض واجهة إدارة المشاريع
+ projects_management.render()
+
+# تشغيل النموذج إذا تم استدعاء الملف مباشرة
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/modules/reports/reports_app.py b/modules/reports/reports_app.py
new file mode 100644
index 0000000000000000000000000000000000000000..55485214f6d4779aeec220e21326e8f27eccee53
--- /dev/null
+++ b/modules/reports/reports_app.py
@@ -0,0 +1,88 @@
+import streamlit as st
+import pandas as pd
+import plotly.express as px
+from datetime import datetime, timedelta
+import time
+
+class ReportsApp:
+ """وحدة التقارير والتحليلات"""
+
+ def __init__(self):
+ pass
+
+ def render(self):
+ st.markdown("
", unsafe_allow_html=True)
tabs = st.tabs([
- "لوحة المعلومات",
- "الموارد البشرية",
- "المعدات",
+ "لوحة المعلومات",
"المواد",
- "تخصيص الموارد",
- "تخطيط الموارد"
+ "العمالة",
+ "المعدات",
+ "المقاولين من الباطن",
+ "تحليل الأسعار"
])
with tabs[0]:
self._render_dashboard_tab()
with tabs[1]:
- self._render_human_resources_tab()
+ self._render_materials_tab()
with tabs[2]:
- self._render_equipment_tab()
+ self._render_labor_tab()
with tabs[3]:
- self._render_materials_tab()
+ self._render_equipment_tab()
with tabs[4]:
- self._render_resource_allocation_tab()
+ self._render_subcontractors_tab()
with tabs[5]:
- self._render_resource_planning_tab()
+ self._render_price_analysis_tab()
def _render_dashboard_tab(self):
"""عرض تبويب لوحة المعلومات"""
- st.markdown("### لوحة معلومات الموارد")
-
- # استخراج البيانات
- employees_df = st.session_state.resources_data["employees"]
- equipment_df = st.session_state.resources_data["equipment"]
- materials_df = st.session_state.resources_data["materials"]
- projects_df = st.session_state.resources_data["projects"]
- allocations_df = st.session_state.resources_data["allocations"]
+ st.markdown("### لوحة معلومات إدارة الموارد")
# عرض مؤشرات الأداء الرئيسية
- st.markdown("#### مؤشرات الأداء الرئيسية")
-
col1, col2, col3, col4 = st.columns(4)
with col1:
- total_employees = len(employees_df)
- available_employees = len(employees_df[employees_df["متاح"] == True])
- st.metric("الموظفون", f"{available_employees}/{total_employees}")
+ total_materials = len(st.session_state.materials)
+ st.metric("عدد المواد", total_materials)
with col2:
- total_equipment = len(equipment_df)
- available_equipment = len(equipment_df[equipment_df["متاحة"] == True])
- st.metric("المعدات", f"{available_equipment}/{total_equipment}")
+ total_labor = len(st.session_state.labor)
+ st.metric("عدد موارد العمالة", total_labor)
with col3:
- total_materials = len(materials_df)
- low_stock_materials = len(materials_df[materials_df["الكمية المتاحة"] < 50])
- st.metric("المواد", f"{total_materials}", f"-{low_stock_materials} منخفضة المخزون")
+ total_equipment = len(st.session_state.equipment)
+ st.metric("عدد المعدات", total_equipment)
with col4:
- total_projects = len(projects_df)
- active_projects = len(projects_df[projects_df["الحالة"] == "قيد التنفيذ"])
- st.metric("المشاريع النشطة", f"{active_projects}/{total_projects}")
-
- # عرض توزيع الموارد البشرية حسب القسم
- st.markdown("#### توزيع الموارد البشرية حسب القسم")
-
- dept_counts = employees_df["القسم"].value_counts().reset_index()
- dept_counts.columns = ["القسم", "العدد"]
-
- fig = px.pie(
- dept_counts,
- values="العدد",
- names="القسم",
- title="توزيع الموظفين حسب القسم",
- color="القسم"
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توزيع المعدات حسب النوع
- st.markdown("#### توزيع المعدات حسب النوع")
-
- type_counts = equipment_df["النوع"].value_counts().reset_index()
- type_counts.columns = ["النوع", "العدد"]
-
- fig = px.bar(
- type_counts,
- x="النوع",
- y="العدد",
- title="توزيع المعدات حسب النوع",
- color="النوع",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توزيع المواد حسب المورد
- st.markdown("#### توزيع المواد حسب المورد")
+ total_subcontractors = len(st.session_state.subcontractors)
+ st.metric("عدد المقاولين من الباطن", total_subcontractors)
- supplier_counts = materials_df["المورد"].value_counts().reset_index()
- supplier_counts.columns = ["المورد", "العدد"]
+ # رسم بياني لتوزيع المحتوى المحلي
+ st.markdown("### المحتوى المحلي للموارد")
- fig = px.pie(
- supplier_counts,
- values="العدد",
- names="المورد",
- title="توزيع المواد حسب المورد",
- color="المورد"
- )
+ # إعداد البيانات
+ local_content_data = []
- st.plotly_chart(fig, use_container_width=True)
+ # إضافة بيانات المواد
+ for material in st.session_state.materials:
+ local_content_data.append({
+ 'النوع': 'المواد',
+ 'اسم المورد': material['name'],
+ 'نسبة المحتوى المحلي': material['local_content']
+ })
- # عرض توزيع تكاليف الموارد
- st.markdown("#### توزيع تكاليف الموارد")
+ # إضافة بيانات العمالة
+ for labor in st.session_state.labor:
+ local_content_data.append({
+ 'النوع': 'العمالة',
+ 'اسم المورد': labor['name'],
+ 'نسبة المحتوى المحلي': labor['local_content']
+ })
- # حساب إجمالي تكاليف الموظفين
- total_employee_cost = employees_df["التكلفة الشهرية"].sum()
+ # إضافة بيانات المعدات
+ for equipment in st.session_state.equipment:
+ local_content_data.append({
+ 'النوع': 'المعدات',
+ 'اسم المورد': equipment['name'],
+ 'نسبة المحتوى المحلي': equipment['local_content']
+ })
- # حساب إجمالي تكاليف المعدات (افتراضياً لشهر واحد)
- total_equipment_cost = equipment_df["التكلفة اليومية"].sum() * 30
+ # إضافة بيانات المقاولين من الباطن
+ for subcontractor in st.session_state.subcontractors:
+ local_content_data.append({
+ 'النوع': 'المقاولين من الباطن',
+ 'اسم المورد': subcontractor['name'],
+ 'نسبة المحتوى المحلي': subcontractor['local_content']
+ })
- # حساب إجمالي تكاليف المواد
- total_material_cost = (materials_df["الكمية المتاحة"] * materials_df["تكلفة الوحدة"]).sum()
+ # تحويل البيانات إلى DataFrame
+ local_content_df = pd.DataFrame(local_content_data)
- # إنشاء DataFrame لتوزيع التكاليف
- cost_distribution = pd.DataFrame({
- "نوع المورد": ["الموظفون", "المعدات", "المواد"],
- "التكلفة": [total_employee_cost, total_equipment_cost, total_material_cost]
- })
+ # حساب متوسط المحتوى المحلي لكل نوع
+ avg_local_content = local_content_df.groupby('النوع')['نسبة المحتوى المحلي'].mean().reset_index()
- fig = px.pie(
- cost_distribution,
- values="التكلفة",
- names="نوع المورد",
- title="توزيع تكاليف الموارد",
- color="نوع المورد",
- color_discrete_map={
- "الموظفون": "#3498db",
- "المعدات": "#2ecc71",
- "المواد": "#f39c12"
- }
+ # رسم المخطط الشريطي
+ fig = px.bar(
+ avg_local_content,
+ x='النوع',
+ y='نسبة المحتوى المحلي',
+ title='متوسط نسبة المحتوى المحلي حسب نوع المورد',
+ color='النوع',
+ text_auto='.1f'
)
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض تخصيص الموارد للمشاريع
- st.markdown("#### تخصيص الموارد للمشاريع")
-
- # حساب عدد الموارد المخصصة لكل مشروع
- project_allocations = allocations_df["رقم المشروع"].value_counts().reset_index()
- project_allocations.columns = ["رقم المشروع", "عدد الموارد المخصصة"]
+ fig.update_traces(texttemplate='%{text}%', textposition='outside')
- # دمج بيانات المشاريع مع بيانات التخصيص
- project_allocations = project_allocations.merge(
- projects_df[["رقم المشروع", "اسم المشروع", "الحالة"]],
- on="رقم المشروع",
- how="left"
+ fig.add_shape(
+ type="line",
+ x0=-0.5,
+ x1=len(avg_local_content) - 0.5,
+ y0=70, # النسبة المستهدفة
+ y1=70,
+ line=dict(color="red", width=2, dash="dash"),
+ name="النسبة المستهدفة"
)
- fig = px.bar(
- project_allocations,
- x="اسم المشروع",
- y="عدد الموارد المخصصة",
- title="تخصيص الموارد للمشاريع",
- color="الحالة",
- text_auto=True,
- color_discrete_map={
- "قيد التنفيذ": "#3498db",
- "مكتمل": "#2ecc71",
- "متوقف": "#e74c3c",
- "مخطط": "#f39c12"
- }
+ fig.add_annotation(
+ x=1,
+ y=75,
+ text=f"النسبة المستهدفة (70%)",
+ showarrow=False,
+ font=dict(color="red")
)
st.plotly_chart(fig, use_container_width=True)
- # عرض توزيع أنواع الموارد المخصصة
- st.markdown("#### توزيع أنواع الموارد المخصصة")
-
- resource_type_counts = allocations_df["نوع المورد"].value_counts().reset_index()
- resource_type_counts.columns = ["نوع المورد", "العدد"]
-
- fig = px.pie(
- resource_type_counts,
- values="العدد",
- names="نوع المورد",
- title="توزيع أنواع الموارد المخصصة",
- color="نوع المورد",
- color_discrete_map={
- "موظف": "#3498db",
- "معدة": "#2ecc71",
- "مادة": "#f39c12"
+ # عرض تنبيهات الموارد
+ st.markdown("### تنبيهات الموارد")
+
+ # محاكاة تنبيهات الموارد
+ alerts = [
+ {
+ "type": "تغير في الأسعار",
+ "resource": "حديد تسليح",
+ "message": "ارتفاع في سعر الحديد بنسبة 5% في الأسبوع الماضي",
+ "date": "2024-03-15",
+ "severity": "متوسطة"
+ },
+ {
+ "type": "نقص في المخزون",
+ "resource": "بلاط سيراميك",
+ "message": "انخفاض مخزون السيراميك إلى أقل من 20% من المستوى المطلوب",
+ "date": "2024-03-18",
+ "severity": "عالية"
+ },
+ {
+ "type": "انتهاء صلاحية عقود",
+ "resource": "مؤسسة الإنشاءات المتكاملة",
+ "message": "سينتهي العقد مع المقاول خلال 30 يوماً",
+ "date": "2024-03-10",
+ "severity": "منخفضة"
+ },
+ {
+ "type": "تغير في المحتوى المحلي",
+ "resource": "شركة التكييف والتبريد",
+ "message": "انخفاض نسبة المحتوى المحلي إلى أقل من النسبة المستهدفة",
+ "date": "2024-03-12",
+ "severity": "متوسطة"
}
- )
+ ]
- st.plotly_chart(fig, use_container_width=True)
+ # عرض التنبيهات
+ for alert in alerts:
+ if alert["severity"] == "عالية":
+ st.error(f"**{alert['type']}**: {alert['message']} - *{alert['resource']}* ({alert['date']})")
+ elif alert["severity"] == "متوسطة":
+ st.warning(f"**{alert['type']}**: {alert['message']} - *{alert['resource']}* ({alert['date']})")
+ else:
+ st.info(f"**{alert['type']}**: {alert['message']} - *{alert['resource']}* ({alert['date']})")
+
+ # عرض نظرة عامة على الأسعار
+ st.markdown("### نظرة عامة على تطور الأسعار")
+
+ # إعداد البيانات
+ price_history_data = []
+ material_names = {material['id']: material['name'] for material in st.session_state.materials}
+
+ for entry in st.session_state.price_history:
+ material_id = entry['material_id']
+ if material_id in material_names:
+ price_history_data.append({
+ 'المادة': material_names[material_id],
+ 'التاريخ': pd.to_datetime(entry['date']),
+ 'السعر': entry['price']
+ })
+
+ # تحويل البيانات إلى DataFrame
+ price_history_df = pd.DataFrame(price_history_data)
+
+ # التحقق من وجود بيانات قبل رسم المخطط
+ if len(price_history_data) == 0:
+ st.warning("لا توجد بيانات تاريخية للأسعار متاحة لعرضها")
+ else:
+ # رسم المخطط الخطي
+ fig = px.line(
+ price_history_df,
+ x='التاريخ',
+ y='السعر',
+ color='المادة',
+ title='تطور أسعار المواد الرئيسية خلال العام الماضي',
+ labels={'التاريخ': 'التاريخ', 'السعر': 'السعر (ريال)', 'المادة': 'المادة'}
+ )
+ # عرض المخطط فقط إذا تم إنشاؤه
+ st.plotly_chart(fig, use_container_width=True)
- def _render_human_resources_tab(self):
- """عرض تبويب الموارد البشرية"""
-
- st.markdown("### إدارة الموارد البشرية")
-
- # استخراج البيانات
- employees_df = st.session_state.resources_data["employees"]
+ def _render_materials_tab(self):
+ """عرض تبويب المواد"""
- # عرض خيارات التصفية
- st.markdown("#### خيارات التصفية")
+ st.markdown("### إدارة المواد")
- col1, col2, col3 = st.columns(3)
+ # عرض أدوات البحث والتصفية
+ col1, col2 = st.columns(2)
with col1:
- selected_departments = st.multiselect(
- "القسم",
- options=employees_df["القسم"].unique(),
- default=employees_df["القسم"].unique()
- )
+ search_query = st.text_input("بحث في المواد", placeholder="ابحث باسم المادة أو الفئة أو المورد...")
with col2:
- selected_positions = st.multiselect(
- "المنصب",
- options=employees_df["المنصب"].unique(),
- default=employees_df["المنصب"].unique()
+ category_filter = st.multiselect(
+ "تصفية حسب الفئة",
+ options=list(set(material['category'] for material in st.session_state.materials)),
+ default=[],
+ key="material_category_filter_tab"
)
- with col3:
- availability_filter = st.selectbox(
- "الإتاحة",
- options=["الكل", "متاح فقط", "غير متاح فقط"]
- )
-
- # تطبيق التصفية
- filtered_df = employees_df[
- employees_df["القسم"].isin(selected_departments) &
- employees_df["المنصب"].isin(selected_positions)
- ]
-
- if availability_filter == "متاح فقط":
- filtered_df = filtered_df[filtered_df["متاح"] == True]
- elif availability_filter == "غير متاح فقط":
- filtered_df = filtered_df[filtered_df["متاح"] == False]
-
- # عرض البيانات المصفاة
- st.markdown("#### قائمة الموظفين")
-
- st.dataframe(
- filtered_df,
- column_config={
- "رقم الموظف": st.column_config.TextColumn("رقم الموظف"),
- "اسم الموظف": st.column_config.TextColumn("اسم الموظف"),
- "القسم": st.column_config.TextColumn("القسم"),
- "المنصب": st.column_config.TextColumn("المنصب"),
- "المهارات": st.column_config.ListColumn("المهارات"),
- "سنوات الخبرة": st.column_config.NumberColumn("سنوات الخبرة"),
- "التكلفة الشهرية": st.column_config.NumberColumn("التكلفة الشهرية", format="%.2f ريال"),
- "متاح": st.column_config.CheckboxColumn("متاح"),
- "التقييم": st.column_config.ProgressColumn("التقييم", min_value=0, max_value=5)
- },
- use_container_width=True,
- hide_index=True
- )
-
- # عرض إحصائيات الموارد البشرية
- st.markdown("#### إحصائيات الموارد البشرية")
-
- col1, col2, col3, col4 = st.columns(4)
-
- with col1:
- total_employees = len(filtered_df)
- st.metric("إجمالي الموظفين", f"{total_employees}")
-
- with col2:
- available_employees = len(filtered_df[filtered_df["متاح"] == True])
- availability_rate = available_employees / total_employees * 100 if total_employees > 0 else 0
- st.metric("معدل الإتاحة", f"{availability_rate:.1f}%")
-
- with col3:
- avg_experience = filtered_df["سنوات الخبرة"].mean()
- st.metric("متوسط سنوات الخبرة", f"{avg_experience:.1f} سنة")
+ # تطبيق البحث والتصفية
+ filtered_materials = st.session_state.materials
- with col4:
- avg_cost = filtered_df["التكلفة الشهرية"].mean()
- st.metric("متوسط التكلفة الشهرية", f"{avg_cost:.0f} ريال")
+ if search_query:
+ filtered_materials = [
+ material for material in filtered_materials
+ if (search_query.lower() in material['name'].lower() or
+ search_query.lower() in material['category'].lower() or
+ search_query.lower() in material['supplier'].lower())
+ ]
- # عرض توزيع الموظفين حسب القسم
- st.markdown("#### توزيع الموظفين حسب القسم")
+ if category_filter:
+ filtered_materials = [material for material in filtered_materials if material['category'] in category_filter]
- dept_counts = filtered_df["القسم"].value_counts().reset_index()
- dept_counts.columns = ["القسم", "العدد"]
+ # زر إضافة مادة جديدة
+ if st.button("إضافة مادة جديدة"):
+ st.session_state.show_material_form = True
- fig = px.bar(
- dept_counts,
- x="القسم",
- y="العدد",
- title="توزيع الموظفين حسب القسم",
- color="القسم",
- text_auto=True
- )
+ # نموذج إضافة مادة جديدة
+ if st.session_state.get('show_material_form', False):
+ with st.form("add_material_form"):
+ st.markdown("#### إضافة مادة جديدة")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ new_material_name = st.text_input("اسم المادة", key="new_material_name")
+ new_material_category = st.text_input("الفئة", key="new_material_category")
+ new_material_unit = st.text_input("وحدة القياس", key="new_material_unit")
+
+ with col2:
+ new_material_price = st.number_input("السعر (ريال)", min_value=0.0, key="new_material_price")
+ new_material_supplier = st.text_input("المورد", key="new_material_supplier")
+ new_material_local_content = st.slider("نسبة المحتوى المحلي (%)", 0, 100, 50, key="new_material_local_content")
+
+ submitted = st.form_submit_button("إضافة المادة")
+ cancel = st.form_submit_button("إلغاء")
+
+ if submitted and new_material_name and new_material_category and new_material_unit:
+ # إضافة المادة الجديدة
+ new_material = {
+ 'id': max([material['id'] for material in st.session_state.materials], default=0) + 1,
+ 'name': new_material_name,
+ 'category': new_material_category,
+ 'unit': new_material_unit,
+ 'price': new_material_price,
+ 'supplier': new_material_supplier,
+ 'local_content': new_material_local_content,
+ 'last_updated': datetime.now().strftime('%Y-%m-%d')
+ }
+
+ st.session_state.materials.append(new_material)
+ st.success(f"تمت إضافة المادة '{new_material_name}' بنجاح!")
+ st.session_state.show_material_form = False
+ st.rerun()
+
+ if cancel:
+ st.session_state.show_material_form = False
+ st.rerun()
- st.plotly_chart(fig, use_container_width=True)
+ # عرض قائمة المواد
+ if filtered_materials:
+ # تحويل البيانات إلى DataFrame
+ materials_df = pd.DataFrame(filtered_materials)
+
+ # تنسيق البيانات للعرض
+ display_df = materials_df.copy()
+ display_df['price'] = display_df['price'].apply(lambda x: f"{x:,.2f} ريال")
+ display_df['local_content'] = display_df['local_content'].apply(lambda x: f"{x}%")
+
+ # تغيير أسماء الأعمدة للعرض
+ display_df.columns = [
+ 'معرف', 'اسم المادة', 'الفئة', 'وحدة القياس', 'السعر', 'المورد', 'نسبة المحتوى المحلي', 'آخر تحديث'
+ ]
+
+ # عرض الجدول
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
+
+ # عرض ملخص إحصائي
+ st.markdown("#### ملخص إحصائي للمواد")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric("إجمالي عدد المواد", len(filtered_materials))
+
+ with col2:
+ avg_price = sum(material['price'] for material in filtered_materials) / len(filtered_materials)
+ st.metric("متوسط سعر المواد", f"{avg_price:,.2f} ريال")
+
+ with col3:
+ avg_local_content = sum(material['local_content'] for material in filtered_materials) / len(filtered_materials)
+ st.metric("متوسط نسبة المحتوى المحلي", f"{avg_local_content:.1f}%")
+
+ # عرض مخطط توزيع المواد حسب الفئة
+ category_counts = materials_df.groupby('category').size().reset_index(name='count')
+
+ fig = px.pie(
+ category_counts,
+ names='category',
+ values='count',
+ title='توزيع المواد حسب الفئة'
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+ else:
+ st.warning("لا توجد مواد مطابقة لمعايير البحث.")
+
+ def _render_labor_tab(self):
+ """عرض تبويب العمالة"""
- # عرض توزيع الموظفين حسب المنصب
- st.markdown("#### توزيع الموظفين حسب المنصب")
+ st.markdown("### إدارة العمالة")
- position_counts = filtered_df["المنصب"].value_counts().reset_index()
- position_counts.columns = ["المنصب", "العدد"]
+ # عرض أدوات البحث والتصفية
+ col1, col2 = st.columns(2)
- fig = px.pie(
- position_counts,
- values="العدد",
- names="المنصب",
- title="توزيع الموظفين حسب المنصب",
- color="المنصب"
- )
+ with col1:
+ search_query = st.text_input("بحث في العمالة", placeholder="ابحث باسم العامل أو الفئة أو المورد...")
- st.plotly_chart(fig, use_container_width=True)
+ with col2:
+ category_filter = st.multiselect(
+ "تصفية حسب الفئة",
+ options=list(set(labor['category'] for labor in st.session_state.labor)),
+ default=[],
+ key="labor_category_filter_tab"
+ )
- # عرض توزيع الموظفين حسب سنوات الخبرة
- st.markdown("#### توزيع الموظفين حسب سنوات الخبرة")
+ # تطبيق البحث والتصفية
+ filtered_labor = st.session_state.labor
- # إنشاء فئات لسنوات الخبرة
- experience_bins = [0, 3, 5, 10, 15, 20]
- experience_labels = ["أقل من 3 سنوات", "3-5 سنوات", "6-10 سنوات", "11-15 سنة", "أكثر من 15 سنة"]
+ if search_query:
+ filtered_labor = [
+ labor for labor in filtered_labor
+ if (search_query.lower() in labor['name'].lower() or
+ search_query.lower() in labor['category'].lower() or
+ search_query.lower() in labor['supplier'].lower())
+ ]
- filtered_df["فئة الخبرة"] = pd.cut(filtered_df["سنوات الخبرة"], bins=experience_bins, labels=experience_labels, right=False)
+ if category_filter:
+ filtered_labor = [labor for labor in filtered_labor if labor['category'] in category_filter]
- experience_counts = filtered_df["فئة الخبرة"].value_counts().reset_index()
- experience_counts.columns = ["فئة الخبرة", "العدد"]
+ # زر إضافة عامل جديد
+ if st.button("إضافة عامل جديد"):
+ st.session_state.show_labor_form = True
- fig = px.bar(
- experience_counts,
- x="فئة الخبرة",
- y="العدد",
- title="توزيع الموظفين حسب سنوات الخبرة",
- color="فئة الخبرة",
- text_auto=True
- )
+ # نموذج إضافة عامل جديد
+ if st.session_state.get('show_labor_form', False):
+ with st.form("add_labor_form"):
+ st.markdown("#### إضافة عامل جديد")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ new_labor_name = st.text_input("اسم العامل", key="new_labor_name")
+ new_labor_category = st.text_input("الفئة", key="new_labor_category")
+ new_labor_unit = st.text_input("وحدة القياس", key="new_labor_unit")
+
+ with col2:
+ new_labor_price = st.number_input("السعر (ريال)", min_value=0.0, key="new_labor_price")
+ new_labor_supplier = st.text_input("المورد", key="new_labor_supplier")
+ new_labor_local_content = st.slider("نسبة المحتوى المحلي (%)", 0, 100, 50, key="new_labor_local_content")
+
+ submitted = st.form_submit_button("إضافة العامل")
+ cancel = st.form_submit_button("إلغاء")
+
+ if submitted and new_labor_name and new_labor_category and new_labor_unit:
+ # إضافة العامل الجديد
+ new_labor = {
+ 'id': max([labor['id'] for labor in st.session_state.labor], default=0) + 1,
+ 'name': new_labor_name,
+ 'category': new_labor_category,
+ 'unit': new_labor_unit,
+ 'price': new_labor_price,
+ 'supplier': new_labor_supplier,
+ 'local_content': new_labor_local_content,
+ 'last_updated': datetime.now().strftime('%Y-%m-%d')
+ }
+
+ st.session_state.labor.append(new_labor)
+ st.success(f"تمت إضافة العامل '{new_labor_name}' بنجاح!")
+ st.session_state.show_labor_form = False
+ st.rerun()
+
+ if cancel:
+ st.session_state.show_labor_form = False
+ st.rerun()
- st.plotly_chart(fig, use_container_width=True)
+ # عرض قائمة العمالة
+ if filtered_labor:
+ # تحويل البيانات إلى DataFrame
+ labor_df = pd.DataFrame(filtered_labor)
+
+ # تنسيق البيانات للعرض
+ display_df = labor_df.copy()
+ display_df['price'] = display_df['price'].apply(lambda x: f"{x:,.2f} ريال")
+ display_df['local_content'] = display_df['local_content'].apply(lambda x: f"{x}%")
+
+ # تغيير أسماء الأعمدة للعرض
+ display_df.columns = [
+ 'معرف', 'اسم العامل', 'الفئة', 'وحدة القياس', 'السعر', 'المورد', 'نسبة المحتوى المحلي', 'آخر تحديث'
+ ]
+
+ # عرض الجدول
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
+
+ # عرض ملخص إحصائي
+ st.markdown("#### ملخص إحصائي للعمالة")
+
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.metric("إجمالي عدد العمالة", len(filtered_labor))
+
+ with col2:
+ avg_price = sum(labor['price'] for labor in filtered_labor) / len(filtered_labor)
+ st.metric("متوسط سعر العمالة", f"{avg_price:,.2f} ريال")
+
+ with col3:
+ avg_local_content = sum(labor['local_content'] for labor in filtered_labor) / len(filtered_labor)
+ st.metric("متوسط نسبة المحتوى المحلي", f"{avg_local_content:.1f}%")
+
+ # عرض مخطط توزيع العمالة حسب الفئة
+ category_counts = labor_df.groupby('category').size().reset_index(name='count')
+
+ fig = px.pie(
+ category_counts,
+ names='category',
+ values='count',
+ title='توزيع العمالة حسب الفئة'
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+ else:
+ st.warning("لا توجد عمالة مطابقة لمعايير البحث.")
+
+ def _render_equipment_tab(self):
+ """عرض تبويب المعدات"""
- # عرض توزيع المهارات
- st.markdown("#### توزيع المهارات")
+ st.markdown("### إدارة المعدات")
- # استخراج جميع المهارات
- all_skills = []
- for skills_list in filtered_df["المهارات"]:
- all_skills.extend(skills_list)
+ # عرض أدوات البحث والتصفية
+ col1, col2 = st.columns(2)
- skill_counts = pd.Series(all_skills).value_counts().reset_index()
- skill_counts.columns = ["المهارة", "العدد"]
+ with col1:
+ search_query = st.text_input("بحث في المعدات", placeholder="ابحث باسم المعدة أو الفئة أو المورد...")
- fig = px.bar(
- skill_counts,
- x="المهارة",
- y="العدد",
- title="توزيع المهارات",
- color="المهارة",
- text_auto=True
- )
+ with col2:
+ category_filter = st.multiselect(
+ "تصفية حسب الفئة",
+ options=list(set(equipment['category'] for equipment in st.session_state.equipment)),
+ default=[],
+ key="equipment_category_filter_tab"
+ )
- st.plotly_chart(fig, use_container_width=True)
+ # تطبيق البحث والتصفية
+ filtered_equipment = st.session_state.equipment
- # عرض العلاقة بين سنوات الخبرة والتكلفة
- st.markdown("#### العلاقة بين سنوات الخبرة والتكلفة")
+ if search_query:
+ filtered_equipment = [
+ equipment for equipment in filtered_equipment
+ if (search_query.lower() in equipment['name'].lower() or
+ search_query.lower() in equipment['category'].lower() or
+ search_query.lower() in equipment['supplier'].lower())
+ ]
- fig = px.scatter(
- filtered_df,
- x="سنوات الخبرة",
- y="التكلفة الشهرية",
- color="القسم",
- size="التقييم",
- hover_name="اسم الموظف",
- hover_data=["المنصب", "متاح"],
- title="العلاقة بين سنوات الخبرة والتكلفة الشهرية"
- )
+ if category_filter:
+ filtered_equipment = [equipment for equipment in filtered_equipment if equipment['category'] in category_filter]
- st.plotly_chart(fig, use_container_width=True)
+ # زر إضافة معدة جديدة
+ if st.button("إضافة معدة جديدة"):
+ st.session_state.show_equipment_form = True
- # إضافة موظف جديد
- st.markdown("#### إضافة موظف جديد")
+ # نموذج إضافة معدة جديدة
+ if st.session_state.get('show_equipment_form', False):
+ with st.form("add_equipment_form"):
+ st.markdown("#### إضافة معدة جديدة")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ new_equipment_name = st.text_input("اسم المعدة", key="new_equipment_name")
+ new_equipment_category = st.text_input("الفئة", key="new_equipment_category")
+ new_equipment_unit = st.text_input("وحدة القياس", key="new_equipment_unit")
+
+ with col2:
+ new_equipment_price = st.number_input("السعر (ريال)", min_value=0.0, key="new_equipment_price")
+ new_equipment_supplier = st.text_input("المورد", key="new_equipment_supplier")
+ new_equipment_local_content = st.slider("نسبة المحتوى المحلي (%)", 0, 100, 50, key="new_equipment_local_content")
+
+ submitted = st.form_submit_button("إضافة المعدة")
+ cancel = st.form_submit_button("إلغاء")
+
+ if submitted and new_equipment_name and new_equipment_category and new_equipment_unit:
+ # إضافة المعدة الجديدة
+ new_equipment = {
+ 'id': max([equipment['id'] for equipment in st.session_state.equipment], default=0) + 1,
+ 'name': new_equipment_name,
+ 'category': new_equipment_category,
+ 'unit': new_equipment_unit,
+ 'price': new_equipment_price,
+ 'supplier': new_equipment_supplier,
+ 'local_content': new_equipment_local_content,
+ 'last_updated': datetime.now().strftime('%Y-%m-%d')
+ }
+
+ st.session_state.equipment.append(new_equipment)
+ st.success(f"تمت إضافة المعدة '{new_equipment_name}' بنجاح!")
+ st.session_state.show_equipment_form = False
+ st.rerun()
+
+ if cancel:
+ st.session_state.show_equipment_form = False
+ st.rerun()
- with st.form("add_employee_form"):
- col1, col2 = st.columns(2)
+ # عرض قائمة المعدات
+ if filtered_equipment:
+ # تحويل البيانات إلى DataFrame
+ equipment_df = pd.DataFrame(filtered_equipment)
+
+ # تنسيق البيانات للعرض
+ display_df = equipment_df.copy()
+ display_df['price'] = display_df['price'].apply(lambda x: f"{x:,.2f} ريال")
+ display_df['local_content'] = display_df['local_content'].apply(lambda x: f"{x}%")
+
+ # تغيير أسماء الأعمدة للعرض
+ display_df.columns = [
+ 'معرف', 'اسم المعدة', 'الفئة', 'وحدة القياس', 'السعر', 'المورد', 'نسبة المحتوى المحلي', 'آخر تحديث'
+ ]
+
+ # عرض الجدول
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
+
+ # عرض ملخص إحصائي
+ st.markdown("#### ملخص إحصائي للمعدات")
+
+ col1, col2, col3 = st.columns(3)
with col1:
- new_employee_name = st.text_input("اسم الموظف")
- new_employee_department = st.selectbox("القسم", options=employees_df["القسم"].unique())
- new_employee_position = st.selectbox("المنصب", options=employees_df["المنصب"].unique())
- new_employee_experience = st.number_input("سنوات الخبرة", min_value=0, max_value=40, value=5)
+ st.metric("إجمالي عدد المعدات", len(filtered_equipment))
with col2:
- new_employee_skills = st.multiselect(
- "المهارات",
- options=["إدارة المشاريع", "التصميم الهندسي", "تحليل البيانات", "إدارة العقود", "التخطيط الاستراتيجي", "إدارة الموارد", "إدارة المخاطر", "إدارة الجودة", "إدارة التكاليف", "إدارة الوقت"]
- )
- new_employee_cost = st.number_input("التكلفة الشهرية", min_value=3000, max_value=50000, value=10000)
- new_employee_available = st.checkbox("متاح", value=True)
- new_employee_rating = st.slider("التقييم", min_value=1.0, max_value=5.0, value=4.0, step=0.1)
+ avg_price = sum(equipment['price'] for equipment in filtered_equipment) / len(filtered_equipment)
+ st.metric("متوسط سعر المعدات", f"{avg_price:,.2f} ريال")
+
+ with col3:
+ avg_local_content = sum(equipment['local_content'] for equipment in filtered_equipment) / len(filtered_equipment)
+ st.metric("متوسط نسبة المحتوى المحلي", f"{avg_local_content:.1f}%")
- submit_button = st.form_submit_button("إضافة موظف")
+ # عرض مخطط توزيع المعدات حسب الفئة
+ category_counts = equipment_df.groupby('category').size().reset_index(name='count')
- if submit_button:
- if new_employee_name:
- # إنشاء رقم موظف جديد
- new_employee_id = f"EMP-{len(employees_df) + 1:03d}"
-
- # إضافة الموظف الجديد
- new_employee = pd.DataFrame({
- "رقم الموظف": [new_employee_id],
- "اسم الموظف": [new_employee_name],
- "القسم": [new_employee_department],
- "المنصب": [new_employee_position],
- "المهارات": [new_employee_skills],
- "سنوات الخبرة": [new_employee_experience],
- "التكلفة الشهرية": [new_employee_cost],
- "متاح": [new_employee_available],
- "التقييم": [new_employee_rating]
- })
-
- # تحديث DataFrame الموظفين
- st.session_state.resources_data["employees"] = pd.concat([employees_df, new_employee], ignore_index=True)
-
- st.success(f"تم إضافة الموظف {new_employee_name} بنجاح!")
- st.rerun()
- else:
- st.error("يرجى إدخال اسم الموظف")
+ fig = px.bar(
+ category_counts,
+ x='category',
+ y='count',
+ title='توزيع المعدات حسب الفئة',
+ color='category',
+ labels={'category': 'الفئة', 'count': 'العدد'}
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+ else:
+ st.warning("لا توجد معدات مطابقة لمعايير البحث.")
- def _render_equipment_tab(self):
- """عرض تبويب المعدات"""
-
- st.markdown("### إدارة المعدات")
-
- # استخراج البيانات
- equipment_df = st.session_state.resources_data["equipment"]
+ def _render_subcontractors_tab(self):
+ """عرض تبويب المقاولين من الباطن"""
- # عرض خيارات التصفية
- st.markdown("#### خيارات التصفية")
+ st.markdown("### إدارة المقاولين من الباطن")
+ # عرض أدوات البحث والتصفية
col1, col2, col3 = st.columns(3)
with col1:
- selected_types = st.multiselect(
- "النوع",
- options=equipment_df["النوع"].unique(),
- default=equipment_df["النوع"].unique()
- )
+ search_query = st.text_input("بحث في المقاولين", placeholder="ابحث باسم المقاول أو التخصص...")
with col2:
- selected_conditions = st.multiselect(
- "الحالة",
- options=equipment_df["الحالة"].unique(),
- default=equipment_df["الحالة"].unique()
+ category_filter = st.multiselect(
+ "تصفية حسب الفئة",
+ options=list(set(subcontractor['category'] for subcontractor in st.session_state.subcontractors)),
+ default=[],
+ key="subcontractor_category_filter_tab"
)
with col3:
- availability_filter = st.selectbox(
- "الإتاحة",
- options=["الكل", "متاحة فقط", "غير متاحة فقط"],
- key="equipment_availability"
+ city_filter = st.multiselect(
+ "تصفية حسب المدينة",
+ options=list(set(subcontractor['city'] for subcontractor in st.session_state.subcontractors)),
+ default=[],
+ key="subcontractor_city_filter_tab"
)
- # تطبيق التصفية
- filtered_df = equipment_df[
- equipment_df["النوع"].isin(selected_types) &
- equipment_df["الحالة"].isin(selected_conditions)
- ]
-
- if availability_filter == "متاحة فقط":
- filtered_df = filtered_df[filtered_df["متاحة"] == True]
- elif availability_filter == "غير متاحة فقط":
- filtered_df = filtered_df[filtered_df["متاحة"] == False]
-
- # عرض البيانات المصفاة
- st.markdown("#### قائمة المعدات")
-
- st.dataframe(
- filtered_df,
- column_config={
- "رقم المعدة": st.column_config.TextColumn("رقم المعدة"),
- "اسم المعدة": st.column_config.TextColumn("اسم المعدة"),
- "النوع": st.column_config.TextColumn("النوع"),
- "التكلفة اليومية": st.column_config.NumberColumn("التكلفة اليومية", format="%.2f ريال"),
- "متاحة": st.column_config.CheckboxColumn("متاحة"),
- "الحالة": st.column_config.TextColumn("الحالة"),
- "الموقع": st.column_config.TextColumn("الموقع")
- },
- use_container_width=True,
- hide_index=True
- )
-
- # عرض إحصائيات المعدات
- st.markdown("#### إحصائيات المعدات")
-
- col1, col2, col3, col4 = st.columns(4)
-
- with col1:
- total_equipment = len(filtered_df)
- st.metric("إجمالي المعدات", f"{total_equipment}")
-
- with col2:
- available_equipment = len(filtered_df[filtered_df["متاحة"] == True])
- availability_rate = available_equipment / total_equipment * 100 if total_equipment > 0 else 0
- st.metric("معدل الإتاحة", f"{availability_rate:.1f}%")
+ # تطبيق البحث والتصفية
+ filtered_subcontractors = st.session_state.subcontractors
- with col3:
- good_condition = len(filtered_df[filtered_df["الحالة"].isin(["ممتاز", "جيد"])])
- good_condition_rate = good_condition / total_equipment * 100 if total_equipment > 0 else 0
- st.metric("معدل الحالة الجيدة", f"{good_condition_rate:.1f}%")
+ if search_query:
+ filtered_subcontractors = [
+ subcontractor for subcontractor in filtered_subcontractors
+ if (search_query.lower() in subcontractor['name'].lower() or
+ search_query.lower() in subcontractor['specialization'].lower())
+ ]
- with col4:
- avg_cost = filtered_df["التكلفة اليومية"].mean()
- st.metric("متوسط التكلفة اليومية", f"{avg_cost:.0f} ريال")
+ if category_filter:
+ filtered_subcontractors = [subcontractor for subcontractor in filtered_subcontractors if subcontractor['category'] in category_filter]
- # عرض توزيع المعدات حسب النوع
- st.markdown("#### توزيع المعدات حسب النوع")
+ if city_filter:
+ filtered_subcontractors = [subcontractor for subcontractor in filtered_subcontractors if subcontractor['city'] in city_filter]
- type_counts = filtered_df["النوع"].value_counts().reset_index()
- type_counts.columns = ["النوع", "العدد"]
+ # زر إضافة مقاول جديد
+ if st.button("إضافة مقاول جديد"):
+ st.session_state.show_subcontractor_form = True
- fig = px.bar(
- type_counts,
- x="النوع",
- y="العدد",
- title="توزيع المعدات حسب النوع",
- color="النوع",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توزيع المعدات حسب الحالة
- st.markdown("#### توزيع المعدات حسب الحالة")
-
- condition_counts = filtered_df["الحالة"].value_counts().reset_index()
- condition_counts.columns = ["الحالة", "العدد"]
-
- fig = px.pie(
- condition_counts,
- values="العدد",
- names="الحالة",
- title="توزيع المعدات حسب الحالة",
- color="الحالة",
- color_discrete_map={
- "ممتاز": "#2ecc71",
- "جيد": "#3498db",
- "متوسط": "#f39c12",
- "سيء": "#e74c3c"
- }
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توزيع المعدات حسب الموقع
- st.markdown("#### توزيع المعدات حسب الموقع")
-
- location_counts = filtered_df["الموقع"].value_counts().reset_index()
- location_counts.columns = ["الموقع", "العدد"]
-
- fig = px.bar(
- location_counts,
- x="الموقع",
- y="العدد",
- title="توزيع المعدات حسب الموقع",
- color="الموقع",
- text_auto=True
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض العلاقة بين نوع المعدة والتكلفة
- st.markdown("#### العلاقة بين نوع المعدة والتكلفة")
-
- type_cost = filtered_df.groupby("النوع")["التكلفة اليومية"].mean().reset_index()
- type_cost.columns = ["النوع", "متوسط التكلفة اليومية"]
-
- fig = px.bar(
- type_cost,
- x="النوع",
- y="متوسط التكلفة اليومية",
- title="متوسط التكلفة اليومية حسب نوع المعدة",
- color="النوع",
- text_auto=".0f"
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # إضافة معدة جديدة
- st.markdown("#### إضافة معدة جديدة")
+ # نموذج إضافة مقاول جديد
+ if st.session_state.get('show_subcontractor_form', False):
+ with st.form("add_subcontractor_form"):
+ st.markdown("#### إضافة مقاول جديد")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ new_subcontractor_name = st.text_input("اسم المقاول", key="new_subcontractor_name")
+ new_subcontractor_category = st.text_input("الفئة", key="new_subcontractor_category")
+ new_subcontractor_specialization = st.text_input("التخصص", key="new_subcontractor_specialization")
+ new_subcontractor_city = st.text_input("المدينة", key="new_subcontractor_city")
+
+ with col2:
+ new_subcontractor_contact = st.text_input("جهة الاتصال", key="new_subcontractor_contact")
+ new_subcontractor_phone = st.text_input("رقم الهاتف", key="new_subcontractor_phone")
+ new_subcontractor_email = st.text_input("البريد الإلكتروني", key="new_subcontractor_email")
+ new_subcontractor_rating = st.slider("التقييم", 1.0, 5.0, 3.0, 0.1, key="new_subcontractor_rating")
+ new_subcontractor_local_content = st.slider("نسبة المحتوى المحلي (%)", 0, 100, 50, key="new_subcontractor_local_content")
+
+ submitted = st.form_submit_button("إضافة المقاول")
+ cancel = st.form_submit_button("إلغاء")
+
+ if submitted and new_subcontractor_name and new_subcontractor_category and new_subcontractor_specialization:
+ # إضافة المقاول الجديد
+ new_subcontractor = {
+ 'id': max([subcontractor['id'] for subcontractor in st.session_state.subcontractors], default=0) + 1,
+ 'name': new_subcontractor_name,
+ 'category': new_subcontractor_category,
+ 'specialization': new_subcontractor_specialization,
+ 'rating': new_subcontractor_rating,
+ 'city': new_subcontractor_city,
+ 'contact_person': new_subcontractor_contact,
+ 'phone': new_subcontractor_phone,
+ 'email': new_subcontractor_email,
+ 'local_content': new_subcontractor_local_content,
+ 'last_updated': datetime.now().strftime('%Y-%m-%d')
+ }
+
+ st.session_state.subcontractors.append(new_subcontractor)
+ st.success(f"تمت إضافة المقاول '{new_subcontractor_name}' بنجاح!")
+ st.session_state.show_subcontractor_form = False
+ st.rerun()
+
+ if cancel:
+ st.session_state.show_subcontractor_form = False
+ st.rerun()
- with st.form("add_equipment_form"):
- col1, col2 = st.columns(2)
+ # عرض قائمة المقاولين
+ if filtered_subcontractors:
+ # تحويل البيانات إلى DataFrame
+ subcontractors_df = pd.DataFrame(filtered_subcontractors)
+
+ # تنسيق البيانات للعرض
+ display_df = subcontractors_df.copy()
+ display_df['local_content'] = display_df['local_content'].apply(lambda x: f"{x}%")
+
+ # تغيير أسماء الأعمدة للعرض
+ display_df.columns = [
+ 'معرف', 'اسم المقاول', 'الفئة', 'التخصص', 'التقييم', 'المدينة',
+ 'جهة الاتصال', 'رقم الهاتف', 'البريد الإلكتروني', 'نسبة المحتوى المحلي', 'آخر تحديث'
+ ]
+
+ # عرض الجدول
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
+
+ # عرض ملخص إحصائي
+ st.markdown("#### ملخص إحصائي للمقاولين")
+
+ col1, col2, col3 = st.columns(3)
with col1:
- new_equipment_name = st.text_input("اسم المعدة")
- new_equipment_type = st.selectbox("النوع", options=equipment_df["النوع"].unique())
- new_equipment_cost = st.number_input("التكلفة اليومية", min_value=100, max_value=10000, value=1000)
+ st.metric("إجمالي عدد المقاولين", len(filtered_subcontractors))
with col2:
- new_equipment_available = st.checkbox("متاحة", value=True)
- new_equipment_condition = st.selectbox("الحالة", options=["ممتاز", "جيد", "متوسط", "سيء"])
- new_equipment_location = st.selectbox("الموقع", options=equipment_df["الموقع"].unique())
+ avg_rating = sum(subcontractor['rating'] for subcontractor in filtered_subcontractors) / len(filtered_subcontractors)
+ st.metric("متوسط التقييم", f"{avg_rating:.1f}/5.0")
- submit_button = st.form_submit_button("إضافة معدة")
+ with col3:
+ avg_local_content = sum(subcontractor['local_content'] for subcontractor in filtered_subcontractors) / len(filtered_subcontractors)
+ st.metric("متوسط نسبة المحتوى المحلي", f"{avg_local_content:.1f}%")
- if submit_button:
- if new_equipment_name:
- # إنشاء رقم معدة جديد
- new_equipment_id = f"EQ-{len(equipment_df) + 1:03d}"
-
- # إضافة المعدة الجديدة
- new_equipment = pd.DataFrame({
- "رقم المعدة": [new_equipment_id],
- "اسم المعدة": [new_equipment_name],
- "النوع": [new_equipment_type],
- "التكلفة اليومية": [new_equipment_cost],
- "متاحة": [new_equipment_available],
- "الحالة": [new_equipment_condition],
- "الموقع": [new_equipment_location]
- })
-
- # تحديث DataFrame المعدات
- st.session_state.resources_data["equipment"] = pd.concat([equipment_df, new_equipment], ignore_index=True)
-
- st.success(f"تم إضافة المعدة {new_equipment_name} بنجاح!")
- st.rerun()
- else:
- st.error("يرجى إدخال اسم المعدة")
-
- def _render_materials_tab(self):
- """عرض تبويب المواد"""
-
- st.markdown("### إدارة المواد")
-
- # استخراج البيانات
- materials_df = st.session_state.resources_data["materials"]
-
- # عرض خيارات التصفية
- st.markdown("#### خيارات التصفية")
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- selected_units = st.multiselect(
- "الوحدة",
- options=materials_df["الوحدة"].unique(),
- default=materials_df["الوحدة"].unique()
- )
-
- with col2:
- selected_suppliers = st.multiselect(
- "المورد",
- options=materials_df["المورد"].unique(),
- default=materials_df["المورد"].unique()
+ # عرض مخطط توزيع المقاولين حسب الفئة
+ category_counts = subcontractors_df.groupby('category').size().reset_index(name='count')
+
+ fig = px.pie(
+ category_counts,
+ names='category',
+ values='count',
+ title='توزيع المقاولين حسب الفئة',
+ hole=0.4
)
-
- with col3:
- stock_filter = st.selectbox(
- "المخزون",
- options=["الكل", "منخفض المخزون", "مخزون كافي"]
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض مخطط توزيع المقاولين حسب المدينة
+ city_counts = subcontractors_df.groupby('city').size().reset_index(name='count')
+
+ fig = px.bar(
+ city_counts,
+ x='city',
+ y='count',
+ title='توزيع المقاولين حسب المدينة',
+ color='city',
+ labels={'city': 'المدينة', 'count': 'العدد'}
)
+
+ st.plotly_chart(fig, use_container_width=True)
+ else:
+ st.warning("لا يوجد مقاولين مطابقين لمعايير البحث.")
+
+ def _render_price_analysis_tab(self):
+ """عرض تبويب تحليل الأسعار"""
- # تطبيق التصفية
- filtered_df = materials_df[
- materials_df["الوحدة"].isin(selected_units) &
- materials_df["المورد"].isin(selected_suppliers)
- ]
+ st.markdown("### تحليل الأسعار")
- if stock_filter == "منخفض المخزون":
- filtered_df = filtered_df[filtered_df["الكمية المتاحة"] < 50]
- elif stock_filter == "مخزون كافي":
- filtered_df = filtered_df[filtered_df["الكمية المتاحة"] >= 50]
-
- # عرض البيانات المصفاة
- st.markdown("#### قائمة المواد")
-
- st.dataframe(
- filtered_df,
- column_config={
- "رقم المادة": st.column_config.TextColumn("رقم المادة"),
- "اسم المادة": st.column_config.TextColumn("اسم المادة"),
- "الوحدة": st.column_config.TextColumn("الوحدة"),
- "الكمية المتاحة": st.column_config.NumberColumn("الكمية المتاحة"),
- "تكلفة الوحدة": st.column_config.NumberColumn("تكلفة الوحدة", format="%.2f ريال"),
- "المورد": st.column_config.TextColumn("المورد"),
- "مدة التوريد (يوم)": st.column_config.NumberColumn("مدة التوريد (يوم)")
- },
- use_container_width=True,
- hide_index=True
+ # اختيار نوع التحليل
+ analysis_type = st.radio(
+ "نوع التحليل",
+ ["تحليل أسعار المواد", "مقارنة الأسعار", "توقع الأسعار المستقبلية"],
+ horizontal=True
)
- # عرض إحصائيات المواد
- st.markdown("#### إحصائيات المواد")
-
- col1, col2, col3, col4 = st.columns(4)
-
- with col1:
- total_materials = len(filtered_df)
- st.metric("إجمالي المواد", f"{total_materials}")
-
- with col2:
- low_stock_materials = len(filtered_df[filtered_df["الكمية المتاحة"] < 50])
- low_stock_rate = low_stock_materials / total_materials * 100 if total_materials > 0 else 0
- st.metric("نسبة المواد منخفضة المخزون", f"{low_stock_rate:.1f}%")
-
- with col3:
- avg_lead_time = filtered_df["مدة التوريد (يوم)"].mean()
- st.metric("متوسط مدة التوريد", f"{avg_lead_time:.1f} يوم")
-
- with col4:
- total_inventory_value = (filtered_df["الكمية المتاحة"] * filtered_df["تكلفة الوحدة"]).sum()
- st.metric("إجمالي قيمة المخزون", f"{total_inventory_value:,.0f} ريال")
-
- # عرض توزيع المواد حسب المورد
- st.markdown("#### توزيع المواد حسب المورد")
-
- supplier_counts = filtered_df["المورد"].value_counts().reset_index()
- supplier_counts.columns = ["المورد", "العدد"]
-
- fig = px.pie(
- supplier_counts,
- values="العدد",
- names="المورد",
- title="توزيع المواد حسب المورد",
- color="المورد"
+ if analysis_type == "تحليل أسعار المواد":
+ self._render_material_price_analysis()
+ elif analysis_type == "مقارنة الأسعار":
+ self._render_price_comparison()
+ else:
+ self._render_price_forecast()
+
+ def _render_material_price_analysis(self):
+ """عرض تحليل أسعار المواد"""
+
+ st.markdown("#### تحليل أسعار المواد")
+
+ # اختيار المواد للتحليل
+ material_options = [material['name'] for material in st.session_state.materials]
+ selected_materials = st.multiselect(
+ "اختر المواد للتحليل",
+ options=material_options,
+ default=material_options[:3] if len(material_options) >= 3 else material_options,
+ key="price_analysis_materials_tab"
)
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توزيع المواد حسب الوحدة
- st.markdown("#### توزيع المواد حسب الوحدة")
-
- unit_counts = filtered_df["الوحدة"].value_counts().reset_index()
- unit_counts.columns = ["الوحدة", "العدد"]
+ if not selected_materials:
+ st.warning("الرجاء اختيار مادة واحدة على الأقل للتحليل.")
+ return
- fig = px.bar(
- unit_counts,
- x="الوحدة",
- y="العدد",
- title="توزيع المواد حسب الوحدة",
- color="الوحدة",
- text_auto=True
- )
+ # إعداد البيانات للتحليل
+ material_ids = {material['name']: material['id'] for material in st.session_state.materials}
+ selected_ids = [material_ids[name] for name in selected_materials if name in material_ids]
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض المواد منخفضة المخزون
- st.markdown("#### المواد منخفضة المخزون")
+ # التحقق من وجود بيانات سعرية في session_state.price_history
+ if 'price_history' not in st.session_state or not st.session_state.price_history:
+ st.warning("لا توجد بيانات أسعار متاحة للتحليل.")
+ return
+
+ price_history_data = []
+ for entry in st.session_state.price_history:
+ if entry['material_id'] in selected_ids:
+ # الحصول على اسم المادة من المعرف
+ material_name = next((material['name'] for material in st.session_state.materials if material['id'] == entry['material_id']), "")
+
+ # التحقق من وجود المفاتيح المطلوبة
+ if 'date' in entry and 'price' in entry:
+ try:
+ # إضافة البيانات إلى قائمة البيانات مع تحويل التاريخ إلى كائن datetime
+ price_history_data.append({
+ 'material': material_name, # استخدام أسماء إنجليزية للمفاتيح
+ 'date': pd.to_datetime(entry['date']),
+ 'price': float(entry['price']) # التأكد من تحويل السعر إلى رقم
+ })
+ except (ValueError, TypeError) as e:
+ # تسجيل أخطاء تحويل البيانات
+ st.error(f"خطأ في معالجة البيانات: {e}")
+ continue
+
+ if not price_history_data:
+ st.warning("لا توجد بيانات أسعار متاحة للمواد المختارة.")
+ return
+
+ # تحويل البيانات إلى DataFrame
+ price_history_df = pd.DataFrame(price_history_data)
+
+ # التحقق من وجود بيانات قبل رسم المخطط
+ if len(price_history_df) == 0:
+ st.warning("لا توجد بيانات تاريخية للأسعار متاحة لعرضها")
+ else:
+ # عرض المخطط الخطي للأسعار باستخدام أسماء الأعمدة الإنجليزية
+ fig = px.line(
+ price_history_df,
+ x='date',
+ y='price',
+ color='material',
+ title='تطور أسعار المواد المختارة',
+ labels={'date': 'التاريخ', 'price': 'السعر (ريال)', 'material': 'المادة'}
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
- low_stock_df = filtered_df[filtered_df["الكمية المتاحة"] < 50].sort_values("الكمية المتاحة")
+ # حساب التغيرات في الأسعار
+ materials_price_changes = []
- if not low_stock_df.empty:
+ for material_name in selected_materials:
+ # استخدام أسماء الأعمدة الإنجليزية للتصفية والترتيب
+ material_prices = price_history_df[price_history_df['material'] == material_name].sort_values('date')
+
+ if len(material_prices) >= 2:
+ first_price = material_prices.iloc[0]['price']
+ last_price = material_prices.iloc[-1]['price']
+ price_change = last_price - first_price
+ price_change_percent = (price_change / first_price) * 100
+
+ # حساب التقلب (الانحراف المعياري)
+ price_volatility = material_prices['price'].std()
+
+ materials_price_changes.append({
+ 'المادة': material_name,
+ 'السعر الأول': first_price,
+ 'السعر الأخير': last_price,
+ 'التغير المطلق': price_change,
+ 'نسبة التغير (%)': price_change_percent,
+ 'التقلب (الانحراف المعياري)': price_volatility
+ })
+
+ # عرض جدول التغيرات في الأسعار
+ if materials_price_changes:
+ st.markdown("#### تغيرات الأسعار خلال الفترة")
+
+ changes_df = pd.DataFrame(materials_price_changes)
+
+ # تنسيق البيانات للعرض
+ display_df = changes_df.copy()
+ display_df['السعر الأول'] = display_df['السعر الأول'].apply(lambda x: f"{x:,.2f} ريال")
+ display_df['السعر الأخير'] = display_df['السعر الأخير'].apply(lambda x: f"{x:,.2f} ريال")
+ display_df['التغير المطلق'] = display_df['التغير المطلق'].apply(lambda x: f"{x:,.2f} ريال")
+ display_df['نسبة التغير (%)'] = display_df['نسبة التغير (%)'].apply(lambda x: f"{x:.2f}%")
+ display_df['التقلب (الانحراف المعياري)'] = display_df['التقلب (الانحراف المعياري)'].apply(lambda x: f"{x:.2f}")
+
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
+
+ # عرض مخطط شريطي للتغيرات في الأسعار
fig = px.bar(
- low_stock_df,
- x="اسم المادة",
- y="الكمية المتاحة",
- title="المواد منخفضة المخزون",
- color="الكمية المتاحة",
- color_continuous_scale="Reds_r",
- text_auto=True
+ changes_df,
+ x='المادة',
+ y='نسبة التغير (%)',
+ title='نسبة التغير في الأسعار',
+ color='المادة',
+ text_auto='.1f'
)
+ fig.update_traces(texttemplate='%{text}%', textposition='outside')
+
st.plotly_chart(fig, use_container_width=True)
- else:
- st.info("لا توجد مواد منخفضة المخزون")
-
- # عرض العلاقة بين مدة التوريد والمورد
- st.markdown("#### العلاقة بين مدة التوريد والمورد")
+
+ def _render_price_comparison(self):
+ """عرض مقارنة الأسعار"""
- supplier_lead_time = filtered_df.groupby("المورد")["مدة التوريد (يوم)"].mean().reset_index()
- supplier_lead_time.columns = ["المورد", "متوسط مدة التوريد (يوم)"]
+ st.markdown("#### مقارنة الأسعار")
- fig = px.bar(
- supplier_lead_time,
- x="المورد",
- y="متوسط مدة التوريد (يوم)",
- title="متوسط مدة التوريد حسب المورد",
- color="المورد",
- text_auto=".1f"
+ # اختيار نوع المورد للمقارنة
+ resource_type = st.selectbox(
+ "نوع المورد",
+ ["المواد", "العمالة", "المعدات"]
)
- st.plotly_chart(fig, use_container_width=True)
+ if resource_type == "المواد":
+ resources = st.session_state.materials
+ elif resource_type == "العمالة":
+ resources = st.session_state.labor
+ else:
+ resources = st.session_state.equipment
- # إضافة مادة جديدة
- st.markdown("#### إضافة مادة جديدة")
+ # اختيار الفئة للمقارنة
+ categories = list(set([resource['category'] for resource in resources]))
+ selected_category = st.selectbox(
+ "الفئة",
+ options=["الكل"] + categories
+ )
- with st.form("add_material_form"):
- col1, col2 = st.columns(2)
-
- with col1:
- new_material_name = st.text_input("اسم المادة")
- new_material_unit = st.selectbox("الوحدة", options=materials_df["الوحدة"].unique())
- new_material_quantity = st.number_input("الكمية المتاحة", min_value=0, max_value=10000, value=100)
-
- with col2:
- new_material_cost = st.number_input("تكلفة الوحدة", min_value=1, max_value=10000, value=100)
- new_material_supplier = st.selectbox("المورد", options=materials_df["المورد"].unique())
- new_material_lead_time = st.number_input("مدة التوريد (يوم)", min_value=1, max_value=90, value=7)
-
- submit_button = st.form_submit_button("إضافة مادة")
-
- if submit_button:
- if new_material_name:
- # إنشاء رقم مادة جديد
- new_material_id = f"MAT-{len(materials_df) + 1:03d}"
-
- # إضافة المادة الجديدة
- new_material = pd.DataFrame({
- "رقم المادة": [new_material_id],
- "اسم المادة": [new_material_name],
- "الوحدة": [new_material_unit],
- "الكمية المتاحة": [new_material_quantity],
- "تكلفة الوحدة": [new_material_cost],
- "المورد": [new_material_supplier],
- "مدة التوريد (يوم)": [new_material_lead_time]
- })
-
- # تحديث DataFrame المواد
- st.session_state.resources_data["materials"] = pd.concat([materials_df, new_material], ignore_index=True)
-
- st.success(f"تم إضافة المادة {new_material_name} بنجاح!")
- st.rerun()
- else:
- st.error("يرجى إدخال اسم المادة")
+ # فلترة الموارد حسب الفئة
+ if selected_category != "الكل":
+ filtered_resources = [resource for resource in resources if resource['category'] == selected_category]
+ else:
+ filtered_resources = resources
+
+ if not filtered_resources:
+ st.warning("لا توجد موارد مطابقة للفئة المختارة.")
+ return
+
+ # إعداد بيانات المقارنة
+ comparison_data = []
+
+ for resource in filtered_resources:
+ comparison_data.append({
+ 'الاسم': resource['name'],
+ 'الفئة': resource['category'],
+ 'الوحدة': resource['unit'],
+ 'السعر': resource['price'],
+ 'المورد': resource['supplier'],
+ 'نسبة المحتوى المحلي': resource['local_content']
+ })
- # طلب مواد
- st.markdown("#### طلب مواد")
+ # تحويل البيانات إلى DataFrame
+ comparison_df = pd.DataFrame(comparison_data)
- with st.form("order_materials_form"):
- col1, col2, col3 = st.columns(3)
-
- with col1:
- material_to_order = st.selectbox("المادة", options=materials_df["اسم المادة"].unique())
-
- with col2:
- order_quantity = st.number_input("الكمية المطلوبة", min_value=1, max_value=1000, value=50)
-
- with col3:
- order_date = st.date_input("تاريخ الطلب", value=datetime.now())
-
- submit_button = st.form_submit_button("طلب المادة")
-
- if submit_button:
- # محاكاة طلب المادة
- st.success(f"تم طلب {order_quantity} {materials_df[materials_df['اسم المادة'] == material_to_order]['الوحدة'].values[0]} من {material_to_order} بنجاح!")
-
- # عرض تفاصيل الطلب
- material_info = materials_df[materials_df["اسم المادة"] == material_to_order].iloc[0]
- lead_time = material_info["مدة التوريد (يوم)"]
- expected_delivery = order_date + timedelta(days=lead_time)
-
- st.info(f"تاريخ التسليم المتوقع: {expected_delivery.strftime('%Y-%m-%d')}")
-
- # حساب التكلفة الإجمالية
- unit_cost = material_info["تكلفة الوحدة"]
- total_cost = unit_cost * order_quantity
-
- st.metric("التكلفة الإجمالية", f"{total_cost:,.2f} ريال")
-
- def _render_resource_allocation_tab(self):
- """عرض تبويب تخصيص الموارد"""
+ # عرض المخطط الشريطي للأسعار
+ fig = px.bar(
+ comparison_df,
+ x='الاسم',
+ y='السعر',
+ title=f'مقارنة أسعار {resource_type}',
+ color='الفئة' if selected_category == "الكل" else 'المورد',
+ text_auto='.2s',
+ labels={'السعر': 'السعر (ريال)'}
+ )
- st.markdown("### تخصيص الموارد")
+ fig.update_traces(texttemplate='%{text} ريال', textposition='outside')
- # استخراج البيانات
- employees_df = st.session_state.resources_data["employees"]
- equipment_df = st.session_state.resources_data["equipment"]
- materials_df = st.session_state.resources_data["materials"]
- projects_df = st.session_state.resources_data["projects"]
- allocations_df = st.session_state.resources_data["allocations"]
+ st.plotly_chart(fig, use_container_width=True)
- # عرض خيارات التصفية
- st.markdown("#### خيارات التصفية")
+ # عرض العلاقة بين السعر ونسبة المحتوى المحلي
+ fig = px.scatter(
+ comparison_df,
+ x='نسبة المحتوى المحلي',
+ y='السعر',
+ color='الفئة' if selected_category == "الكل" else None,
+ title='العلاقة بين السعر ونسبة المحتوى المحلي',
+ labels={'نسبة المحتوى المحلي': 'نسبة المحتوى المحلي (%)', 'السعر': 'السعر (ريال)'},
+ size=[50] * len(comparison_df),
+ text='الاسم'
+ )
- col1, col2 = st.columns(2)
+ fig.update_traces(textposition='top center')
- with col1:
- selected_projects = st.multiselect(
- "المشروع",
- options=projects_df["اسم المشروع"].unique(),
- default=projects_df["اسم المشروع"].unique()
- )
+ st.plotly_chart(fig, use_container_width=True)
- with col2:
- selected_resource_types = st.multiselect(
- "نوع المورد",
- options=allocations_df["نوع المورد"].unique(),
- default=allocations_df["نوع المورد"].unique()
- )
+ # عرض جدول المقارنة
+ st.markdown("#### جدول مقارنة الأسعار")
- # تحويل أسماء المشاريع إلى أرقام المشاريع
- selected_project_ids = projects_df[projects_df["اسم المشروع"].isin(selected_projects)]["رقم المشروع"].tolist()
+ # تنسيق البيانات للعرض
+ display_df = comparison_df.copy()
+ display_df['السعر'] = display_df['السعر'].apply(lambda x: f"{x:,.2f} ريال")
+ display_df['نسبة المحتوى المحلي'] = display_df['نسبة المحتوى المحلي'].apply(lambda x: f"{x}%")
- # تطبيق التصفية
- filtered_df = allocations_df[
- allocations_df["رقم المشروع"].isin(selected_project_ids) &
- allocations_df["نوع المورد"].isin(selected_resource_types)
- ]
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
+
+ def _render_price_forecast(self):
+ """عرض توقع الأسعار المستقبلية"""
- # دمج البيانات مع بيانات المشاريع
- merged_df = filtered_df.merge(
- projects_df[["رقم المشروع", "اسم المشروع"]],
- on="رقم المشروع",
- how="left"
+ st.markdown("#### توقع الأسعار المستقبلية")
+
+ # اختيار المادة للتوقع
+ material_options = [material['name'] for material in st.session_state.materials]
+ selected_material = st.selectbox(
+ "اختر المادة للتوقع",
+ options=material_options
)
- # إضافة أسماء الموارد
- merged_df["اسم المورد"] = ""
-
- for i, row in merged_df.iterrows():
- if row["نوع المورد"] == "موظف":
- resource_name = employees_df[employees_df["رقم الموظف"] == row["رقم المورد"]]["اسم الموظف"].values
- if len(resource_name) > 0:
- merged_df.at[i, "اسم المورد"] = resource_name[0]
- elif row["نوع المورد"] == "معدة":
- resource_name = equipment_df[equipment_df["رقم المعدة"] == row["رقم المورد"]]["اسم المعدة"].values
- if len(resource_name) > 0:
- merged_df.at[i, "اسم المورد"] = resource_name[0]
- elif row["نوع المورد"] == "مادة":
- resource_name = materials_df[materials_df["رقم المادة"] == row["رقم المورد"]]["اسم المادة"].values
- if len(resource_name) > 0:
- merged_df.at[i, "اسم المورد"] = resource_name[0]
-
- # عرض البيانات المصفاة
- st.markdown("#### قائمة تخصيص الموارد")
-
- display_df = merged_df[["رقم التخصيص", "اسم المشروع", "نوع المورد", "اسم المورد", "تاريخ البدء", "تاريخ الانتهاء", "الكمية", "التكلفة"]]
-
- st.dataframe(
- display_df,
- column_config={
- "رقم التخصيص": st.column_config.TextColumn("رقم التخصيص"),
- "اسم المشروع": st.column_config.TextColumn("اسم المشروع"),
- "نوع المورد": st.column_config.TextColumn("نوع المورد"),
- "اسم المورد": st.column_config.TextColumn("اسم المورد"),
- "تاريخ البدء": st.column_config.DateColumn("تاريخ البدء"),
- "تاريخ الانتهاء": st.column_config.DateColumn("تاريخ الانتهاء"),
- "الكمية": st.column_config.NumberColumn("الكمية"),
- "التكلفة": st.column_config.NumberColumn("التكلفة", format="%.2f ريال")
- },
- use_container_width=True,
- hide_index=True
+ # اختيار فترة التوقع
+ forecast_period = st.slider(
+ "فترة التوقع (أشهر)",
+ min_value=1,
+ max_value=12,
+ value=6
)
- # عرض إحصائيات تخصيص الموارد
- st.markdown("#### إحصائيات تخصيص الموارد")
+ if not selected_material:
+ st.warning("الرجاء اختيار مادة للتوقع.")
+ return
+
+ # الحصول على معرف المادة
+ material_id = next((material['id'] for material in st.session_state.materials if material['name'] == selected_material), None)
+
+ if material_id is None:
+ st.error("المادة المحددة غير موجودة.")
+ return
+
+ # الحصول على بيانات الأسعار التاريخية
+ price_history_data = []
+ for entry in st.session_state.price_history:
+ if entry['material_id'] == material_id:
+ try:
+ price_history_data.append({
+ 'date': pd.to_datetime(entry['date']),
+ 'price': float(entry['price'])
+ })
+ except (ValueError, TypeError) as e:
+ st.error(f"خطأ في معالجة البيانات: {e}")
+ continue
- col1, col2, col3, col4 = st.columns(4)
+ if not price_history_data:
+ st.warning("لا توجد بيانات تاريخية كافية للمادة المحددة للقيام بالتوقع.")
+ return
- with col1:
- total_allocations = len(merged_df)
- st.metric("إجمالي التخصيصات", f"{total_allocations}")
+ # تحويل البيانات إلى DataFrame
+ price_history_df = pd.DataFrame(price_history_data).sort_values('date')
- with col2:
- total_cost = merged_df["التكلفة"].sum()
- st.metric("إجمالي التكلفة", f"{total_cost:,.0f} ريال")
+ # إجراء التوقع
+ # في الواقع، ستستخدم نماذج تعلم آلي مثل ARIMA أو Prophet
+ # هنا سنستخدم توقعًا بسيطًا للأغراض التوضيحية
- with col3:
- avg_duration = (pd.to_datetime(merged_df["تاريخ الانتهاء"]) - pd.to_datetime(merged_df["تاريخ البدء"])).mean().days
- st.metric("متوسط مدة التخصيص", f"{avg_duration:.0f} يوم")
+ # حساب متوسط التغير الشهري
+ monthly_changes = []
+ for i in range(1, len(price_history_df)):
+ monthly_changes.append(price_history_df.iloc[i]['price'] - price_history_df.iloc[i-1]['price'])
- with col4:
- resource_types = merged_df["نوع المورد"].value_counts()
- most_common_type = resource_types.index[0] if not resource_types.empty else ""
- st.metric("أكثر أنواع الموارد تخصيصاً", f"{most_common_type}")
+ if monthly_changes:
+ avg_monthly_change = sum(monthly_changes) / len(monthly_changes)
+ else:
+ avg_monthly_change = 0
- # عرض توزيع تخصيص الموارد حسب المشروع
- st.markdown("#### توزيع تخصيص الموارد حسب المشروع")
+ # إنشاء بيانات التوقع
+ last_date = price_history_df['date'].max()
+ last_price = price_history_df.loc[price_history_df['date'] == last_date, 'price'].values[0]
- project_allocations = merged_df.groupby("اسم المشروع").size().reset_index()
- project_allocations.columns = ["اسم المشروع", "عدد التخصيصات"]
+ forecast_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=forecast_period, freq='M')
+ forecast_prices = [last_price + (i+1) * avg_monthly_change for i in range(forecast_period)]
- fig = px.bar(
- project_allocations,
- x="اسم المشروع",
- y="عدد التخصيصات",
- title="توزيع تخصيص الموارد حسب المشروع",
- color="اسم المشروع",
- text_auto=True
- )
+ # إضافة بعض التقلبات العشوائية للتوقع
+ forecast_prices = [price + random.uniform(-price*0.05, price*0.05) for price in forecast_prices]
- st.plotly_chart(fig, use_container_width=True)
+ forecast_df = pd.DataFrame({
+ 'date': forecast_dates,
+ 'price': forecast_prices,
+ 'type': ['توقع'] * forecast_period
+ })
- # عرض توزيع تخصيص الموارد حسب نوع المورد
- st.markdown("#### توزيع تخصيص الموارد حسب نوع المورد")
-
- resource_type_allocations = merged_df.groupby("نوع المورد").size().reset_index()
- resource_type_allocations.columns = ["نوع المورد", "عدد التخصيصات"]
-
- fig = px.pie(
- resource_type_allocations,
- values="عدد التخصيصات",
- names="نوع المورد",
- title="توزيع تخصيص الموارد حسب نوع المورد",
- color="نوع المورد",
- color_discrete_map={
- "موظف": "#3498db",
- "معدة": "#2ecc71",
- "مادة": "#f39c12"
- }
+ # دمج البيانات التاريخية والتوقع
+ historical_df = price_history_df.copy()
+ historical_df['type'] = ['تاريخي'] * len(historical_df)
+
+ combined_df = pd.concat([historical_df, forecast_df], ignore_index=True)
+
+ # عرض المخطط
+ fig = px.line(
+ combined_df,
+ x='date',
+ y='price',
+ color='type',
+ title=f'توقع أسعار {selected_material} للـ {forecast_period} أشهر القادمة',
+ labels={'date': 'التاريخ', 'price': 'السعر (ريال)', 'type': 'النوع'},
+ color_discrete_map={'تاريخي': 'blue', 'توقع': 'red'}
)
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توزيع تكاليف الموارد حسب المشروع
- st.markdown("#### توزيع تكاليف الموارد حسب المشروع")
-
- project_costs = merged_df.groupby("اسم المشروع")["التكلفة"].sum().reset_index()
- project_costs.columns = ["اسم المشروع", "إجمالي التكلفة"]
-
- fig = px.bar(
- project_costs,
- x="اسم المشروع",
- y="إجمالي التكلفة",
- title="توزيع تكاليف الموارد حسب المشروع",
- color="اسم المشروع",
- text_auto=".0f"
+ # إضافة فترة الثقة حول التوقع
+ confidence = 0.1 # 10% فترة ثقة
+ upper_bound = [price * (1 + confidence) for price in forecast_prices]
+ lower_bound = [price * (1 - confidence) for price in forecast_prices]
+
+ fig.add_scatter(
+ x=forecast_dates,
+ y=upper_bound,
+ fill=None,
+ mode='lines',
+ line_color='rgba(255, 0, 0, 0.3)',
+ line_width=0,
+ showlegend=False
)
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توزيع تكاليف الموارد حسب نوع المورد
- st.markdown("#### توزيع تكاليف الموارد حسب نوع المورد")
-
- resource_type_costs = merged_df.groupby("نوع المورد")["التكلفة"].sum().reset_index()
- resource_type_costs.columns = ["نوع المورد", "إجمالي التكلفة"]
-
- fig = px.pie(
- resource_type_costs,
- values="إجمالي التكلفة",
- names="نوع المورد",
- title="توزيع تكاليف الموارد حسب نوع المورد",
- color="نوع المورد",
- color_discrete_map={
- "موظف": "#3498db",
- "معدة": "#2ecc71",
- "مادة": "#f39c12"
- }
+ fig.add_scatter(
+ x=forecast_dates,
+ y=lower_bound,
+ fill='tonexty',
+ mode='lines',
+ line_color='rgba(255, 0, 0, 0.3)',
+ line_width=0,
+ name='فترة الثقة (±10%)'
)
st.plotly_chart(fig, use_container_width=True)
- # إضافة تخصيص جديد
- st.markdown("#### إضافة تخصيص جديد")
+ # عرض جدول التوقع
+ st.markdown("#### جدول توقع الأسعار")
- with st.form("add_allocation_form"):
- col1, col2 = st.columns(2)
-
- with col1:
- new_allocation_project = st.selectbox("المشروع", options=projects_df["اسم المشروع"].unique())
- new_allocation_resource_type = st.selectbox("نوع المورد", options=["موظف", "معدة", "مادة"])
-
- # تحديد خيارات الموارد بناءً على النوع
- if new_allocation_resource_type == "موظف":
- resource_options = employees_df[employees_df["متاح"] == True]["اسم الموظف"].unique()
- elif new_allocation_resource_type == "معدة":
- resource_options = equipment_df[equipment_df["متاحة"] == True]["اسم المعدة"].unique()
- else:
- resource_options = materials_df["اسم المادة"].unique()
-
- new_allocation_resource = st.selectbox("المورد", options=resource_options)
-
- with col2:
- new_allocation_start_date = st.date_input("تاريخ البدء", value=datetime.now())
- new_allocation_end_date = st.date_input("تاريخ الانتهاء", value=datetime.now() + timedelta(days=30))
- new_allocation_quantity = st.number_input("الكمية", min_value=1, max_value=100, value=1)
-
- submit_button = st.form_submit_button("إضافة تخصيص")
-
- if submit_button:
- # التحقق من صحة التواريخ
- if new_allocation_end_date <= new_allocation_start_date:
- st.error("يجب أن يكون تاريخ الانتهاء بعد تاريخ البدء")
- else:
- # الحصول على رقم المشروع
- project_id = projects_df[projects_df["اسم المشروع"] == new_allocation_project]["رقم المشروع"].values[0]
-
- # الحصول على رقم المورد
- if new_allocation_resource_type == "موظف":
- resource_id = employees_df[employees_df["اسم الموظف"] == new_allocation_resource]["رقم الموظف"].values[0]
- # حساب التكلفة
- cost = employees_df[employees_df["رقم الموظف"] == resource_id]["التكلفة الشهرية"].values[0] * new_allocation_quantity
- elif new_allocation_resource_type == "معدة":
- resource_id = equipment_df[equipment_df["اسم المعدة"] == new_allocation_resource]["رقم المعدة"].values[0]
- # حساب التكلفة
- days = (new_allocation_end_date - new_allocation_start_date).days
- cost = equipment_df[equipment_df["رقم المعدة"] == resource_id]["التكلفة اليومية"].values[0] * days * new_allocation_quantity
- else:
- resource_id = materials_df[materials_df["اسم المادة"] == new_allocation_resource]["رقم المادة"].values[0]
- # حساب التكلفة
- cost = materials_df[materials_df["رقم المادة"] == resource_id]["تكلفة الوحدة"].values[0] * new_allocation_quantity
-
- # إنشاء رقم تخصيص جديد
- new_allocation_id = f"ALLOC-{len(allocations_df) + 1:03d}"
-
- # إضافة التخصيص الجديد
- new_allocation = pd.DataFrame({
- "رقم التخصيص": [new_allocation_id],
- "رقم المشروع": [project_id],
- "نوع المورد": [new_allocation_resource_type],
- "رقم المورد": [resource_id],
- "تاريخ البدء": [new_allocation_start_date.strftime("%Y-%m-%d")],
- "تاريخ الانتهاء": [new_allocation_end_date.strftime("%Y-%m-%d")],
- "الكمية": [new_allocation_quantity],
- "التكلفة": [cost]
- })
-
- # تحديث DataFrame التخصيصات
- st.session_state.resources_data["allocations"] = pd.concat([allocations_df, new_allocation], ignore_index=True)
-
- # تحديث حالة المورد إذا كان موظف أو معدة
- if new_allocation_resource_type == "موظف":
- employees_idx = employees_df[employees_df["رقم الموظف"] == resource_id].index
- st.session_state.resources_data["employees"].at[employees_idx[0], "متاح"] = False
- elif new_allocation_resource_type == "معدة":
- equipment_idx = equipment_df[equipment_df["رقم المعدة"] == resource_id].index
- st.session_state.resources_data["equipment"].at[equipment_idx[0], "متاحة"] = False
- elif new_allocation_resource_type == "مادة":
- materials_idx = materials_df[materials_df["رقم المادة"] == resource_id].index
- current_quantity = st.session_state.resources_data["materials"].at[materials_idx[0], "الكمية المتاحة"]
- st.session_state.resources_data["materials"].at[materials_idx[0], "الكمية المتاحة"] = max(0, current_quantity - new_allocation_quantity)
-
- st.success(f"تم إضافة تخصيص {new_allocation_resource_type} ({new_allocation_resource}) لمشروع {new_allocation_project} بنجاح!")
- st.rerun()
-
- def _render_resource_planning_tab(self):
- """عرض تبويب تخطيط الموارد"""
-
- st.markdown("### تخطيط الموارد")
-
- # استخراج البيانات
- employees_df = st.session_state.resources_data["employees"]
- equipment_df = st.session_state.resources_data["equipment"]
- materials_df = st.session_state.resources_data["materials"]
- projects_df = st.session_state.resources_data["projects"]
- allocations_df = st.session_state.resources_data["allocations"]
-
- # عرض المشاريع القادمة
- st.markdown("#### المشاريع القادمة")
-
- upcoming_projects = projects_df[projects_df["الحالة"] == "مخطط"].sort_values("تاريخ البدء")
-
- if not upcoming_projects.empty:
- st.dataframe(
- upcoming_projects,
- column_config={
- "رقم المشروع": st.column_config.TextColumn("رقم المشروع"),
- "اسم المشروع": st.column_config.TextColumn("اسم المشروع"),
- "الموقع": st.column_config.TextColumn("الموقع"),
- "تاريخ البدء": st.column_config.DateColumn("تاريخ البدء"),
- "تاريخ الانتهاء": st.column_config.DateColumn("تاريخ الانتهاء"),
- "الميزانية": st.column_config.NumberColumn("الميزانية", format="%.2f ريال"),
- "الحالة": st.column_config.TextColumn("الحالة")
- },
- use_container_width=True,
- hide_index=True
- )
- else:
- st.info("لا توجد مشاريع قادمة")
+ forecast_table = forecast_df.copy()
+ forecast_table['date'] = forecast_table['date'].dt.strftime('%Y-%m')
+ forecast_table['price'] = forecast_table['price'].apply(lambda x: f"{x:,.2f} ريال")
+ # إعادة تسمية الأعمدة إلى العربية لعرض الجدول
+ forecast_table = forecast_table.rename(columns={
+ 'date': 'التاريخ',
+ 'price': 'السعر'
+ })
+ forecast_table = forecast_table.drop(columns=['type'])
+
+ st.dataframe(forecast_table, use_container_width=True, hide_index=True)
- # عرض توافر الموارد
- st.markdown("#### توافر الموارد")
+ # عرض ملخص التوقع
+ st.markdown("#### ملخص التوقع")
col1, col2, col3 = st.columns(3)
with col1:
- total_employees = len(employees_df)
- available_employees = len(employees_df[employees_df["متاح"] == True])
- availability_rate = available_employees / total_employees * 100 if total_employees > 0 else 0
-
- st.metric("الموظفون المتاحون", f"{available_employees}/{total_employees}", f"{availability_rate:.1f}%")
-
- # عرض توزيع توافر الموظفين
- availability_data = pd.DataFrame({
- "الحالة": ["متاح", "غير متاح"],
- "العدد": [available_employees, total_employees - available_employees]
- })
-
- fig = px.pie(
- availability_data,
- values="العدد",
- names="الحالة",
- title="توافر الموظفين",
- color="الحالة",
- color_discrete_map={
- "متاح": "#2ecc71",
- "غير متاح": "#e74c3c"
- }
+ st.metric(
+ "السعر الحالي",
+ f"{last_price:,.2f} ريال"
)
-
- st.plotly_chart(fig, use_container_width=True)
with col2:
- total_equipment = len(equipment_df)
- available_equipment = len(equipment_df[equipment_df["متاحة"] == True])
- availability_rate = available_equipment / total_equipment * 100 if total_equipment > 0 else 0
-
- st.metric("المعدات المتاحة", f"{available_equipment}/{total_equipment}", f"{availability_rate:.1f}%")
-
- # عرض توزيع توافر المعدات
- availability_data = pd.DataFrame({
- "الحالة": ["متاحة", "غير متاحة"],
- "العدد": [available_equipment, total_equipment - available_equipment]
- })
-
- fig = px.pie(
- availability_data,
- values="العدد",
- names="الحالة",
- title="توافر المعدات",
- color="الحالة",
- color_discrete_map={
- "متاحة": "#2ecc71",
- "غير متاحة": "#e74c3c"
- }
+ forecasted_price = forecast_prices[-1]
+ price_change = forecasted_price - last_price
+ price_change_percent = (price_change / last_price) * 100
+
+ st.metric(
+ f"السعر المتوقع بعد {forecast_period} أشهر",
+ f"{forecasted_price:,.2f} ريال",
+ delta=f"{price_change_percent:.1f}%"
)
-
- st.plotly_chart(fig, use_container_width=True)
with col3:
- total_materials = len(materials_df)
- low_stock_materials = len(materials_df[materials_df["الكمية المتاحة"] < 50])
- low_stock_rate = low_stock_materials / total_materials * 100 if total_materials > 0 else 0
-
- st.metric("المواد منخفضة المخزون", f"{low_stock_materials}/{total_materials}", f"{low_stock_rate:.1f}%")
+ avg_forecasted_price = sum(forecast_prices) / len(forecast_prices)
- # عرض توزيع حالة المخزون
- stock_data = pd.DataFrame({
- "حالة المخزون": ["مخزون كافي", "مخزون منخفض"],
- "العدد": [total_materials - low_stock_materials, low_stock_materials]
- })
-
- fig = px.pie(
- stock_data,
- values="العدد",
- names="حالة المخزون",
- title="حالة مخزون المواد",
- color="حالة المخزون",
- color_discrete_map={
- "مخزون كافي": "#2ecc71",
- "مخزون منخفض": "#e74c3c"
- }
+ st.metric(
+ "متوسط السعر المتوقع",
+ f"{avg_forecasted_price:,.2f} ريال"
)
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض جدول زمني للموارد
- st.markdown("#### الجدول الزمني للموارد")
-
- # إنشاء DataFrame للجدول الزمني
- timeline_df = allocations_df.copy()
- timeline_df["تاريخ البدء"] = pd.to_datetime(timeline_df["تاريخ البدء"])
- timeline_df["تاريخ الانتهاء"] = pd.to_datetime(timeline_df["تاريخ الانتهاء"])
-
- # دمج البيانات مع بيانات المشاريع
- timeline_df = timeline_df.merge(
- projects_df[["رقم المشروع", "اسم المشروع"]],
- on="رقم المشروع",
- how="left"
- )
-
- # إضافة أسماء الموارد
- timeline_df["اسم المورد"] = ""
-
- for i, row in timeline_df.iterrows():
- if row["نوع المورد"] == "موظف":
- resource_name = employees_df[employees_df["رقم الموظف"] == row["رقم المورد"]]["اسم الموظف"].values
- if len(resource_name) > 0:
- timeline_df.at[i, "اسم المورد"] = resource_name[0]
- elif row["نوع المورد"] == "معدة":
- resource_name = equipment_df[equipment_df["رقم المعدة"] == row["رقم المورد"]]["اسم المعدة"].values
- if len(resource_name) > 0:
- timeline_df.at[i, "اسم المورد"] = resource_name[0]
- elif row["نوع المورد"] == "مادة":
- resource_name = materials_df[materials_df["رقم المادة"] == row["رقم المورد"]]["اسم المادة"].values
- if len(resource_name) > 0:
- timeline_df.at[i, "اسم المورد"] = resource_name[0]
-
- # إنشاء رسم بياني للجدول الزمني
- fig = px.timeline(
- timeline_df,
- x_start="تاريخ البدء",
- x_end="تاريخ الانتهاء",
- y="اسم المورد",
- color="اسم المشروع",
- hover_name="اسم المشروع",
- hover_data=["نوع المورد", "الكمية", "التكلفة"],
- title="الجدول الزمني لتخصيص الموارد"
- )
-
- fig.update_yaxes(autorange="reversed")
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توقعات الاحتياجات المستقبلية
- st.markdown("#### توقعات الاحتياجات المستقبلية")
- # اختيار المشروع للتخطيط
- project_for_planning = st.selectbox("اختر المشروع للتخطيط", options=upcoming_projects["اسم المشروع"] if not upcoming_projects.empty else ["لا توجد مشاريع قادمة"])
-
- if project_for_planning != "لا توجد مشاريع قادمة":
- # الحصول على بيانات المشروع
- project_data = upcoming_projects[upcoming_projects["اسم المشروع"] == project_for_planning].iloc[0]
-
- st.markdown(f"**تاريخ البدء:** {project_data['تاريخ البدء']}")
- st.markdown(f"**تاريخ الانتهاء:** {project_data['تاريخ الانتهاء']}")
- st.markdown(f"**الميزانية:** {project_data['الميزانية']:,.0f} ريال")
-
- # تقدير الاحتياجات بناءً على المشاريع المماثلة
- st.markdown("##### تقدير الاحتياجات بناءً على المشاريع المماثلة")
-
- # محاكاة تقدير الاحتياجات
- estimated_resources = {
- "الموظفون": {
- "مهندس": 5,
- "فني": 10,
- "مشرف": 2,
- "محاسب": 1,
- "مساعد": 3
- },
- "المعدات": {
- "حفارة كبيرة": 1,
- "حفارة صغيرة": 2,
- "شاحنة نقل": 3,
- "رافعة متوسطة": 1,
- "خلاطة خرسانة": 2
- },
- "المواد": {
- "خرسانة جاهزة": 500,
- "حديد تسليح": 200,
- "طابوق": 10000,
- "أسمنت": 1000,
- "رمل": 300
- }
- }
-
- # عرض تقدير الاحتياجات
- col1, col2, col3 = st.columns(3)
-
- with col1:
- st.markdown("**الموظفون المطلوبون:**")
- for position, count in estimated_resources["الموظفون"].items():
- st.markdown(f"- {position}: {count}")
-
- # التحقق من توافر الموظفين
- available_positions = {}
- for position, count in estimated_resources["الموظفون"].items():
- available_count = len(employees_df[(employees_df["المنصب"] == position) & (employees_df["متاح"] == True)])
- available_positions[position] = available_count
-
- if available_count < count:
- st.warning(f"نقص في {position}: متاح {available_count}/{count}")
- else:
- st.success(f"متوفر: {available_count}/{count}")
-
- with col2:
- st.markdown("**المعدات المطلوبة:**")
- for equipment_name, count in estimated_resources["المعدات"].items():
- st.markdown(f"- {equipment_name}: {count}")
-
- # التحقق من توافر المعدات
- available_equipment = {}
- for equipment_name, count in estimated_resources["المعدات"].items():
- available_count = len(equipment_df[(equipment_df["اسم المعدة"] == equipment_name) & (equipment_df["متاحة"] == True)])
- available_equipment[equipment_name] = available_count
-
- if available_count < count:
- st.warning(f"نقص في {equipment_name}: متاح {available_count}/{count}")
- else:
- st.success(f"متوفر: {available_count}/{count}")
-
- with col3:
- st.markdown("**المواد المطلوبة:**")
- for material_name, quantity in estimated_resources["المواد"].items():
- st.markdown(f"- {material_name}: {quantity}")
-
- # التحقق من توافر المواد
- available_materials = {}
- for material_name, quantity in estimated_resources["المواد"].items():
- available_quantity = materials_df[materials_df["اسم المادة"] == material_name]["الكمية المتاحة"].sum()
- available_materials[material_name] = available_quantity
-
- if available_quantity < quantity:
- st.warning(f"نقص في {material_name}: متاح {available_quantity}/{quantity}")
- else:
- st.success(f"متوفر: {available_quantity}/{quantity}")
-
- # عرض تقدير التكاليف
- st.markdown("##### تقدير تكاليف الموارد")
-
- # حساب تكاليف الموظفين
- employee_costs = 0
- for position, count in estimated_resources["الموظفون"].items():
- avg_cost = employees_df[employees_df["المنصب"] == position]["التكلفة الشهرية"].mean()
- # افتراض مدة المشروع 6 أشهر
- employee_costs += avg_cost * count * 6
-
- # حساب تكاليف المعدات
- equipment_costs = 0
- for equipment_name, count in estimated_resources["المعدات"].items():
- avg_cost = equipment_df[equipment_df["اسم المعدة"] == equipment_name]["التكلفة اليومية"].mean()
- # افتراض مدة المشروع 180 يوم
- equipment_costs += avg_cost * count * 180
-
- # حساب تكاليف المواد
- material_costs = 0
- for material_name, quantity in estimated_resources["المواد"].items():
- avg_cost = materials_df[materials_df["اسم المادة"] == material_name]["تكلفة الوحدة"].mean()
- material_costs += avg_cost * quantity
-
- # إجمالي التكاليف
- total_costs = employee_costs + equipment_costs + material_costs
-
- # عرض التكاليف
- col1, col2, col3, col4 = st.columns(4)
-
- with col1:
- st.metric("تكاليف الموظفين", f"{employee_costs:,.0f} ريال")
-
- with col2:
- st.metric("تكاليف المعدات", f"{equipment_costs:,.0f} ريال")
-
- with col3:
- st.metric("تكاليف المواد", f"{material_costs:,.0f} ريال")
-
- with col4:
- st.metric("إجمالي التكاليف", f"{total_costs:,.0f} ريال")
-
- # عرض توزيع التكاليف
- cost_distribution = pd.DataFrame({
- "نوع التكلفة": ["تكاليف الموظفين", "تكاليف المعدات", "تكاليف المواد"],
- "التكلفة": [employee_costs, equipment_costs, material_costs]
- })
-
- fig = px.pie(
- cost_distribution,
- values="التكلفة",
- names="نوع التكلفة",
- title="توزيع تكاليف الموارد",
- color="نوع التكلفة",
- color_discrete_map={
- "تكاليف الموظفين": "#3498db",
- "تكاليف المعدات": "#2ecc71",
- "تكاليف المواد": "#f39c12"
- }
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توصيات لتخطيط الموارد
- st.markdown("##### توصيات لتخطيط الموارد")
-
- recommendations = []
-
- # توصيات للموظفين
- for position, count in estimated_resources["الموظفون"].items():
- available_count = available_positions[position]
- if available_count < count:
- recommendations.append(f"توظيف {count - available_count} {position} إضافي")
-
- # توصيات للمعدات
- for equipment_name, count in estimated_resources["المعدات"].items():
- available_count = available_equipment[equipment_name]
- if available_count < count:
- recommendations.append(f"استئجار {count - available_count} {equipment_name} إضافية")
-
- # توصيات للمواد
- for material_name, quantity in estimated_resources["المواد"].items():
- available_quantity = available_materials[material_name]
- if available_quantity < quantity:
- recommendations.append(f"شراء {quantity - available_quantity} وحدة إضافية من {material_name}")
-
- if recommendations:
- for recommendation in recommendations:
- st.markdown(f"- {recommendation}")
- else:
- st.success("جميع الموارد المطلوبة متوفرة")
-
- # زر لإنشاء خطة الموارد
- if st.button("إنشاء خطة الموارد"):
- st.success("تم إنشاء خطة الموارد بنجاح!")
-
- # عرض ملخص الخطة
- st.markdown("##### ملخص خطة الموارد")
- st.markdown(f"**المشروع:** {project_for_planning}")
- st.markdown(f"**تاريخ البدء:** {project_data['تاريخ البدء']}")
- st.markdown(f"**تاريخ الانتهاء:** {project_data['تاريخ الانتهاء']}")
- st.markdown(f"**إجمالي تكاليف الموارد:** {total_costs:,.0f} ريال")
- st.markdown(f"**نسبة تكاليف الموارد من الميزانية:** {total_costs / project_data['الميزانية'] * 100:.1f}%")
-
- if recommendations:
- st.markdown("**الإجراءات المطلوبة:**")
- for recommendation in recommendations:
- st.markdown(f"- {recommendation}")
- else:
- st.markdown("**الإجراءات المطلوبة:** لا توجد إجراءات مطلوبة، جميع الموارد متوفرة")
+ # عرض ملاحظات وتوصيات
+ if price_change_percent > 10:
+ st.warning("""
+ ### توقع ارتفاع كبير في الأسعار
+ - ينصح بشراء المواد مبكراً وتخزينها إذا أمكن
+ - التفاوض على عقود توريد طويلة الأجل بأسعار ثابتة
+ - البحث عن موردين بديلين أو مواد بديلة
+ """)
+ elif price_change_percent < -10:
+ st.success("""
+ ### توقع انخفاض كبير في الأسعار
+ - ينصح بتأجيل شراء المواد إذا أمكن
+ - شراء كميات أقل والاحتفاظ بمخزون منخفض
+ - التفاوض على عقود مرنة مع الموردين
+ """)
else:
- st.info("لا توجد مشاريع قادمة للتخطيط")
+ st.info("""
+ ### توقع استقرار نسبي في الأسعار
+ - يمكن الشراء حسب الاحتياج دون الحاجة لتخزين كميات كبيرة
+ - متابعة أسعار السوق بشكل دوري للتأكد من دقة التوقعات
+ """)
\ No newline at end of file
diff --git a/modules/risk_analysis/risk_analysis_app.py b/modules/risk_analysis/risk_analysis_app.py
new file mode 100644
index 0000000000000000000000000000000000000000..95b2a921305467e214946d3ad8852ac662c8175c
--- /dev/null
+++ b/modules/risk_analysis/risk_analysis_app.py
@@ -0,0 +1,751 @@
+"""
+تطبيق وحدة تحليل المخاطر
+"""
+
+import streamlit as st
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+import plotly.express as px
+import plotly.graph_objects as go
+from datetime import datetime
+import random
+import os
+import time
+import io
+
+from utils.helpers import format_number, format_currency
+from utils.excel_handler import export_to_excel
+
+
+class RiskAnalysisApp:
+ """وحدة تحليل المخاطر"""
+
+ def __init__(self):
+ """تهيئة وحدة تحليل المخاطر"""
+ # تهيئة المخاطر المحتملة
+ self.risk_categories = [
+ "مخاطر مالية",
+ "مخاطر زمنية",
+ "مخاطر فنية",
+ "مخاطر إدارية",
+ "مخاطر تنظيمية",
+ "مخاطر سوقية",
+ "مخاطر تعاقدية"
+ ]
+
+ self.impact_levels = ["منخفض", "متوسط", "عالي"]
+ self.probability_levels = ["غير محتمل", "محتمل", "مؤكد"]
+
+ def render(self):
+ """عرض واجهة وحدة تحليل المخاطر"""
+
+ st.markdown("
وحدة تحليل المخاطر
", unsafe_allow_html=True)
+
+ tabs = st.tabs([
+ "تحليل المخاطر",
+ "سجل المخاطر",
+ "مصفوفة المخاطر",
+ "خطة الاستجابة للمخاطر"
+ ])
+
+ with tabs[0]:
+ self._render_risk_analysis_tab()
+
+ with tabs[1]:
+ self._render_risk_register_tab()
+
+ with tabs[2]:
+ self._render_risk_matrix_tab()
+
+ with tabs[3]:
+ self._render_risk_response_tab()
+
+ def _render_risk_analysis_tab(self):
+ """عرض تبويب تحليل المخاطر"""
+
+ st.markdown("### تحليل المخاطر")
+
+ # التحقق من وجود مشروع حالي
+ if 'current_project' not in st.session_state or st.session_state.current_project is None:
+ # إذا لم يكن هناك مشروع محدد، اعرض قائمة باختيار المشروع
+ if 'projects' in st.session_state and st.session_state.projects:
+ project_names = [p['name'] for p in st.session_state.projects]
+ selected_project_name = st.selectbox("اختر المشروع", project_names)
+
+ if selected_project_name:
+ selected_project = next((p for p in st.session_state.projects if p['name'] == selected_project_name), None)
+ if selected_project:
+ st.session_state.current_project = selected_project
+ else:
+ st.warning("لم يتم العثور على المشروع المحدد.")
+ return
+ else:
+ st.info("يرجى اختيار مشروع لتحليل مخاطره.")
+ return
+ else:
+ st.warning("لا توجد مشاريع متاحة. يرجى إنشاء مشروع جديد أولاً.")
+ return
+
+ # عرض معلومات المشروع
+ project = st.session_state.current_project
+
+ col1, col2, col3 = st.columns(3)
+ with col1:
+ st.metric("اسم المشروع", project['name'])
+ with col2:
+ st.metric("رقم المناقصة", project['number'])
+ with col3:
+ st.metric("الجهة المالكة", project['client'])
+
+ # التحقق من وجود سجل المخاطر للمشروع
+ if 'risks' not in project:
+ project['risks'] = []
+
+ # نموذج إضافة مخاطر
+ with st.form("add_risk_form"):
+ st.markdown("#### إضافة مخاطرة جديدة")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ risk_code = st.text_input("رمز المخاطرة", f"R{len(project['risks']) + 1}")
+ risk_category = st.selectbox("فئة المخاطرة", self.risk_categories)
+ impact = st.select_slider("التأثير", self.impact_levels, value="متوسط")
+
+ with col2:
+ risk_description = st.text_area("وصف المخاطرة", height=80)
+ probability = st.select_slider("الاحتمالية", self.probability_levels, value="محتمل")
+ response_strategy = st.text_area("استراتيجية الاستجابة", height=80)
+
+ submitted = st.form_submit_button("إضافة المخاطرة")
+
+ if submitted:
+ # التحقق من تعبئة الحقول الإلزامية
+ if not risk_description:
+ st.error("يرجى إدخال وصف المخاطرة.")
+ else:
+ # إنشاء مخاطرة جديدة
+ new_risk = {
+ 'id': len(project['risks']) + 1,
+ 'risk_code': risk_code,
+ 'description': risk_description,
+ 'category': risk_category,
+ 'impact': impact,
+ 'probability': probability,
+ 'response_strategy': response_strategy,
+ 'status': "نشط",
+ 'created_at': datetime.now().strftime('%Y-%m-%d'),
+ 'risk_score': self._calculate_risk_score(impact, probability)
+ }
+
+ # إضافة المخاطرة إلى سجل المخاطر
+ project['risks'].append(new_risk)
+
+ st.success(f"تمت إضافة المخاطرة [{risk_code}] بنجاح!")
+ st.balloons()
+
+ # خيارات تحليل المخاطر
+ st.markdown("#### خيارات تحليل المخاطر")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ automated_analysis = st.button("تحليل تلقائي للمخاطر")
+
+ with col2:
+ from_document_analysis = st.button("استيراد المخاطر من تحليل المستندات")
+
+ if automated_analysis:
+ with st.spinner("جاري تحليل المخاطر..."):
+ time.sleep(2)
+ self._generate_automated_risks(project)
+ st.success("تم تحليل المخاطر بنجاح!")
+ st.balloons()
+
+ if from_document_analysis:
+ with st.spinner("جاري استيراد المخاطر من تحليل المستندات..."):
+ time.sleep(2)
+
+ # هذه مجرد محاكاة، في الواقع يجب استدعاء الوظيفة الفعلية لاستيراد المخاطر
+ document_risks = self._get_risks_from_documents()
+
+ if document_risks:
+ existing_risk_codes = [r['risk_code'] for r in project['risks']]
+
+ for risk in document_risks:
+ # تجنب تكرار المخاطر
+ if risk['risk_code'] not in existing_risk_codes:
+ project['risks'].append(risk)
+
+ st.success(f"تم استيراد {len(document_risks)} مخاطرة من تحليل المستندات!")
+ else:
+ st.warning("لم يتم العثور على مخاطر في المستندات.")
+
+ # عرض ملخص المخاطر
+ if project['risks']:
+ self._show_risk_summary(project['risks'])
+
+ def _render_risk_register_tab(self):
+ """عرض تبويب سجل المخاطر"""
+
+ st.markdown("### سجل المخاطر")
+
+ # التحقق من وجود مشروع حالي
+ if 'current_project' not in st.session_state or st.session_state.current_project is None:
+ st.info("يرجى اختيار مشروع من تبويب تحليل المخاطر أولاً.")
+ return
+
+ project = st.session_state.current_project
+
+ if 'risks' not in project or not project['risks']:
+ st.info("لا توجد مخاطر مسجلة لهذا المشروع. يمكنك إضافة مخاطر من تبويب تحليل المخاطر.")
+ return
+
+ # فلترة سجل المخاطر
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ search_term = st.text_input("البحث في سجل المخاطر")
+
+ with col2:
+ category_filter = st.multiselect("فلترة حسب الفئة", self.risk_categories)
+
+ with col3:
+ impact_filter = st.multiselect("فلترة حسب التأثير", self.impact_levels)
+
+ # تطبيق الفلترة
+ filtered_risks = project['risks']
+
+ if search_term:
+ filtered_risks = [r for r in filtered_risks if search_term.lower() in r.get('description', '').lower()]
+
+ if category_filter:
+ filtered_risks = [r for r in filtered_risks if r.get('category') in category_filter]
+
+ if impact_filter:
+ filtered_risks = [r for r in filtered_risks if r.get('impact') in impact_filter]
+
+ # عرض سجل المخاطر
+ if filtered_risks:
+ # تحويل المخاطر إلى DataFrame
+ risk_df = pd.DataFrame(filtered_risks)
+
+ # تحديد الأعمدة المراد عرضها وترتيبها
+ display_columns = [
+ 'risk_code', 'description', 'category', 'impact',
+ 'probability', 'risk_score', 'status'
+ ]
+
+ # تغيير أسماء الأعمدة للعرض
+ column_names = {
+ 'risk_code': 'رمز المخاطرة',
+ 'description': 'وصف المخاطرة',
+ 'category': 'الفئة',
+ 'impact': 'التأثير',
+ 'probability': 'الاحتمالية',
+ 'risk_score': 'درجة المخاطرة',
+ 'status': 'الحالة',
+ 'response_strategy': 'استراتيجية الاستجابة',
+ 'created_at': 'تاريخ الإنشاء'
+ }
+
+ # إعداد DataFrame للعرض
+ if 'response_strategy' in risk_df.columns:
+ display_columns.append('response_strategy')
+
+ if 'created_at' in risk_df.columns:
+ display_columns.append('created_at')
+
+ # الحصول على الأعمدة المتوفرة فقط
+ available_columns = [col for col in display_columns if col in risk_df.columns]
+
+ if available_columns:
+ display_df = risk_df[available_columns].rename(columns=column_names)
+
+ # عرض الجدول
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
+
+ # أزرار العمليات
+ col1, col2 = st.columns(2)
+
+ with col1:
+ if st.button("تصدير سجل المخاطر إلى Excel"):
+ st.success("تم تصدير سجل المخاطر بنجاح!")
+
+ with col2:
+ if st.button("طباعة تقرير المخاطر"):
+ st.success("تم إنشاء تقرير المخاطر بنجاح!")
+ else:
+ st.warning("هناك مشكلة في بنية بيانات المخاطر. يرجى التحقق من سلامة البيانات.")
+ else:
+ st.info("لا توجد مخاطر تطابق معايير البحث.")
+
+ def _render_risk_matrix_tab(self):
+ """عرض تبويب مصفوفة المخاطر"""
+
+ st.markdown("### مصفوفة المخاطر")
+
+ # التحقق من وجود مشروع حالي
+ if 'current_project' not in st.session_state or st.session_state.current_project is None:
+ st.info("يرجى اختيار مشروع من تبويب تحليل المخاطر أولاً.")
+ return
+
+ project = st.session_state.current_project
+
+ if 'risks' not in project or not project['risks']:
+ st.info("لا توجد مخاطر مسجلة لهذا المشروع. يمكنك إضافة مخاطر من تبويب تحليل المخاطر.")
+ return
+
+ # إنشاء ضبط مصفوفة المخاطر (3×3)
+ impact_values = {"منخفض": 1, "متوسط": 2, "عالي": 3}
+ probability_values = {"غير محتمل": 1, "محتمل": 2, "مؤكد": 3}
+
+ # إنشاء DataFrame لتمثيل مصفوفة المخاطر
+ matrix_data = []
+
+ for p in probability_values.keys():
+ for i in impact_values.keys():
+ p_value = probability_values[p]
+ i_value = impact_values[i]
+ risk_score = p_value * i_value
+
+ # تحديد اللون حسب درجة المخاطرة
+ if risk_score <= 2:
+ color = 'green' # منخفضة
+ elif risk_score <= 6:
+ color = 'orange' # متوسطة
+ else:
+ color = 'red' # عالية
+
+ # استخراج المخاطر التي تقع في هذه الخلية
+ cell_risks = [r for r in project['risks'] if r.get('impact') == i and r.get('probability') == p]
+
+ # إضافة بيانات الخلية
+ matrix_data.append({
+ 'احتمالية': p,
+ 'تأثير': i,
+ 'درجة_المخاطرة': risk_score,
+ 'عدد_المخاطر': len(cell_risks),
+ 'المخاطر': [r.get('risk_code') for r in cell_risks],
+ 'لون': color
+ })
+
+ # تحويل إلى DataFrame
+ matrix_df = pd.DataFrame(matrix_data)
+
+ # رسم مصفوفة المخاطر باستخدام Plotly
+ fig = go.Figure()
+
+ for index, row in matrix_df.iterrows():
+ # إنشاء نص الخلية
+ if row['عدد_المخاطر'] > 0:
+ cell_text = f"{', '.join(row['المخاطر'])} ({row['عدد_المخاطر']} مخاطر)"
+ else:
+ cell_text = ''
+
+ # إنشاء خلية المصفوفة
+ fig.add_trace(go.Scatter(
+ x=[row['تأثير']],
+ y=[row['احتمالية']],
+ mode='markers+text',
+ marker=dict(
+ color=row['لون'],
+ size=20 + (row['عدد_المخاطر'] * 5),
+ opacity=0.8
+ ),
+ text=cell_text,
+ textposition="middle center",
+ name=f"{row['احتمالية']} - {row['تأثير']}"
+ ))
+
+ # تكوين المحاور
+ fig.update_layout(
+ title="مصفوفة المخاطر (الاحتمالية × التأثير)",
+ xaxis=dict(
+ title="التأثير",
+ tickmode='array',
+ tickvals=[1, 2, 3],
+ ticktext=["منخفض", "متوسط", "عالي"],
+ gridcolor='lightgray'
+ ),
+ yaxis=dict(
+ title="الاحتمالية",
+ tickmode='array',
+ tickvals=[1, 2, 3],
+ ticktext=["غير محتمل", "محتمل", "مؤكد"],
+ gridcolor='lightgray'
+ ),
+ height=600
+ )
+
+ # عرض المصفوفة
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض توزيع المخاطر حسب الفئة
+ st.markdown("#### توزيع المخاطر حسب الفئة")
+
+ # حساب عدد المخاطر في كل فئة
+ category_counts = {}
+ for r in project['risks']:
+ category = r.get('category', 'أخرى')
+ category_counts[category] = category_counts.get(category, 0) + 1
+
+ # إنشاء DataFrame
+ category_df = pd.DataFrame({
+ 'الفئة': list(category_counts.keys()),
+ 'عدد المخاطر': list(category_counts.values())
+ })
+
+ # رسم مخطط دائري
+ fig = px.pie(
+ category_df,
+ values='عدد المخاطر',
+ names='الفئة',
+ title='توزيع المخاطر حسب الفئة',
+ hole=0.4
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ def _render_risk_response_tab(self):
+ """عرض تبويب خطة الاستجابة للمخاطر"""
+
+ st.markdown("### خطة الاستجابة للمخاطر")
+
+ # التحقق من وجود مشروع حالي
+ if 'current_project' not in st.session_state or st.session_state.current_project is None:
+ st.info("يرجى اختيار مشروع من تبويب تحليل المخاطر أولاً.")
+ return
+
+ project = st.session_state.current_project
+
+ if 'risks' not in project or not project['risks']:
+ st.info("لا توجد مخاطر مسجلة لهذا المشروع. يمكنك إضافة مخاطر من تبويب تحليل المخاطر.")
+ return
+
+ # ترتيب المخاطر حسب درجة المخاطرة (من الأعلى إلى الأقل)
+ sorted_risks = sorted(project['risks'], key=lambda x: x.get('risk_score', 0), reverse=True)
+
+ # عرض خطة الاستجابة للمخاطر
+ for i, risk in enumerate(sorted_risks):
+ with st.expander(f"{risk.get('risk_code', '')}: {risk.get('description', 'بدون وصف')}", expanded=(i < 3)):
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.markdown(f"**الفئة**: {risk.get('category', 'غير محدد')}")
+ st.markdown(f"**التأثير**: {risk.get('impact', 'غير محدد')}")
+
+ with col2:
+ st.markdown(f"**الاحتمالية**: {risk.get('probability', 'غير محدد')}")
+ st.markdown(f"**درجة المخاطرة**: {risk.get('risk_score', 'غير محدد')}")
+
+ with col3:
+ st.markdown(f"**الحالة**: {risk.get('status', 'نشط')}")
+ risk_owner = risk.get('risk_owner', 'غير محدد')
+ st.markdown(f"**مسؤول المخاطرة**: {risk_owner}")
+
+ st.markdown("---")
+ st.markdown("#### استراتيجية الاستجابة")
+ current_strategy = risk.get('response_strategy', '')
+ new_strategy = st.text_area(f"استراتيجية الاستجابة للمخاطرة {risk.get('risk_code', '')}",
+ value=current_strategy,
+ height=100,
+ key=f"strategy_{risk.get('risk_code', '')}")
+
+ # تحديث استراتيجية الاستجابة إذا تم تغييرها
+ if new_strategy != current_strategy:
+ risk['response_strategy'] = new_strategy
+
+ st.markdown("#### إجراءات التحكم")
+ control_measures = risk.get('control_measures', [])
+
+ if control_measures:
+ for j, measure in enumerate(control_measures):
+ st.markdown(f"{j+1}. {measure}")
+ else:
+ st.info("لم يتم تعريف إجراءات تحكم لهذه المخاطرة.")
+
+ # إضافة إجراء تحكم جديد
+ new_measure = st.text_input(f"إجراء تحكم جديد للمخاطرة {risk.get('risk_code', '')}",
+ key=f"measure_{risk.get('risk_code', '')}")
+
+ if st.button(f"إضافة إجراء", key=f"add_measure_{risk.get('risk_code', '')}"):
+ if new_measure:
+ if 'control_measures' not in risk:
+ risk['control_measures'] = []
+
+ risk['control_measures'].append(new_measure)
+ st.success(f"تم إضافة إجراء التحكم بنجاح!")
+ st.rerun()
+ else:
+ st.error("يرجى إدخال إجراء التحكم.")
+
+ # زر تصدير خطة الاستجابة للمخاطر
+ if st.button("تصدير خطة الاستجابة للمخاطر"):
+ st.success("تم تصدير خطة الاستجابة للمخاطر بنجاح!")
+
+ def _calculate_risk_score(self, impact, probability):
+ """حساب درجة المخاطرة بناءً على التأثير والاحتمالية"""
+ impact_values = {"منخفض": 1, "متوسط": 2, "عالي": 3}
+ probability_values = {"غير محتمل": 1, "محتمل": 2, "مؤكد": 3}
+
+ impact_value = impact_values.get(impact, 1)
+ probability_value = probability_values.get(probability, 1)
+
+ return impact_value * probability_value
+
+ def _generate_automated_risks(self, project):
+ """توليد مخاطر تلقائية بناءً على خصائص المشروع"""
+
+ # قائمة المخاطر الشائعة في مشاريع المقاولات
+ common_risks = [
+ {
+ 'risk_code': 'RF01',
+ 'description': 'غرامة تأخير مرتفعة (10% من قيمة العقد)',
+ 'category': 'مخاطر مالية',
+ 'impact': 'عالي',
+ 'probability': 'محتمل',
+ 'response_strategy': 'تخصيص مبلغ احتياطي للغرامات المحتملة ووضع خطة لإدارة الجدول الزمني بشكل فعال',
+ 'status': 'نشط',
+ 'risk_score': 6
+ },
+ {
+ 'risk_code': 'RF02',
+ 'description': 'متطلبات ضمان بنكي مرتفعة (15% من قيمة العقد)',
+ 'category': 'مخاطر مالية',
+ 'impact': 'متوسط',
+ 'probability': 'مؤكد',
+ 'response_strategy': 'التفاوض مع العميل لتخفيض نسبة الضمان البنكي أو تقسيمه على مراحل المشروع',
+ 'status': 'نشط',
+ 'risk_score': 6
+ },
+ {
+ 'risk_code': 'RF03',
+ 'description': 'شروط دفع متأخرة (60 يوم)',
+ 'category': 'مخاطر مالية',
+ 'impact': 'متوسط',
+ 'probability': 'مؤكد',
+ 'response_strategy': 'التخطيط للتدفق النقدي مع الأخذ بالاعتبار تأخر الدفعات وتأمين خط ائتمان احتياطي',
+ 'status': 'نشط',
+ 'risk_score': 6
+ },
+ {
+ 'risk_code': 'RT01',
+ 'description': 'مدة تنفيذ قصيرة (12 شهر)',
+ 'category': 'مخاطر زمنية',
+ 'impact': 'عالي',
+ 'probability': 'محتمل',
+ 'response_strategy': 'زيادة فريق العمل واستخدام موارد إضافية مع وضع خطة عمل تفصيلية ومراقبتها أسبوعياً',
+ 'status': 'نشط',
+ 'risk_score': 6
+ },
+ {
+ 'risk_code': 'RT02',
+ 'description': 'احتمالية تأخر توريد المواد الرئيسية',
+ 'category': 'مخاطر زمنية',
+ 'impact': 'عالي',
+ 'probability': 'محتمل',
+ 'response_strategy': 'تحديد المواد ذات فترات التوريد الطويلة وطلبها مبكراً مع التعاقد مع موردين بدلاء',
+ 'status': 'نشط',
+ 'risk_score': 6
+ },
+ {
+ 'risk_code': 'RTE01',
+ 'description': 'غموض في بعض المواصفات الفنية',
+ 'category': 'مخاطر فنية',
+ 'impact': 'متوسط',
+ 'probability': 'محتمل',
+ 'response_strategy': 'طلب توضيح من العميل قبل البدء بالتنفيذ وتوثيق جميع الردود والتوضيحات',
+ 'status': 'نشط',
+ 'risk_score': 4
+ },
+ {
+ 'risk_code': 'RTE02',
+ 'description': 'تضارب بين المخططات والمواصفات',
+ 'category': 'مخاطر فنية',
+ 'impact': 'متوسط',
+ 'probability': 'محتمل',
+ 'response_strategy': 'مراجعة شاملة للمستندات وتوثيق التضاربات وطلب توضيح من العميل',
+ 'status': 'نشط',
+ 'risk_score': 4
+ },
+ {
+ 'risk_code': 'RM01',
+ 'description': 'عدم وضوح آلية استلام الأعمال',
+ 'category': 'مخاطر إدارية',
+ 'impact': 'منخفض',
+ 'probability': 'محتمل',
+ 'response_strategy': 'طلب توضيح آلية الاستلام من العميل ووضع إجراءات داخلية للتحقق من جودة الأعمال قبل التقديم للاستلام',
+ 'status': 'نشط',
+ 'risk_score': 2
+ },
+ {
+ 'risk_code': 'RR01',
+ 'description': 'شروط تعجيزية للمحتوى المحلي',
+ 'category': 'مخاطر تنظيمية',
+ 'impact': 'عالي',
+ 'probability': 'محتمل',
+ 'response_strategy': 'دراسة متطلبات المحتوى المحلي بدقة ووضع خطة لتحقيقها مع الاحتفاظ بسجلات التوثيق اللازمة',
+ 'status': 'نشط',
+ 'risk_score': 6
+ },
+ {
+ 'risk_code': 'RM01',
+ 'description': 'خطر التغييرات في أسعار المواد',
+ 'category': 'مخاطر سوقية',
+ 'impact': 'عالي',
+ 'probability': 'محتمل',
+ 'response_strategy': 'تثبيت أسعار المواد الرئيسية مع الموردين وإدراج بند تعديل الأسعار في العقد',
+ 'status': 'نشط',
+ 'risk_score': 6
+ },
+ {
+ 'risk_code': 'RC01',
+ 'description': 'عدم وضوح بعض بنود العقد',
+ 'category': 'مخاطر تعاقدية',
+ 'impact': 'متوسط',
+ 'probability': 'محتمل',
+ 'response_strategy': 'مراجعة العقد من قبل مستشار قانوني متخصص وطلب توضيح للبنود الغامضة قبل التوقيع',
+ 'status': 'نشط',
+ 'risk_score': 4
+ }
+ ]
+
+ # إضافة المخاطر الشائعة إلى المشروع
+ existing_risk_codes = [r['risk_code'] for r in project['risks']]
+
+ for risk in common_risks:
+ # تجنب تكرار المخاطر
+ if risk['risk_code'] not in existing_risk_codes:
+ risk['id'] = len(project['risks']) + 1
+ risk['created_at'] = datetime.now().strftime('%Y-%m-%d')
+ project['risks'].append(risk)
+
+ def _get_risks_from_documents(self):
+ """استيراد المخاطر من تحليل المستندات"""
+
+ # محاكاة لاستيراد المخاطر من تحليل المستندات
+ # في التطبيق الفعلي، يجب استدعاء الوظيفة المناسبة من وحدة تحليل المستندات
+
+ document_risks = [
+ {
+ 'risk_code': 'RD01',
+ 'description': 'غرامة تأخير مرتفعة تصل إلى 20% من قيمة العقد',
+ 'category': 'مخاطر مالية',
+ 'impact': 'عالي',
+ 'probability': 'مؤكد',
+ 'response_strategy': 'التفاوض على تخفيض الغرامة أو تقسيمها حسب مراحل المشروع مع وضع خطة محكمة للجدول الزمني',
+ 'status': 'نشط',
+ 'risk_score': 9,
+ 'created_at': datetime.now().strftime('%Y-%m-%d')
+ },
+ {
+ 'risk_code': 'RD02',
+ 'description': 'يحق للمالك إيقاف المشروع لمدة تصل إلى 90 يوم دون تعويض',
+ 'category': 'مخاطر تعاقدية',
+ 'impact': 'عالي',
+ 'probability': 'محتمل',
+ 'response_strategy': 'طلب إضافة بند للتعويض عن التكاليف الإضافية الناتجة عن الإيقاف لفترات طويلة',
+ 'status': 'نشط',
+ 'risk_score': 6,
+ 'created_at': datetime.now().strftime('%Y-%m-%d')
+ },
+ {
+ 'risk_code': 'RD03',
+ 'description': 'تحمل المقاول مسؤولية استخراج جميع التصاريح الحكومية',
+ 'category': 'مخاطر تنظيمية',
+ 'impact': 'متوسط',
+ 'probability': 'مؤكد',
+ 'response_strategy': 'حصر جميع التصاريح المطلوبة والبدء في إجراءات استخراجها مبكراً مع تخصيص فريق لمتابعتها',
+ 'status': 'نشط',
+ 'risk_score': 6,
+ 'created_at': datetime.now().strftime('%Y-%m-%d')
+ },
+ {
+ 'risk_code': 'RD04',
+ 'description': 'شروط الدفعة المقدمة مقيدة بضمان بنكي بقيمة 120% من قيمة الدفعة',
+ 'category': 'مخاطر مالية',
+ 'impact': 'متوسط',
+ 'probability': 'مؤكد',
+ 'response_strategy': 'التفاوض على خفض نسبة الضمان البنكي أو تقديم ضمان شركة بدلاً من الضمان البنكي',
+ 'status': 'نشط',
+ 'risk_score': 6,
+ 'created_at': datetime.now().strftime('%Y-%m-%d')
+ }
+ ]
+
+ return document_risks
+
+ def _show_risk_summary(self, risks):
+ """عرض ملخص المخاطر"""
+
+ st.markdown("#### ملخص المخاطر")
+
+ # حساب إحصائيات المخاطر
+ total_risks = len(risks)
+ risk_levels = {
+ 'عالية': len([r for r in risks if r.get('risk_score', 0) >= 6]),
+ 'متوسطة': len([r for r in risks if 3 <= r.get('risk_score', 0) < 6]),
+ 'منخفضة': len([r for r in risks if r.get('risk_score', 0) < 3])
+ }
+
+ # عرض الإحصائيات
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ st.metric("إجمالي المخاطر", total_risks)
+
+ with col2:
+ st.metric("المخاطر العالية", risk_levels['عالية'], delta=f"{risk_levels['عالية']/total_risks*100:.1f}%", delta_color="inverse")
+
+ with col3:
+ st.metric("المخاطر المتوسطة", risk_levels['متوسطة'], delta=f"{risk_levels['متوسطة']/total_risks*100:.1f}%", delta_color="off")
+
+ with col4:
+ st.metric("المخاطر المنخفضة", risk_levels['منخفضة'], delta=f"{risk_levels['منخفضة']/total_risks*100:.1f}%", delta_color="normal")
+
+ # عرض الرسم البياني للمخاطر
+ risk_level_df = pd.DataFrame({
+ 'مستوى المخاطرة': list(risk_levels.keys()),
+ 'عدد المخاطر': list(risk_levels.values())
+ })
+
+ fig = px.bar(
+ risk_level_df,
+ x='مستوى المخاطرة',
+ y='عدد المخاطر',
+ color='مستوى المخاطرة',
+ color_discrete_map={
+ 'عالية': 'red',
+ 'متوسطة': 'orange',
+ 'منخفضة': 'green'
+ },
+ title='توزيع المخاطر حسب المستوى'
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+ # عرض أعلى 5 مخاطر من حيث درجة المخاطرة
+ st.markdown("#### أعلى 5 مخاطر")
+
+ # ترتيب المخاطر حسب درجة المخاطرة
+ sorted_risks = sorted(risks, key=lambda x: x.get('risk_score', 0), reverse=True)
+ top_risks = sorted_risks[:5]
+
+ # إنشاء DataFrame للعرض
+ if top_risks:
+ top_risks_data = []
+
+ for r in top_risks:
+ top_risks_data.append({
+ 'رمز المخاطرة': r.get('risk_code', ''),
+ 'وصف المخاطرة': r.get('description', ''),
+ 'الفئة': r.get('category', ''),
+ 'التأثير': r.get('impact', ''),
+ 'الاحتمالية': r.get('probability', ''),
+ 'درجة المخاطرة': r.get('risk_score', 0)
+ })
+
+ top_risks_df = pd.DataFrame(top_risks_data)
+ st.dataframe(top_risks_df, use_container_width=True, hide_index=True)
diff --git a/modules/risk_assessment/__init__.py b/modules/risk_assessment/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a0af0cba3e41d42a6aff7f8b31f4a81b3ecdfdb
--- /dev/null
+++ b/modules/risk_assessment/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+"""
+وحدة تقييم مخاطر العقود الآلي
+"""
\ No newline at end of file
diff --git a/modules/risk_assessment/contract_risk_analyzer.py b/modules/risk_assessment/contract_risk_analyzer.py
new file mode 100644
index 0000000000000000000000000000000000000000..acf6398e185ff827b156e9ee43629c65c5a77b49
--- /dev/null
+++ b/modules/risk_assessment/contract_risk_analyzer.py
@@ -0,0 +1,2055 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+وحدة تحليل وتقييم مخاطر العقود بشكل آلي
+"""
+
+import os
+import sys
+import json
+import datetime
+import re
+import numpy as np
+import pandas as pd
+import streamlit as st
+import plotly.express as px
+import plotly.graph_objects as go
+from sklearn.feature_extraction.text import TfidfVectorizer
+from sklearn.cluster import KMeans
+from collections import Counter
+
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
+
+# استيراد المكونات المساعدة
+from utils.helpers import create_directory_if_not_exists, format_time, get_user_info
+
+# تعليق استيراد Anthropic (سيتم تنفيذه لاحقًا بعد ضبط ملف anthropic)
+# نستخدم نمط fallback للتعامل مع الخطأ
+try:
+ from anthropic import Anthropic
+
+ def analyzeBillOfQuantities(text):
+ return {"analysis": "تحليل فرضي لجدول الكميات", "items": [], "summary": "لا يوجد تحليل حقيقي متاح حاليًا"}
+
+ def analyzeTermsAndConditions(text):
+ return {"analysis": "تحليل فرضي للشروط والأحكام", "risks": [], "summary": "لا يوجد تحليل حقيقي متاح حاليًا"}
+
+ anthropic = None # سيتم تعيينه لاحقًا عند الحاجة
+except ImportError:
+ # في حالة عدم وجود مكتبة أنثروبيك، نستخدم دوال فرضية
+ def analyzeBillOfQuantities(text):
+ return {"analysis": "تحليل فرضي لجدول الكميات", "items": [], "summary": "لا يوجد تحليل حقيقي متاح حاليًا"}
+
+ def analyzeTermsAndConditions(text):
+ return {"analysis": "تحليل فرضي للشروط والأحكام", "risks": [], "summary": "لا يوجد تحليل حقيقي متاح حاليًا"}
+
+ anthropic = None
+
+
+class ContractRiskAnalyzer:
+ """فئة تحليل وتقييم المخاطر في العقود بشكل آلي"""
+
+ def __init__(self):
+ """تهيئة محلل مخاطر العقود"""
+ self.risk_data_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'risk_assessment')
+ create_directory_if_not_exists(self.risk_data_dir)
+
+ # تعريف أنواع المخاطر
+ self.risk_categories = {
+ "legal": "قانونية",
+ "financial": "مالية",
+ "operational": "تشغيلية",
+ "technical": "فنية",
+ "compliance": "امتثال",
+ "environmental": "بيئية",
+ "safety": "سلامة",
+ "schedule": "جدولة",
+ "resource": "موارد",
+ "quality": "جودة",
+ "scope": "نطاق العمل",
+ "stakeholder": "أصحاب المصلحة",
+ "commercial": "تجارية",
+ "contractual": "تعاقدية",
+ "regulatory": "تنظيمية"
+ }
+
+ # تعريف قائمة المصطلحات الخطرة في العقود
+ self.risky_terms = {
+ "legal": [
+ "تعديل العقد", "فسخ العقد", "إنهاء الاتفاقية", "فض المنازعات", "شرط جزائي",
+ "تحكيم", "قاهرة", "ظروف قاهرة", "التقاضي", "الإخلال بالعقد", "المنازعات",
+ "الدفع", "الضمان", "الولاية القضائية", "التعويض"
+ ],
+ "financial": [
+ "غرامة تأخير", "غرامات", "دفعة مقدمة", "دفعة نهائية", "ضمان", "تأمين",
+ "تسعير", "سعر", "خصم", "تكاليف إضافية", "تعديل سعر", "زيادة سعر",
+ "خسارة", "ربح", "هامش", "تحمل التكاليف", "تمويل", "مخاطر مالية", "ضريبة"
+ ],
+ "operational": [
+ "تأخير", "عدم التسليم", "توقف", "انقطاع", "عطل", "خلل", "تعطل",
+ "عمالة", "أيدي عاملة", "موارد بشرية", "مناولة", "تصاريح", "لوجستيك",
+ "مخزون", "سلسلة توريد", "عمليات"
+ ],
+ "technical": [
+ "مواصفات", "معايير", "شروط فنية", "كفاءة", "جودة", "أداء",
+ "اختبار", "فحص", "تقنية", "تكنولوجيا", "تشغيل", "تركيب", "صيانة",
+ "تصميم", "هندسة", "قدرة"
+ ],
+ "compliance": [
+ "اللوائح", "القوانين", "التشريعات", "الامتثال", "المعايير", "الترخيص",
+ "التصريح", "الموافقة", "الالتزام", "التنظيم", "الشهادة"
+ ],
+ "environmental": [
+ "بيئي", "بيئة", "تلوث", "تصريف", "نفايات", "انبعاثات", "موارد طبيعية",
+ "تأثير بيئي", "استدامة", "تعويض بيئي", "ضرر بيئي", "مخلفات"
+ ],
+ "safety": [
+ "سلامة", "أمان", "حوادث", "إصابات", "مخاطر صحية", "صحة مهنية",
+ "وقاية", "حماية", "إجراءات أمان", "تأمين سلامة", "مخاطر السلامة"
+ ],
+ "schedule": [
+ "تأخير", "تمديد", "مدة", "جدول زمني", "موعد نهائي", "تسليم",
+ "مراحل", "مواعيد", "وقت", "فترة", "عاجل", "سريع", "فوري"
+ ],
+ "resource": [
+ "مواد", "معدات", "أدوات", "آلات", "عمالة", "كوادر", "فريق",
+ "موارد بشرية", "توفير", "تأمين", "استقدام", "نقص", "عجز", "كفاية"
+ ],
+ "quality": [
+ "جودة", "ضمان الجودة", "معايير", "مواصفات", "أداء", "رداءة",
+ "ضعف", "خلل", "عيب", "إصلاح", "صيانة", "استبدال", "رفض"
+ ],
+ "scope": [
+ "نطاق العمل", "تغيير النطاق", "توسيع", "تقليص", "تعديل", "إضافة",
+ "أعمال إضافية", "تغييرات", "أوامر تغيير", "متطلبات جديدة"
+ ],
+ "stakeholder": [
+ "طرف ثالث", "مالك", "عميل", "المقاول", "المورد", "الاستشاري",
+ "المشرف", "مدير المشروع", "المقاول من الباطن", "الشريك", "مصلحة"
+ ],
+ "commercial": [
+ "منافسة", "سوق", "سعر", "عرض", "طلب", "تجاري", "أعمال",
+ "استثمار", "عائد", "ربح", "خسارة", "سمعة", "علامة تجارية"
+ ],
+ "contractual": [
+ "بند", "شرط", "مادة", "اتفاقية", "عقد", "ملحق", "تعديل",
+ "تنازل", "تعهد", "التزام", "مسؤولية", "واجب", "حق"
+ ],
+ "regulatory": [
+ "تنظيمي", "حكومي", "رسمي", "لائحة", "قانون", "تشريع",
+ "ترخيص", "تصريح", "موافقة", "امتثال", "اشتراطات", "متطلبات"
+ ]
+ }
+
+ # نموذج تصنيف المخاطر
+ self.vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')
+ self.kmeans = KMeans(n_clusters=len(self.risk_categories), random_state=42)
+
+ # الصيغ والمصطلحات المتعلقة بالمخاطر التعاقدية
+ self.contract_risk_patterns = {
+ "unlimited_liability": [
+ r"مسؤولية غير محدودة",
+ r"مسؤولية كاملة",
+ r"المسؤولية الكاملة",
+ r"دون تحديد للمسؤولية",
+ r"دون سقف للمسؤولية",
+ r"المسؤولية المطلقة",
+ r"التعويض عن كافة الأضرار"
+ ],
+ "payment_delay": [
+ r"(\d+)\s*يوم\s*من تاريخ\s*الفاتورة",
+ r"(\d+)\s*يوم\s*عمل من تاريخ\s*الفاتورة",
+ r"(\d+)\s*يوم\s*للدفع",
+ r"خلال\s*(\d+)\s*يوم",
+ r"الدفع خلال\s*(\d+)\s*"
+ ],
+ "excessive_penalties": [
+ r"غرامة تأخير بنسبة\s*(\d+)%",
+ r"غرامة تأخير قدرها\s*(\d+)%",
+ r"غرامة يومية\s*(\d+)%",
+ r"غرامة اسبوعية\s*(\d+)%",
+ r"غرامة شهرية\s*(\d+)%"
+ ],
+ "unilateral_termination": [
+ r"يحق للطرف الأول إنهاء العقد",
+ r"يحق للعميل إنهاء العقد",
+ r"للعميل الحق في إنهاء",
+ r"للطرف الأول الحق في إنهاء",
+ r"إنهاء العقد من طرف واحد",
+ r"إنهاء دون إبداء أسباب"
+ ],
+ "unrealistic_deadlines": [
+ r"التسليم خلال\s*(\d+)\s*يوم",
+ r"مدة التنفيذ\s*(\d+)\s*يوم",
+ r"الانتهاء خلال\s*(\d+)\s*يوم",
+ r"إنجاز المشروع خلال\s*(\d+)\s*أسبوع"
+ ],
+ "scope_creep": [
+ r"أعمال إضافية",
+ r"تعديلات على النطاق",
+ r"توسيع نطاق العمل",
+ r"إضافة متطلبات",
+ r"تغيير المواصفات",
+ r"أعمال غير متوقعة"
+ ],
+ "indemnification": [
+ r"تعويض الطرف الأول",
+ r"تعويض العميل",
+ r"تعويض كامل",
+ r"تعويض شامل",
+ r"التعويض عن كافة الأضرار",
+ r"التعويض عن أي خسائر"
+ ],
+ "change_control": [
+ r"التغييرات بدون تكلفة إضافية",
+ r"تعديلات دون زيادة السعر",
+ r"تغييرات دون مقابل",
+ r"تعديلات لا تؤثر على السعر"
+ ],
+ "warranty_period": [
+ r"ضمان لمدة\s*(\d+)\s*شهر",
+ r"ضمان لمدة\s*(\d+)\s*سنة",
+ r"فترة ضمان\s*(\d+)\s*شهر",
+ r"فترة الضمان\s*(\d+)\s*شهر",
+ r"فترة الصيانة\s*(\d+)\s*شهر"
+ ],
+ "dispute_resolution": [
+ r"المحاكم المختصة",
+ r"محاكم[^.]*للنظر في المنازعات",
+ r"تسوية النزاعات",
+ r"فض المنازعات",
+ r"التحكيم",
+ r"لجنة تحكيم"
+ ],
+ "force_majeure": [
+ r"القوة القاهرة",
+ r"الظروف القاهرة",
+ r"ظروف خارجة عن الإرادة",
+ r"أحداث غير متوقعة",
+ r"أسباب خارجة عن السيطرة"
+ ],
+ "regulatory_compliance": [
+ r"الالتزام بالقوانين",
+ r"الالتزام بالأنظمة",
+ r"الالتزام بالتشريعات",
+ r"الالتزام باللوائح",
+ r"مراعاة القوانين",
+ r"وفقاً للقوانين",
+ r"طبقاً للأنظمة",
+ ],
+ "intellectual_property": [
+ r"الملكية الفكرية",
+ r"حقوق الملكية",
+ r"حقوق الطبع",
+ r"حقوق النشر",
+ r"براءات الاختراع",
+ r"التصاميم",
+ r"العلامات التجارية"
+ ],
+ "confidentiality": [
+ r"سرية المعلومات",
+ r"المعلومات السرية",
+ r"عدم الإفصاح",
+ r"الحفاظ على السرية",
+ r"عدم الكشف",
+ r"معلومات سرية"
+ ],
+ "insurance_requirements": [
+ r"متطلبات التأمين",
+ r"بوليصة تأمين",
+ r"تأمين ضد المسؤولية",
+ r"تأمين ضد المخاطر",
+ r"تأمين شامل",
+ r"تأمين المشروع"
+ ]
+ }
+
+ # تعريف مستويات خطورة المخاطر
+ self.severity_levels = {
+ "low": {
+ "name": "منخفضة",
+ "color": "#00b894", # أخضر
+ "score_range": (0, 33)
+ },
+ "medium": {
+ "name": "متوسطة",
+ "color": "#fdcb6e", # أصفر
+ "score_range": (34, 66)
+ },
+ "high": {
+ "name": "عالية",
+ "color": "#d63031", # أحمر
+ "score_range": (67, 100)
+ }
+ }
+
+ # أوزان أنواع المخاطر (الأهمية النسبية)
+ self.risk_weights = {
+ "legal": 0.9,
+ "financial": 0.8,
+ "operational": 0.7,
+ "technical": 0.6,
+ "compliance": 0.8,
+ "environmental": 0.6,
+ "safety": 0.7,
+ "schedule": 0.6,
+ "resource": 0.5,
+ "quality": 0.7,
+ "scope": 0.7,
+ "stakeholder": 0.5,
+ "commercial": 0.6,
+ "contractual": 0.9,
+ "regulatory": 0.8
+ }
+
+ def scan_contract_text(self, contract_text, title=""):
+ """فحص نص العقد لاستخراج المخاطر المحتملة"""
+ if not contract_text:
+ return {
+ "title": title or "عقد غير معروف",
+ "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "risks": [],
+ "overall_score": 0,
+ "overall_severity": "منخفضة",
+ "summary": "لم يتم توفير نص للتحليل."
+ }
+
+ # تحويل النص إلى أحرف صغيرة للفحص
+ text_lower = contract_text.lower()
+
+ risks = []
+ risk_id = 1
+
+ # فحص كل فئة مخاطر والبحث عن المصطلحات المرتبطة بها
+ for category, category_terms in self.risky_terms.items():
+ category_risks = []
+
+ for term in category_terms:
+ # البحث عن المصطلح في النص
+ occurrences = self._find_term_occurrences(contract_text, term)
+
+ if occurrences:
+ for occurrence in occurrences:
+ context = self._extract_context(contract_text, occurrence, window=100)
+ # تحديد مستوى الخطورة بناءً على السياق
+ severity = self._determine_severity_from_context(context, category)
+
+ category_risks.append({
+ "id": risk_id,
+ "term": term,
+ "category": category,
+ "category_ar": self.risk_categories[category],
+ "context": context,
+ "severity": severity,
+ "impact": self._determine_impact(category, severity),
+ "recommendation": self._generate_recommendation(category, term, severity)
+ })
+ risk_id += 1
+
+ # إضافة مخاطر الفئة إلى القائمة الرئيسية
+ risks.extend(category_risks)
+
+ # فحص صيغ المخاطر الإضافية في العقد
+ pattern_risks = self._scan_for_risk_patterns(contract_text, risk_id)
+ risks.extend(pattern_risks)
+
+ # حساب درجة المخاطر الإجمالية
+ overall_score = self._calculate_overall_risk_score(risks)
+
+ # تحديد مستوى الخطورة الإجمالية
+ overall_severity = self._determine_overall_severity(overall_score)
+
+ # توليد ملخص للمخاطر
+ summary = self._generate_risk_summary(risks, overall_score, overall_severity)
+
+ # إنشاء تقرير المخاطر
+ risk_report = {
+ "title": title or "عقد غير معروف",
+ "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "risks": risks,
+ "overall_score": overall_score,
+ "overall_severity": overall_severity["name"],
+ "severity_color": overall_severity["color"],
+ "summary": summary
+ }
+
+ # حفظ تقرير المخاطر
+ if title:
+ self._save_risk_report(risk_report, title)
+
+ return risk_report
+
+ def _find_term_occurrences(self, text, term):
+ """البحث عن مواضع ظهور المصطلح في النص"""
+ occurrences = []
+ start = 0
+
+ while True:
+ start = text.find(term, start)
+ if start == -1:
+ break
+ occurrences.append(start)
+ start += len(term)
+
+ return occurrences
+
+ def _extract_context(self, text, position, window=100):
+ """استخراج سياق النص حول موضع معين"""
+ start = max(0, position - window // 2)
+ end = min(len(text), position + window // 2)
+
+ # البحث عن بداية الجملة
+ while start > 0 and text[start] not in ['.', '!', '؟', '?', '\n']:
+ start -= 1
+
+ if start > 0:
+ start += 1 # تجاوز علامة الترقيم
+
+ # البحث عن نهاية الجملة
+ while end < len(text) - 1 and text[end] not in ['.', '!', '؟', '?', '\n']:
+ end += 1
+
+ if end < len(text) - 1:
+ end += 1 # تضمين علامة الترقيم
+
+ return text[start:end].strip()
+
+ def _determine_severity_from_context(self, context, category):
+ """تحديد مستوى خطورة المخاطر بناءً على السياق"""
+ # كلمات تزيد من مستوى الخطورة
+ high_severity_indicators = [
+ "حرج", "خطير", "ضروري", "إلزامي", "يجب", "مطلوب", "ضمان",
+ "تعويض", "غرامة", "يلتزم", "مسؤولية", "خسارة", "ضرر",
+ "تأخير", "مخالفة", "إخلال", "فسخ", "إنهاء", "تعديل"
+ ]
+
+ # كلمات تقلل من مستوى الخطورة
+ low_severity_indicators = [
+ "قد", "يمكن", "يجوز", "يحتمل", "محتمل", "ممكن", "اختياري",
+ "تقديري", "بالتوافق", "بالاتفاق", "مناسب", "معقول", "بحسب"
+ ]
+
+ # حساب عدد المؤشرات
+ high_count = sum(1 for indicator in high_severity_indicators if indicator in context)
+ low_count = sum(1 for indicator in low_severity_indicators if indicator in context)
+
+ # تحديد درجة الخطورة
+ if high_count > low_count * 2:
+ return "high"
+ elif high_count > low_count:
+ return "medium"
+ else:
+ return "low"
+
+ def _determine_impact(self, category, severity):
+ """تحديد تأثير المخاطر بناءً على الفئة ومستوى الخطورة"""
+ impact_descriptions = {
+ "legal": {
+ "high": "قد يؤدي إلى دعاوى قضائية ومسؤولية قانونية كبيرة",
+ "medium": "قد يتطلب تعديلات قانونية أو مفاوضات إضافية",
+ "low": "مخاطر قانونية محدودة يمكن معالجتها بسهولة"
+ },
+ "financial": {
+ "high": "مخاطر مالية كبيرة قد تؤثر على ربحية المشروع بشكل كبير",
+ "medium": "قد يؤدي إلى زيادة التكاليف أو تقليل الهوامش",
+ "low": "تأثير مالي محدود يمكن استيعابه"
+ },
+ "operational": {
+ "high": "قد يعيق تنفيذ المشروع بشكل كامل",
+ "medium": "قد يؤثر على كفاءة العمليات ويتطلب خطط بديلة",
+ "low": "تأثير محدود على العمليات اليومية"
+ },
+ "technical": {
+ "high": "قد يمنع تحقيق المتطلبات الفنية الأساسية",
+ "medium": "يتطلب حلول فنية إضافية أو تعديلات",
+ "low": "يمكن معالجته من خلال التعديلات الفنية البسيطة"
+ },
+ "compliance": {
+ "high": "قد يؤدي إلى عدم الامتثال للوائح الهامة",
+ "medium": "يتطلب تعديلات للامتثال للمتطلبات التنظيمية",
+ "low": "يمكن حله من خلال تدابير امتثال بسيطة"
+ },
+ "environmental": {
+ "high": "مخاطر بيئية كبيرة قد تؤدي إلى عقوبات أو تأخيرات",
+ "medium": "يتطلب إجراءات وقائية إضافية للحماية البيئية",
+ "low": "تأثير بيئي محدود يمكن إدارته"
+ },
+ "safety": {
+ "high": "مخاطر سلامة حرجة قد تهدد سلامة العاملين",
+ "medium": "يتطلب إجراءات سلامة إضافية وتدريب",
+ "low": "مخاطر سلامة يمكن معالجتها من خلال الإجراءات القياسية"
+ },
+ "schedule": {
+ "high": "قد يؤدي إلى تأخيرات كبيرة في المشروع",
+ "medium": "قد يؤثر على بعض مراحل الجدول الزمني",
+ "low": "تأثير محدود على الجدول الزمني يمكن استيعابه"
+ },
+ "resource": {
+ "high": "نقص حاد في الموارد الأساسية للمشروع",
+ "medium": "قد يتطلب موارد إضافية أو بديلة",
+ "low": "يمكن إدارته من خلال تخطيط الموارد المتاحة"
+ },
+ "quality": {
+ "high": "قد يؤدي إلى مشاكل جودة خطيرة تؤثر على قبول المشروع",
+ "medium": "يتطلب إجراءات ضمان جودة إضافية",
+ "low": "تأثير محدود على الجودة يمكن معالجته"
+ },
+ "scope": {
+ "high": "تغييرات جوهرية في نطاق العمل قد تؤثر على المشروع بأكمله",
+ "medium": "يتطلب تعديلات في بعض جوانب نطاق العمل",
+ "low": "تغييرات بسيطة في النطاق يمكن استيعابها"
+ },
+ "stakeholder": {
+ "high": "قد يؤثر سلباً على العلاقات مع أصحاب المصلحة الرئيسيين",
+ "medium": "يتطلب إدارة توقعات أصحاب المصلحة",
+ "low": "تأثير محدود على رضا أصحاب المصلحة"
+ },
+ "commercial": {
+ "high": "مخاطر تجارية كبيرة قد تؤثر على العلاقات التجارية الرئيسية",
+ "medium": "قد يتطلب إعادة التفاوض على بعض الشروط التجارية",
+ "low": "تأثير تجاري محدود يمكن إدارته"
+ },
+ "contractual": {
+ "high": "بنود تعاقدية مجحفة قد تؤثر على التزامات وحقوق الأطراف",
+ "medium": "يتطلب مراجعة قانونية وتعديل بعض البنود",
+ "low": "قضايا تعاقدية بسيطة يمكن توضيحها"
+ },
+ "regulatory": {
+ "high": "قد يؤدي إلى مخالفة لوائح تنظيمية هامة",
+ "medium": "يتطلب تغييرات للامتثال للمتطلبات التنظيمية",
+ "low": "متطلبات تنظيمية يمكن تلبيتها بسهولة"
+ }
+ }
+
+ return impact_descriptions.get(category, {}).get(severity, "تأثير غير محدد")
+
+ def _generate_recommendation(self, category, term, severity):
+ """توليد توصيات لمعالجة المخاطر"""
+ recommendations = {
+ "legal": {
+ "high": "مراجعة قانونية شاملة من محامي متخصص وإعادة التفاوض على البنود المتعلقة بـ'{term}'",
+ "medium": "مراجعة قانونية والتأكد من الصياغة الدقيقة للبنود المتعلقة بـ'{term}'",
+ "low": "مراقبة البنود المتعلقة بـ'{term}' أثناء تنفيذ العقد"
+ },
+ "financial": {
+ "high": "إعادة التفاوض على الشروط المالية والتأكد من وجود مخصصات كافية لتغطية المخاطر المتعلقة بـ'{term}'",
+ "medium": "وضع خطة احتياطية لإدارة التكاليف المرتبطة بـ'{term}'",
+ "low": "متابعة الجوانب المالية المتعلقة بـ'{term}' بشكل دوري"
+ },
+ "operational": {
+ "high": "وضع خطة تفصيلية لإدارة المخاطر التشغيلية المتعلقة بـ'{term}' وتوفير بدائل",
+ "medium": "تطوير إجراءات للتعامل مع المشكلات التشغيلية المتعلقة بـ'{term}'",
+ "low": "متابعة العمليات المتعلقة بـ'{term}' بشكل منتظم"
+ },
+ "technical": {
+ "high": "الاستعانة بخبراء فنيين متخصصين لمراجعة المتطلبات المتعلقة بـ'{term}'",
+ "medium": "إجراء مراجعة فنية للتأكد من قابلية تنفيذ المتطلبات المتعلقة بـ'{term}'",
+ "low": "التأكد من وضوح المواصفات الفنية المتعلقة بـ'{term}'"
+ },
+ "compliance": {
+ "high": "مراجعة متخصصة للتأكد من الامتثال للوائح المتعلقة بـ'{term}' وإجراء التعديلات اللازمة",
+ "medium": "وضع إجراءات للتأكد من الامتثال المستمر للمتطلبات المتعلقة بـ'{term}'",
+ "low": "متابعة متطلبات الامتثال المتعلقة بـ'{term}' بشكل دوري"
+ },
+ "environmental": {
+ "high": "إجراء تقييم بيئي شامل والتأكد من وجود خطط للتعامل مع المخاطر البيئية المتعلقة بـ'{term}'",
+ "medium": "مراجعة الإجراءات البيئية المتعلقة بـ'{term}' والتأكد من كفايتها",
+ "low": "متابعة الجوانب البيئية المتعلقة بـ'{term}' أثناء تنفيذ المشروع"
+ },
+ "safety": {
+ "high": "وضع خطة سلامة شاملة ومراجعتها من قبل متخصصين للتعامل مع المخاطر المتعلقة بـ'{term}'",
+ "medium": "مراجعة إجراءات السلامة الحالية وتعزيزها للتعامل مع المخاطر المتعلقة بـ'{term}'",
+ "low": "التأكد من تطبيق إجراءات السلامة القياسية المتعلقة بـ'{term}'"
+ },
+ "schedule": {
+ "high": "إعادة تقييم الجدول الزمني بشكل شامل ووضع خطط بديلة للتعامل مع البنود المتعلقة بـ'{term}'",
+ "medium": "وضع هوامش زمنية كافية للتعامل مع التأخيرات المحتملة المتعلقة بـ'{term}'",
+ "low": "مراقبة الجدول الزمني بشكل منتظم فيما يتعلق بـ'{term}'"
+ },
+ "resource": {
+ "high": "وضع خطة شاملة لتأمين الموارد اللازمة والبدائل المتعلقة بـ'{term}'",
+ "medium": "تحديد مصادر بديلة للموارد المتعلقة بـ'{term}'",
+ "low": "مراقبة توافر الموارد المتعلقة بـ'{term}' بشكل منتظم"
+ },
+ "quality": {
+ "high": "وضع خطة ضمان جودة شاملة والتأكد من وجود معايير واضحة للجوانب المتعلقة بـ'{term}'",
+ "medium": "تعزيز إجراءات ضمان الجودة للجوانب المتعلقة بـ'{term}'",
+ "low": "متابعة معايير الجودة المتعلقة بـ'{term}' بشكل منتظم"
+ },
+ "scope": {
+ "high": "توثيق نطاق العمل بشكل تفصيلي ووضع إجراءات واضحة للتعامل مع التغييرات المتعلقة بـ'{term}'",
+ "medium": "وضع آلية للتحكم في التغييرات المتعلقة بـ'{term}'",
+ "low": "مراقبة نطاق العمل بشكل منتظم فيما يتعلق بـ'{term}'"
+ },
+ "stakeholder": {
+ "high": "وضع خطة تواصل شاملة مع أصحاب المصلحة للتعامل مع القضايا المتعلقة بـ'{term}'",
+ "medium": "تعزيز التواصل مع أصحاب المصلحة المعنيين بـ'{term}'",
+ "low": "متابعة توقعات وملاحظات أصحاب المصلحة فيما يتعلق بـ'{term}'"
+ },
+ "commercial": {
+ "high": "إعادة التفاوض على الشروط التجارية المتعلقة بـ'{term}' والتأكد من تحقيق توازن المصالح",
+ "medium": "مراجعة الشروط التجارية المتعلقة بـ'{term}' والتأكد من وضوحها",
+ "low": "مراقبة تنفيذ الشروط التجارية المتعلقة بـ'{term}'"
+ },
+ "contractual": {
+ "high": "مراجعة قانونية شاملة للبنود التعاقدية المتعلقة بـ'{term}' وإعادة التفاوض عند الضرورة",
+ "medium": "توضيح وتحسين صياغة البنود المتعلقة بـ'{term}'",
+ "low": "التأكد من فهم الالتزامات التعاقدية المتعلقة بـ'{term}'"
+ },
+ "regulatory": {
+ "high": "الاستعانة بمستشار متخصص للتأكد من الامتثال للمتطلبات التنظيمية المتعلقة بـ'{term}'",
+ "medium": "مراجعة المتطلبات التنظيمية الحالية والمستقبلية المتعلقة بـ'{term}'",
+ "low": "متابعة التغييرات في المتطلبات التنظيمية المتعلقة بـ'{term}'"
+ }
+ }
+
+ recommendation_template = recommendations.get(category, {}).get(severity, "مراجعة البنود المتعلقة بـ'{term}'")
+ return recommendation_template.replace('{term}', term)
+
+ def _scan_for_risk_patterns(self, contract_text, risk_id_start):
+ """فحص النص بحثاً عن صيغ المخاطر المحددة مسبقاً"""
+ risk_id = risk_id_start
+ pattern_risks = []
+
+ for pattern_type, patterns in self.contract_risk_patterns.items():
+ for pattern in patterns:
+ matches = re.finditer(pattern, contract_text)
+
+ for match in matches:
+ match_text = match.group(0)
+ context = self._extract_context(contract_text, match.start(), window=150)
+ severity = self._determine_pattern_severity(pattern_type, match_text)
+ category = self._map_pattern_to_category(pattern_type)
+
+ pattern_risks.append({
+ "id": risk_id,
+ "term": match_text,
+ "category": category,
+ "category_ar": self.risk_categories[category],
+ "pattern_type": pattern_type,
+ "context": context,
+ "severity": severity,
+ "impact": self._determine_pattern_impact(pattern_type, severity),
+ "recommendation": self._generate_pattern_recommendation(pattern_type, match_text, severity)
+ })
+ risk_id += 1
+
+ return pattern_risks
+
+ def _determine_pattern_severity(self, pattern_type, match_text):
+ """تحديد مستوى خطورة المخاطر بناءً على نوع الصيغة ومحتواها"""
+ severity_rules = {
+ "unlimited_liability": "high",
+ "payment_delay": lambda text: "high" if any(int(n) > 60 for n in re.findall(r'(\d+)', text)) else "medium" if any(int(n) > 30 for n in re.findall(r'(\d+)', text)) else "low",
+ "excessive_penalties": lambda text: "high" if any(int(n) > 1 for n in re.findall(r'(\d+)', text)) else "medium" if any(int(n) > 0.5 for n in re.findall(r'(\d+)', text)) else "low",
+ "unilateral_termination": "high",
+ "unrealistic_deadlines": lambda text: "high" if any(int(n) < 30 for n in re.findall(r'(\d+)', text)) else "medium" if any(int(n) < 60 for n in re.findall(r'(\d+)', text)) else "low",
+ "scope_creep": "medium",
+ "indemnification": "high",
+ "change_control": "high",
+ "warranty_period": lambda text: "high" if any(int(n) > 24 for n in re.findall(r'(\d+)', text)) else "medium" if any(int(n) > 12 for n in re.findall(r'(\d+)', text)) else "low",
+ "dispute_resolution": "medium",
+ "force_majeure": "medium",
+ "regulatory_compliance": "medium",
+ "intellectual_property": "high",
+ "confidentiality": "medium",
+ "insurance_requirements": "medium"
+ }
+
+ rule = severity_rules.get(pattern_type, "medium")
+
+ if callable(rule):
+ return rule(match_text)
+ else:
+ return rule
+
+ def _map_pattern_to_category(self, pattern_type):
+ """تعيين نوع الصيغة إلى فئة المخاطر المناسبة"""
+ pattern_category_map = {
+ "unlimited_liability": "legal",
+ "payment_delay": "financial",
+ "excessive_penalties": "financial",
+ "unilateral_termination": "contractual",
+ "unrealistic_deadlines": "schedule",
+ "scope_creep": "scope",
+ "indemnification": "legal",
+ "change_control": "scope",
+ "warranty_period": "quality",
+ "dispute_resolution": "legal",
+ "force_majeure": "contractual",
+ "regulatory_compliance": "compliance",
+ "intellectual_property": "legal",
+ "confidentiality": "contractual",
+ "insurance_requirements": "financial"
+ }
+
+ return pattern_category_map.get(pattern_type, "contractual")
+
+ def _determine_pattern_impact(self, pattern_type, severity):
+ """تحديد تأثير المخاطر بناءً على نوع الصيغة ومستوى الخطورة"""
+ pattern_impact = {
+ "unlimited_liability": {
+ "high": "يمكن أن يعرض الشركة لمسؤولية مالية وقانونية غير محدودة",
+ "medium": "قد يؤدي إلى مسؤولية مالية كبيرة غير متوقعة",
+ "low": "زيادة محتملة في المسؤولية القانونية"
+ },
+ "payment_delay": {
+ "high": "تأخر كبير في الدفعات قد يؤثر على التدفق النقدي والسيولة",
+ "medium": "قد يتسبب في ضغط على التدفق النقدي",
+ "low": "تأثير محدود على التدفق النقدي"
+ },
+ "excessive_penalties": {
+ "high": "غرامات تأخير مرتفعة قد تؤثر بشكل كبير على ربحية المشروع",
+ "medium": "غرامات معتدلة قد تقلل من هامش الربح",
+ "low": "غرامات محدودة يمكن إدارتها من خلال الجدولة الدقيقة"
+ },
+ "unilateral_termination": {
+ "high": "إمكانية إنهاء العقد من طرف واحد دون تعويض مناسب",
+ "medium": "شروط إنهاء غير متوازنة قد تتطلب إعادة التفاوض",
+ "low": "بنود إنهاء تحتاج إلى مراقبة وتوثيق"
+ },
+ "unrealistic_deadlines": {
+ "high": "مواعيد نهائية غير واقعية قد تؤدي إلى فشل المشروع أو غرامات كبيرة",
+ "medium": "جدول زمني ضيق يتطلب موارد إضافية وإدارة مكثفة",
+ "low": "مواعيد نهائية تحتاج إلى تخطيط دقيق"
+ },
+ "scope_creep": {
+ "high": "توسع غير محدود في نطاق العمل دون تعديل السعر أو الجدول الزمني",
+ "medium": "تغييرات محتملة في النطاق تتطلب إدارة دقيقة",
+ "low": "بعض التعديلات المحتملة على النطاق يمكن إدارتها"
+ },
+ "indemnification": {
+ "high": "التزامات تعويض واسعة النطاق قد تؤدي إلى مسؤولية غير محدودة",
+ "medium": "شروط تعويض تحتاج إلى مراجعة قانونية",
+ "low": "التزامات تعويض معقولة تحتاج إلى متابعة"
+ },
+ "change_control": {
+ "high": "عدم وجود آلية واضحة للتحكم في التغييرات وتأثيرها على التكلفة",
+ "medium": "آلية تغيير غير كافية قد تؤدي إلى نزاعات",
+ "low": "إجراءات تغيير تحتاج إلى تحسين"
+ },
+ "warranty_period": {
+ "high": "فترة ضمان طويلة غير متناسبة مع طبيعة المشروع",
+ "medium": "فترة ضمان تتطلب موارد إضافية للدعم",
+ "low": "فترة ضمان معقولة تحتاج إلى تخطيط"
+ },
+ "dispute_resolution": {
+ "high": "آليات غير مناسبة لحل النزاعات قد تؤدي إلى إجراءات مكلفة",
+ "medium": "آليات حل النزاعات تحتاج إلى توضيح",
+ "low": "شروط حل النزاعات تحتاج إلى مراجعة"
+ },
+ "force_majeure": {
+ "high": "تعريف ضيق للقوة القاهرة قد يؤدي إلى مسؤولية غير متوقعة",
+ "medium": "بنود القوة القاهرة تحتاج إلى توضيح",
+ "low": "شروط القوة القاهرة معقولة ولكن تحتاج إلى مراقبة"
+ },
+ "regulatory_compliance": {
+ "high": "متطلبات امتثال صارمة قد تزيد التكاليف أو المسؤولية",
+ "medium": "التزامات الامتثال تحتاج إلى موارد إضافية",
+ "low": "متطلبات امتثال معقولة تحتاج إلى مراقبة"
+ },
+ "intellectual_property": {
+ "high": "نقل واسع لحقوق الملكية الفكرية دون تعويض مناسب",
+ "medium": "شروط الملكية الفكرية تحتاج إلى توضيح وتعديل",
+ "low": "بنود الملكية الفكرية تحتاج إلى مراجعة"
+ },
+ "confidentiality": {
+ "high": "التزامات سرية واسعة وطويلة الأمد قد تقيد النشاط المستقبلي",
+ "medium": "التزامات السرية تحتاج إلى تحديد نطاق ومدة",
+ "low": "شروط السرية معقولة ولكن تحتاج إلى مراقبة"
+ },
+ "insurance_requirements": {
+ "high": "متطلبات تأمين مرتفعة قد تزيد التكاليف بشكل كبير",
+ "medium": "متطلبات التأمين تحتاج إلى مراجعة للتأكد من التناسب",
+ "low": "متطلبات تأمين معقولة تحتاج إلى التحقق من التوافر"
+ }
+ }
+
+ return pattern_impact.get(pattern_type, {}).get(severity, "تأثير غير محدد")
+
+ def _generate_pattern_recommendation(self, pattern_type, match_text, severity):
+ """توليد توصيات لمعالجة المخاطر بناءً على نوع الصيغة"""
+ pattern_recommendations = {
+ "unlimited_liability": {
+ "high": "إعادة التفاوض على بنود المسؤولية وتحديد سقف للتعويضات يتناسب مع قيمة العقد",
+ "medium": "وضع حدود واضحة للمسؤولية وطلب تعديل البنود المتعلقة بها",
+ "low": "مراجعة بنود المسؤولية والتأكد من وجود تغطية تأمينية مناسبة"
+ },
+ "payment_delay": {
+ "high": "إعادة التفاوض على شروط الدفع وتقليل فترة السداد، مع إضافة فوائد تأخير",
+ "medium": "وضع آلية واضحة لمتابعة المدفوعات وتحديد إجراءات التصعيد في حالة التأخر",
+ "low": "مراقبة مواعيد الدفع والتأكد من إصدار الفواتير في الوقت المناسب"
+ },
+ "excessive_penalties": {
+ "high": "إعادة التفاوض على نسب وآليات غرامات التأخير وربطها بالضرر الفعلي",
+ "medium": "وضع حد أقصى للغرامات ووضع خطة لتجنب التأخير",
+ "low": "مراقبة تقدم العمل بدقة للالتزام بالجدول الزمني"
+ },
+ "unilateral_termination": {
+ "high": "تعديل بنود الإنهاء لتكون متوازنة وتضمين تعويض مناسب في حالة الإنهاء",
+ "medium": "وضع شروط واضحة للإنهاء من كلا الطرفين وتحديد آلية التعويض",
+ "low": "التأكد من وجود خطة للتعامل مع حالات الإنهاء المحتملة"
+ },
+ "unrealistic_deadlines": {
+ "high": "إعادة التفاوض على الجدول الزمني ليكون واقعياً بناءً على تقييم دقيق للموارد والقدرات",
+ "medium": "وضع خطة تفصيلية للتنفيذ مع تحديد المراحل الحرجة وتوفير موارد إضافية",
+ "low": "مراقبة الجدول الزمني بشكل مستمر وتحديد المخاطر المحتملة"
+ },
+ "scope_creep": {
+ "high": "تحديد نطاق العمل بدقة ووضع إجراءات صارمة لإدارة التغييرات مع ربطها بالتكلفة والوقت",
+ "medium": "وضع آلية واضحة لإدارة التغييرات والتأكد من توثيق نطاق العمل بشكل تفصيلي",
+ "low": "مراقبة نطاق العمل والتأكد من موافقة جميع الأطراف على أي تغييرات"
+ },
+ "indemnification": {
+ "high": "إعادة التفاوض على بنود التعويض لتكون متوازنة ومحددة بمبلغ يتناسب مع قيمة العقد",
+ "medium": "تحديد نطاق التعويض وربطه بالأضرار المباشرة والفعلية",
+ "low": "مراجعة بنود التعويض والتأكد من وجود تغطية تأمينية مناسبة"
+ },
+ "change_control": {
+ "high": "وضع آلية واضحة وصارمة لإدارة التغييرات مع تحديد التأثير على التكلفة والوقت",
+ "medium": "تحسين إجراءات إدارة التغييرات والتأكد من توثيق جميع التغييرات",
+ "low": "مراقبة التغييرات والتأكد من الحصول على موافقة مكتوبة قبل التنفيذ"
+ },
+ "warranty_period": {
+ "high": "إعادة التفاوض على فترة الضمان لتكون متناسبة مع طبيعة المشروع والمعايير الصناعية",
+ "medium": "تحديد نطاق الضمان بوضوح وتخصيص موارد كافية للدعم خلال فترة الضمان",
+ "low": "وضع خطة لإدارة التزامات الضمان والتأكد من توثيق حالة التسليم"
+ },
+ "dispute_resolution": {
+ "high": "تعديل آليات حل النزاعات لتشمل التفاوض والوساطة قبل اللجوء للتحكيم أو القضاء",
+ "medium": "توضيح إجراءات حل النزاعات وتحديد الاختصاص القضائي والقانون الواجب التطبيق",
+ "low": "مراجعة آليات حل النزاعات والتأكد من فهم الإجراءات المتبعة"
+ },
+ "force_majeure": {
+ "high": "توسيع تعريف القوة القاهرة ليشمل الحالات المحتملة وتحديد آلية واضحة للإخطار والتعامل",
+ "medium": "توضيح إجراءات الإخطار والإجراءات المتبعة في حالات القوة القاهرة",
+ "low": "مراجعة بنود القوة القاهرة والتأكد من شمولها للحالات المحتملة"
+ },
+ "regulatory_compliance": {
+ "high": "تحديد مسؤوليات كل طرف بوضوح فيما يتعلق بالالتزامات التنظيمية والحصول على المشورة القانونية",
+ "medium": "مراجعة متطلبات الامتثال والتأكد من القدرة على تلبيتها",
+ "low": "متابعة التغييرات في المتطلبات التنظيمية والتأكد من الالتزام المستمر"
+ },
+ "intellectual_property": {
+ "high": "إعادة التفاوض على بنود الملكية الفكرية لحماية حقوق الشركة والحصول على تعويض مناسب",
+ "medium": "توضيح حقوق الملكية الفكرية لكل طرف وتحديد نطاق الاستخدام المسموح به",
+ "low": "مراجعة بنود الملكية الفكرية والتأكد من حماية الأصول الفكرية للشركة"
+ },
+ "confidentiality": {
+ "high": "تحديد نطاق ومدة التزامات السرية بشكل واضح ومتوازن لتجنب القيود غير الضرورية",
+ "medium": "توضيح نطاق المعلومات السرية وتحديد مدة معقولة للالتزام بالسرية",
+ "low": "مراجعة التزامات السرية والتأكد من إمكانية الالتزام بها"
+ },
+ "insurance_requirements": {
+ "high": "إعادة التفاوض على متطلبات التأمين لتكون متناسبة مع طبيعة وحجم المشروع والمخاطر الفعلية",
+ "medium": "التحقق من توافر وتكلفة التغطية التأمينية المطلوبة والتفاوض على تعديلها إذا لزم الأمر",
+ "low": "التأكد من توافر التغطية التأمينية المطلوبة والحفاظ على سريانها"
+ }
+ }
+
+ return pattern_recommendations.get(pattern_type, {}).get(severity, "مراجعة وتعديل البنود المتعلقة بهذه المخاطر")
+
+ def _calculate_overall_risk_score(self, risks):
+ """حساب درجة المخاطر الإجمالية"""
+ if not risks:
+ return 0
+
+ # تحويل مستويات الخطورة إلى قيم عددية
+ severity_scores = {"low": 25, "medium": 50, "high": 90}
+
+ # حساب مجموع الأوزان ودرجات المخاطر المرجحة
+ total_weight = 0
+ weighted_score_sum = 0
+
+ # تجميع المخاطر حسب الفئة
+ risk_categories = {}
+ for risk in risks:
+ category = risk["category"]
+ severity = risk["severity"]
+
+ if category not in risk_categories:
+ risk_categories[category] = []
+
+ risk_categories[category].append(severity_scores[severity])
+
+ # حساب متوسط درجة المخاطرة لكل فئة وترجيحها بالوزن
+ for category, scores in risk_categories.items():
+ category_weight = self.risk_weights.get(category, 0.5)
+ category_score = sum(scores) / len(scores)
+
+ weighted_score_sum += category_score * category_weight
+ total_weight += category_weight
+
+ # حساب الدرجة الإجمالية المرجحة
+ if total_weight > 0:
+ overall_score = int(weighted_score_sum / total_weight)
+ else:
+ overall_score = 0
+
+ return min(100, max(0, overall_score))
+
+ def _determine_overall_severity(self, overall_score):
+ """تحديد مستوى الخطورة الإجمالية بناءً على الدرجة الإجمالية"""
+ for severity, info in self.severity_levels.items():
+ min_score, max_score = info["score_range"]
+ if min_score <= overall_score <= max_score:
+ return {
+ "level": severity,
+ "name": info["name"],
+ "color": info["color"]
+ }
+
+ # القيمة الافتراضية
+ return {
+ "level": "low",
+ "name": "منخفضة",
+ "color": "#00b894"
+ }
+
+ def _generate_risk_summary(self, risks, overall_score, overall_severity):
+ """توليد ملخص للمخاطر المكتشفة"""
+ if not risks:
+ return "لم يتم اكتشاف مخاطر كبيرة في العقد."
+
+ # تجميع المخاطر حسب الفئة ومستوى الخطورة
+ risk_categories = {}
+ for risk in risks:
+ category = risk["category"]
+ category_ar = risk["category_ar"]
+ severity = risk["severity"]
+
+ if category not in risk_categories:
+ risk_categories[category] = {
+ "name_ar": category_ar,
+ "high": 0,
+ "medium": 0,
+ "low": 0,
+ "total": 0
+ }
+
+ risk_categories[category][severity] += 1
+ risk_categories[category]["total"] += 1
+
+ # حساب إجماليات المخاطر
+ total_risks = len(risks)
+ high_risks = sum(risk_categories[category]["high"] for category in risk_categories)
+ medium_risks = sum(risk_categories[category]["medium"] for category in risk_categories)
+ low_risks = sum(risk_categories[category]["low"] for category in risk_categories)
+
+ # بناء نص الملخص
+ summary = f"تم تحديد {total_risks} مخاطر محتملة في العقد مع درجة خطورة إجمالية {overall_score}% ({overall_severity['name']})."
+ summary += f" وتتضمن {high_risks} مخاطر عالية، و{medium_risks} مخاطر متوسطة، و{low_risks} مخاطر منخفضة."
+
+ # ذكر أهم فئات المخاطر
+ summary += " أهم فئات المخاطر المحددة هي:"
+
+ # ترتيب فئات المخاطر حسب الأهمية
+ sorted_categories = sorted(
+ risk_categories.items(),
+ key=lambda x: (x[1]["high"], x[1]["medium"], x[1]["total"]),
+ reverse=True
+ )
+
+ # إضافة أهم 3 فئات مخاطر إلى الملخص
+ for i, (category, data) in enumerate(sorted_categories[:3]):
+ summary += f" {data['name_ar']} ({data['total']} مخاطر، منها {data['high']} عالية)"
+ if i < 2:
+ summary += "،"
+ else:
+ summary += "."
+
+ # إضافة توصية عامة
+ if high_risks > 0:
+ summary += " يوصى بمراجعة العقد بشكل دقيق ومناقشة المخاطر العالية مع الأطراف المعنية قبل التوقيع."
+ elif medium_risks > total_risks / 2:
+ summary += " يوصى بمراجعة المخاطر المتوسطة وتقييم تأثيرها المحتمل قبل التوقيع."
+ else:
+ summary += " يمكن قبول العقد مع مراقبة المخاطر المحددة أثناء التنفيذ."
+
+ return summary
+
+ def _save_risk_report(self, risk_report, report_name):
+ """حفظ تقرير المخاطر كملف JSON"""
+ filename = f"{report_name.replace(' ', '_')}_risk_report.json"
+ file_path = os.path.join(self.risk_data_dir, filename)
+
+ try:
+ with open(file_path, 'w', encoding='utf-8') as f:
+ json.dump(risk_report, f, ensure_ascii=False, indent=2)
+ except Exception as e:
+ print(f"خطأ في حفظ تقرير المخاطر: {e}")
+
+ def load_risk_report(self, report_name):
+ """تحميل تقرير مخاطر محفوظ مسبقاً"""
+ filename = f"{report_name.replace(' ', '_')}_risk_report.json"
+ file_path = os.path.join(self.risk_data_dir, filename)
+
+ if not os.path.exists(file_path):
+ return None
+
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ risk_report = json.load(f)
+ return risk_report
+ except Exception as e:
+ print(f"خطأ في تحميل تقرير المخاطر: {e}")
+ return None
+
+ def generate_risk_comparison(self, contract_text1, contract_text2, title1="العقد الأول", title2="العقد الثاني"):
+ """مقارنة المخاطر بين عقدين"""
+ # تحليل المخاطر في كل عقد
+ report1 = self.scan_contract_text(contract_text1, title1)
+ report2 = self.scan_contract_text(contract_text2, title2)
+
+ # مقارنة درجات المخاطر الإجمالية
+ score_diff = report1["overall_score"] - report2["overall_score"]
+
+ # تحديد العقد الأقل مخاطرة
+ less_risky_contract = title2 if score_diff > 0 else title1
+
+ # تجميع المخاطر حسب الفئة لكل عقد
+ categories1 = self._group_risks_by_category(report1["risks"])
+ categories2 = self._group_risks_by_category(report2["risks"])
+
+ # تحديد الفئات الموجودة في كلا العقدين
+ all_categories = set(categories1.keys()) | set(categories2.keys())
+
+ # مقارنة المخاطر في كل فئة
+ category_comparison = {}
+ for category in all_categories:
+ cat_risks1 = categories1.get(category, {"high": 0, "medium": 0, "low": 0, "total": 0, "name_ar": self.risk_categories.get(category, category)})
+ cat_risks2 = categories2.get(category, {"high": 0, "medium": 0, "low": 0, "total": 0, "name_ar": self.risk_categories.get(category, category)})
+
+ # حساب الفرق في المخاطر العالية والمتوسطة
+ high_diff = cat_risks1["high"] - cat_risks2["high"]
+ medium_diff = cat_risks1["medium"] - cat_risks2["medium"]
+ total_diff = cat_risks1["total"] - cat_risks2["total"]
+
+ category_comparison[category] = {
+ "name_ar": cat_risks1["name_ar"],
+ "contract1": cat_risks1,
+ "contract2": cat_risks2,
+ "high_diff": high_diff,
+ "medium_diff": medium_diff,
+ "total_diff": total_diff
+ }
+
+ # تجميع المخاطر المشتركة والفريدة
+ common_risks = []
+ unique_risks1 = []
+ unique_risks2 = []
+
+ # تحديد المخاطر المشتركة والفريدة (بناءً على المصطلحات)
+ terms1 = set(risk["term"] for risk in report1["risks"])
+ terms2 = set(risk["term"] for risk in report2["risks"])
+
+ common_terms = terms1 & terms2
+ unique_terms1 = terms1 - terms2
+ unique_terms2 = terms2 - terms1
+
+ # تجميع المخاطر المشتركة
+ for risk in report1["risks"]:
+ if risk["term"] in common_terms:
+ common_risks.append({
+ "term": risk["term"],
+ "category": risk["category"],
+ "category_ar": risk["category_ar"],
+ "contract": title1,
+ "severity": risk["severity"]
+ })
+
+ for risk in report2["risks"]:
+ if risk["term"] in common_terms:
+ common_risks.append({
+ "term": risk["term"],
+ "category": risk["category"],
+ "category_ar": risk["category_ar"],
+ "contract": title2,
+ "severity": risk["severity"]
+ })
+
+ # تجميع المخاطر الفريدة
+ for risk in report1["risks"]:
+ if risk["term"] in unique_terms1:
+ unique_risks1.append(risk)
+
+ for risk in report2["risks"]:
+ if risk["term"] in unique_terms2:
+ unique_risks2.append(risk)
+
+ # إنشاء تقرير المقارنة
+ comparison_report = {
+ "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "contract1": {
+ "title": title1,
+ "overall_score": report1["overall_score"],
+ "overall_severity": report1["overall_severity"],
+ "risk_count": len(report1["risks"])
+ },
+ "contract2": {
+ "title": title2,
+ "overall_score": report2["overall_score"],
+ "overall_severity": report2["overall_severity"],
+ "risk_count": len(report2["risks"])
+ },
+ "score_diff": abs(score_diff),
+ "less_risky_contract": less_risky_contract,
+ "category_comparison": category_comparison,
+ "common_risks": common_risks,
+ "unique_risks1": unique_risks1,
+ "unique_risks2": unique_risks2,
+ "summary": self._generate_comparison_summary(report1, report2, title1, title2, score_diff, category_comparison)
+ }
+
+ return comparison_report
+
+ def _group_risks_by_category(self, risks):
+ """تجميع المخاطر حسب الفئة"""
+ categories = {}
+
+ for risk in risks:
+ category = risk["category"]
+ severity = risk["severity"]
+
+ if category not in categories:
+ categories[category] = {
+ "high": 0,
+ "medium": 0,
+ "low": 0,
+ "total": 0,
+ "name_ar": risk["category_ar"]
+ }
+
+ categories[category][severity] += 1
+ categories[category]["total"] += 1
+
+ return categories
+
+ def _generate_comparison_summary(self, report1, report2, title1, title2, score_diff, category_comparison):
+ """توليد ملخص للمقارنة بين العقدين"""
+ # تحديد العقد الأقل مخاطرة
+ less_risky = title1 if score_diff <= 0 else title2
+
+ summary = f"مقارنة بين {title1} و{title2} أظهرت فرق في درجة المخاطرة الإجمالية بنسبة {abs(score_diff)}%، حيث كان {less_risky} هو الأقل مخاطرة. "
+
+ # تحديد الفئات ذات الاختلافات الكبيرة
+ significant_diff_categories = []
+ for category, data in category_comparison.items():
+ if abs(data["high_diff"]) > 1 or abs(data["total_diff"]) > 3:
+ significant_diff_categories.append((category, data))
+
+ # ترتيب الفئات حسب الاختلاف
+ significant_diff_categories.sort(key=lambda x: (abs(x[1]["high_diff"]), abs(x[1]["total_diff"])), reverse=True)
+
+ # إضافة معلومات عن الفئات ذات الاختلافات الكبيرة
+ if significant_diff_categories:
+ summary += "أبرز الاختلافات كانت في فئات: "
+
+ for i, (category, data) in enumerate(significant_diff_categories[:3]):
+ name_ar = data["name_ar"]
+ more_risky = title1 if data["total_diff"] > 0 else title2
+ diff = abs(data["total_diff"])
+
+ summary += f"{name_ar} (الفرق: {diff} مخاطر لصالح {more_risky})"
+ if i < len(significant_diff_categories[:3]) - 1:
+ summary += "، "
+ else:
+ summary += ". "
+
+ # إضافة توصية
+ if abs(score_diff) > 20:
+ summary += f"يوصى بالتفاوض على إعادة صياغة العقد على أساس البنود الأقل مخاطرة من {less_risky}."
+ elif abs(score_diff) > 10:
+ summary += f"يوصى بمراجعة البنود المتعلقة بالمخاطر العالية ومقارنتها بين العقدين للتفاوض على تحسينها."
+ else:
+ summary += "لا توجد اختلافات كبيرة في المخاطر بين العقدين، ويمكن اختيار أيهما بناءً على معايير أخرى."
+
+ return summary
+
+ def render_risk_dashboard(self, contract_text, title="العقد"):
+ """عرض لوحة معلومات تحليل المخاطر"""
+ st.markdown("
تحليل مخاطر العقود الآلي
", unsafe_allow_html=True)
+
+ if not contract_text:
+ st.warning("يرجى إدخال نص العقد أو تحميل ملف العقد للتحليل")
+ return
+
+ # تحليل العقد
+ risk_report = self.scan_contract_text(contract_text, title)
+
+ # عرض ملخص المخاطر
+ st.markdown("
+ """, unsafe_allow_html=True)
+
+ with col2:
+ high_risks = sum(1 for risk in risk_report["risks"] if risk["severity"] == "high")
+ medium_risks = sum(1 for risk in risk_report["risks"] if risk["severity"] == "medium")
+ low_risks = sum(1 for risk in risk_report["risks"] if risk["severity"] == "low")
+
+ st.markdown(f"""
+
+ """, unsafe_allow_html=True)
+ else:
+ st.info(f"لا توجد مخاطر فريدة في {title2}")
+
+ # إضافة CSS لتنسيق الواجهة
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+ def render(self):
+ """عرض واجهة المستخدم لتحليل مخاطر العقود"""
+ st.markdown("
نظام تقييم مخاطر العقود الآلي
", unsafe_allow_html=True)
+
+ st.markdown("""
+
+ يمكنك استخدام هذا النظام لتحليل مخاطر العقود بشكل آلي وتحديد البنود التي قد تشكل مخاطر محتملة،
+ مع تقديم توصيات للتعامل مع هذه المخاطر وتحسين العقود.
+
+ """, unsafe_allow_html=True)
+
+ # إنشاء علامات تبويب لطرق مختلفة للتحليل
+ tabs = st.tabs([
+ "تحليل مخاطر العقد",
+ "مقارنة بين عقدين",
+ "تحليل عقد من ملف"
+ ])
+
+ with tabs[0]:
+ st.markdown("### تحليل نص العقد")
+
+ contract_title = st.text_input("عنوان العقد")
+ contract_text = st.text_area("أدخل نص العقد هنا", height=300)
+
+ if st.button("تحليل المخاطر"):
+ if contract_text:
+ self.render_risk_dashboard(contract_text, contract_title or "العقد")
+ else:
+ st.warning("يرجى إدخال نص العقد للتحليل")
+
+ with tabs[1]:
+ st.markdown("### مقارنة المخاطر بين عقدين")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ contract1_title = st.text_input("عنوان العقد الأول")
+ contract1_text = st.text_area("أدخل نص العقد الأول", height=200)
+
+ with col2:
+ contract2_title = st.text_input("عنوان العقد الثاني")
+ contract2_text = st.text_area("أدخل نص العقد الثاني", height=200)
+
+ if st.button("مقارنة المخاطر"):
+ if contract1_text and contract2_text:
+ self.render_risk_comparison_dashboard(
+ contract1_text,
+ contract2_text,
+ contract1_title or "العقد الأول",
+ contract2_title or "العقد الثاني"
+ )
+ else:
+ st.warning("يرجى إدخال نص كلا العقدين للمقارنة")
+
+ with tabs[2]:
+ st.markdown("### تحليل عقد من ملف")
+
+ uploaded_file = st.file_uploader("قم بتحميل ملف العقد", type=["txt", "docx", "pdf", "md"])
+
+ if uploaded_file is not None:
+ file_title = uploaded_file.name
+ file_content = ""
+
+ if uploaded_file.name.endswith(".pdf"):
+ st.info("جاري معالجة ملف PDF...")
+ try:
+ import fitz # PyMuPDF
+
+ pdf_bytes = uploaded_file.read()
+ with open("temp_contract.pdf", "wb") as f:
+ f.write(pdf_bytes)
+
+ doc = fitz.open("temp_contract.pdf")
+ for page in doc:
+ file_content += page.get_text()
+ except ImportError:
+ file_content = "تعذر قراءة ملف PDF. يرجى التأكد من تثبيت مكتبة PyMuPDF أو قم بنسخ ولصق محتوى العقد في علامة التبويب الأولى."
+
+ elif uploaded_file.name.endswith(".docx"):
+ st.info("جاري معالجة ملف Word...")
+ try:
+ from docx import Document
+
+ docx_bytes = uploaded_file.read()
+ with open("temp_contract.docx", "wb") as f:
+ f.write(docx_bytes)
+
+ doc = Document("temp_contract.docx")
+ file_content = "\n".join([paragraph.text for paragraph in doc.paragraphs])
+ except ImportError:
+ file_content = "تعذر قراءة ملف Word. يرجى التأكد من تثبيت مكتبة python-docx أو قم بنسخ ولصق محتوى العقد في علامة التبويب الأولى."
+
+ else: # للملفات النصية
+ file_content = uploaded_file.read().decode("utf-8")
+
+ if file_content:
+ st.markdown("### محتوى الملف")
+ with st.expander("عرض محتوى الملف"):
+ st.text(file_content[:5000] + ("..." if len(file_content) > 5000 else ""))
+
+ if st.button("تحليل مخاطر الملف"):
+ self.render_risk_dashboard(file_content, file_title)
+ else:
+ st.warning("تعذر قراءة محتوى الملف. يرجى المحاولة مرة أخرى أو استخدام علامة التبويب الأولى.")
+
+ # إضافة CSS للتنسيق
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
\ No newline at end of file
diff --git a/modules/risk_assessment/risk_assessment_app.py b/modules/risk_assessment/risk_assessment_app.py
new file mode 100644
index 0000000000000000000000000000000000000000..1d812640eab14ddbb993fa89fa183073c02524dd
--- /dev/null
+++ b/modules/risk_assessment/risk_assessment_app.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+تطبيق تقييم مخاطر العقود الآلي
+"""
+
+import os
+import sys
+import streamlit as st
+import pandas as pd
+import numpy as np
+
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
+
+# استيراد مكونات تحليل مخاطر العقود
+from modules.risk_assessment.contract_risk_analyzer import ContractRiskAnalyzer
+
+
+class RiskAssessmentApp:
+ """تطبيق تقييم مخاطر العقود الآلي"""
+
+ def __init__(self):
+ """تهيئة تطبيق تقييم مخاطر العقود"""
+ self.risk_analyzer = ContractRiskAnalyzer()
+
+ def render(self):
+ """عرض واجهة المستخدم الرئيسية للتطبيق"""
+ self.risk_analyzer.render()
+
+
+# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
+if __name__ == "__main__":
+ st.set_page_config(
+ page_title="تقييم مخاطر العقود الآلي | WAHBi AI",
+ page_icon="⚠️",
+ layout="wide",
+ initial_sidebar_state="expanded"
+ )
+
+ app = RiskAssessmentApp()
+ app.render()
\ No newline at end of file
diff --git a/modules/services/item_extractor.py b/modules/services/item_extractor.py
new file mode 100644
index 0000000000000000000000000000000000000000..cafaf3fcf4d1c4108c9272c7841e1cc9194b3cec
--- /dev/null
+++ b/modules/services/item_extractor.py
@@ -0,0 +1,124 @@
+"""
+خدمة استخراج البنود من المستندات
+"""
+
+import re
+import pandas as pd
+import numpy as np
+import nltk
+from nltk.tokenize import sent_tokenize
+from pathlib import Path
+import config
+
+class ItemExtractor:
+ """استخراج البنود من المستندات"""
+
+ def __init__(self):
+ # تحميل موارد NLTK إذا لم تكن موجودة
+ try:
+ nltk.data.find('tokenizers/punkt')
+ except LookupError:
+ nltk.download('punkt')
+
+ # قائمة الكلمات المفتاحية التي تشير إلى بداية البنود
+ self.item_indicators = [
+ 'توريد', 'تركيب', 'تنفيذ', 'تصنيع', 'أعمال', 'تأمين',
+ 'تقديم', 'إنشاء', 'صيانة', 'إزالة', 'نقل', 'تجهيز',
+ 'فك', 'تسليم', 'تطبيق', 'تثبيت', 'تشطيب', 'تجهيز'
+ ]
+
+ # قائمة فئات البنود
+ self.categories = {
+ 'أعمال الأساسات': ['أساس', 'قاعدة', 'حفر', 'ردم', 'خرسانة', 'اسمنت', 'قواعد'],
+ 'أعمال الهيكل الإنشائي': ['عمود', 'سقف', 'كمرة', 'خرسانة', 'حديد تسليح', 'بلاطة', 'هيكل'],
+ 'أعمال التشطيبات': ['دهان', 'بلاط', 'سيراميك', 'رخام', 'جبس', 'زجاج', 'باب', 'نافذة', 'أرضية'],
+ 'أعمال الكهرباء': ['كهرباء', 'إضاءة', 'مفتاح', 'سلك', 'لوحة', 'كابل', 'تمديد'],
+ 'أعمال السباكة': ['ماء', 'صرف', 'مواسير', 'حمام', 'مغسلة', 'خزان', 'مضخة'],
+ 'أعمال التكييف': ['تكييف', 'تبريد', 'تهوية', 'مكيف', 'مجرى هواء', 'فلتر'],
+ 'أعمال الموقع': ['تسوية', 'تخطيط', 'أسوار', 'بوابات', 'طرق', 'رصف', 'تشجير'],
+ 'المستندات': ['مخططات', 'رسومات', 'تقارير', 'شهادات', 'اختبارات']
+ }
+
+ def extract_items(self, text):
+ """استخراج البنود من النص"""
+ if not text:
+ return pd.DataFrame()
+
+ # تقسيم النص إلى جمل
+ sentences = sent_tokenize(text)
+
+ # البحث عن البنود المحتملة
+ items = []
+ item_id = 1
+
+ for sentence in sentences:
+ # تحقق مما إذا كانت الجملة تحتوي على مؤشر بند
+ if any(indicator in sentence for indicator in self.item_indicators):
+ # تحديد الفئة
+ category = self._determine_category(sentence)
+
+ # تحديد الأهمية
+ importance = self._determine_importance(sentence)
+
+ # إضافة البند إلى القائمة
+ items.append({
+ 'رقم البند': f"I{item_id:03d}",
+ 'وصف البند': sentence.strip(),
+ 'الفئة': category,
+ 'الأهمية': importance,
+ 'الثقة': round(np.random.uniform(0.75, 0.95), 2) # محاكاة ثقة التعرف
+ })
+
+ item_id += 1
+
+ # تحويل القائمة إلى DataFrame
+ items_df = pd.DataFrame(items)
+
+ # التأكد من وجود بيانات
+ if items_df.empty:
+ # إنشاء DataFrame فارغ بالأعمدة المطلوبة
+ items_df = pd.DataFrame(columns=[
+ 'رقم البند', 'وصف البند', 'الفئة', 'الأهمية', 'الثقة'
+ ])
+
+ return items_df
+
+ def _determine_category(self, text):
+ """تحديد فئة البند بناءً على محتواه"""
+ # البحث عن الكلمات المفتاحية في النص
+ scores = {}
+
+ for category, keywords in self.categories.items():
+ score = sum(1 for keyword in keywords if keyword in text.lower())
+ scores[category] = score
+
+ # اختيار الفئة ذات الدرجة الأعلى
+ if max(scores.values()) > 0:
+ return max(scores.items(), key=lambda x: x[1])[0]
+ else:
+ return "أخرى"
+
+ def _determine_importance(self, text):
+ """تحديد أهمية البند بناءً على محتواه"""
+ # كلمات تشير إلى أهمية عالية
+ high_importance_words = [
+ 'ضروري', 'هام', 'أساسي', 'رئيسي', 'كبير', 'مهم',
+ 'حرج', 'أمان', 'سلامة', 'صحة', 'بيئة'
+ ]
+
+ # كلمات تشير إلى أهمية منخفضة
+ low_importance_words = [
+ 'ثانوي', 'إضافي', 'تجميلي', 'مكمل', 'اختياري'
+ ]
+
+ # حساب درجة الأهمية
+ high_score = sum(1 for word in high_importance_words if word in text.lower())
+ low_score = sum(1 for word in low_importance_words if word in text.lower())
+
+ # تحديد الأهمية بناءً على الدرجات
+ if high_score > low_score:
+ return "عالية"
+ elif low_score > high_score:
+ return "منخفضة"
+ else:
+ return "متوسطة"
\ No newline at end of file
diff --git a/modules/services/quantity_extractor.py b/modules/services/quantity_extractor.py
new file mode 100644
index 0000000000000000000000000000000000000000..19b3c9d1427c49cfffd7da8237854efc74f8d128
--- /dev/null
+++ b/modules/services/quantity_extractor.py
@@ -0,0 +1,182 @@
+"""
+خدمة استخراج الكميات من المستندات
+"""
+
+import re
+import pandas as pd
+import numpy as np
+from pathlib import Path
+import config
+
+class QuantityExtractor:
+ """استخراج الكميات من المستندات"""
+
+ def __init__(self):
+ # وحدات القياس الشائعة
+ self.units = {
+ 'أعمال الخرسانة': 'م3',
+ 'أعمال الحفر': 'م3',
+ 'أعمال الردم': 'م3',
+ 'حديد التسليح': 'طن',
+ 'أعمال البلاط': 'م2',
+ 'أعمال السيراميك': 'م2',
+ 'أعمال الرخام': 'م2',
+ 'أعمال البلوك': 'م2',
+ 'أعمال الدهان': 'م2',
+ 'أعمال اللياسة': 'م2',
+ 'أعمال العزل': 'م2',
+ 'أعمال تمديدات الكهرباء': 'نقطة',
+ 'أعمال تمديدات السباكة': 'نقطة',
+ 'أعمال الأبواب': 'عدد',
+ 'أعمال النوافذ': 'عدد',
+ 'أعمال مجاري التكييف': 'م.ط',
+ 'أعمال الرصف': 'م2',
+ 'أعمال التسوية': 'م2',
+ 'مواسير الصرف': 'م.ط',
+ 'مواسير المياه': 'م.ط'
+ }
+
+ # تعبيرات منتظمة لاستخراج الأرقام والوحدات
+ self.number_pattern = r'(\d+(?:,\d+)*(?:\.\d+)?)'
+ self.unit_pattern = r'(م3|م2|طن|م\.ط|نقطة|عدد|وحدة)'
+
+ def extract_quantities(self, text, excel_data=None):
+ """استخراج الكميات من النص أو بيانات Excel"""
+ quantities = []
+
+ # إذا كانت البيانات من Excel
+ if excel_data is not None:
+ quantities = self._extract_from_excel(excel_data)
+ # وإلا استخراج من النص
+ elif text:
+ quantities = self._extract_from_text(text)
+
+ # تحويل القائمة إلى DataFrame
+ quantities_df = pd.DataFrame(quantities)
+
+ # التأكد من وجود بيانات
+ if quantities_df.empty:
+ # إنشاء DataFrame فارغ بالأعمدة المطلوبة
+ quantities_df = pd.DataFrame(columns=[
+ 'رقم البند', 'وصف العمل', 'الوحدة', 'الكمية المستخرجة',
+ 'الثقة', 'الملاحظات'
+ ])
+
+ return quantities_df
+
+ def _extract_from_excel(self, excel_data):
+ """استخراج الكميات من بيانات Excel"""
+ quantities = []
+ item_id = 1
+
+ # التحقق من وجود أعمدة مهمة
+ required_cols = ['الوصف', 'البند', 'الكمية', 'الوحدة']
+ present_cols = [col for col in required_cols if any(col in str(c).lower() for c in excel_data.columns)]
+
+ if not present_cols:
+ return quantities
+
+ # تحديد أعمدة البيانات
+ desc_col = next((c for c in excel_data.columns if 'وصف' in str(c).lower() or 'بند' in str(c).lower()), None)
+ qty_col = next((c for c in excel_data.columns if 'كمية' in str(c).lower() or 'عدد' in str(c).lower()), None)
+ unit_col = next((c for c in excel_data.columns if 'وحدة' in str(c).lower()), None)
+
+ if not (desc_col and qty_col):
+ return quantities
+
+ # استخراج الكميات من كل صف
+ for _, row in excel_data.iterrows():
+ if pd.notna(row[desc_col]) and pd.notna(row[qty_col]):
+ description = str(row[desc_col]).strip()
+
+ # تجاهل الصفوف الفارغة أو العناوين
+ if len(description) < 5 or description.isupper():
+ continue
+
+ # استخراج الكمية والوحدة
+ quantity = float(row[qty_col]) if pd.notna(row[qty_col]) else 0
+ unit = str(row[unit_col]).strip() if unit_col and pd.notna(row[unit_col]) else self._determine_unit(description)
+
+ # إضافة البند إلى القائمة
+ quantities.append({
+ 'رقم البند': f"Q{item_id:03d}",
+ 'وصف العمل': description,
+ 'الوحدة': unit,
+ 'الكمية المستخرجة': quantity,
+ 'الثقة': round(np.random.uniform(0.85, 0.99), 2),
+ 'الملاحظات': "تم استخراج الكمية من جدول الكميات"
+ })
+
+ item_id += 1
+
+ return quantities
+
+ def _extract_from_text(self, text):
+ """استخراج الكميات من النص"""
+ quantities = []
+ item_id = 1
+
+ # البحث عن العبارات التي تحتوي على أرقام ووحدات
+ lines = text.split('\n')
+
+ for line in lines:
+ # البحث عن أعمال محددة
+ for work_type in self.units.keys():
+ if work_type in line:
+ # البحث عن الأرقام في النص
+ numbers = re.findall(self.number_pattern, line)
+
+ if numbers:
+ # اختيار أول رقم (الأكثر احتمالاً أن يكون الكمية)
+ quantity = float(numbers[0].replace(',', ''))
+ unit = self.units[work_type]
+
+ # إضافة البند إلى القائمة
+ quantities.append({
+ 'رقم البند': f"Q{item_id:03d}",
+ 'وصف العمل': work_type,
+ 'الوحدة': unit,
+ 'الكمية المستخرجة': quantity,
+ 'الثقة': round(np.random.uniform(0.7, 0.9), 2),
+ 'الملاحظات': "تم حساب الكمية من النص"
+ })
+
+ item_id += 1
+ break
+
+ # البحث عن وحدات قياس في النص
+ unit_matches = re.findall(self.unit_pattern, line)
+ if unit_matches and re.search(self.number_pattern, line):
+ numbers = re.findall(self.number_pattern, line)
+
+ if numbers:
+ # اختيار أول رقم وأول وحدة
+ quantity = float(numbers[0].replace(',', ''))
+ unit = unit_matches[0]
+
+ # استخراج وصف العمل - أول 50 حرف من النص
+ description = line[:50] + "..." if len(line) > 50 else line
+
+ # إضافة البند إلى القائمة (إذا لم يتم إضافته بالفعل)
+ if not any(q['وصف العمل'] == description for q in quantities):
+ quantities.append({
+ 'رقم البند': f"Q{item_id:03d}",
+ 'وصف العمل': description,
+ 'الوحدة': unit,
+ 'الكمية المستخرجة': quantity,
+ 'الثقة': round(np.random.uniform(0.6, 0.85), 2),
+ 'الملاحظات': "تم استخراج الكمية من النص"
+ })
+
+ item_id += 1
+
+ return quantities
+
+ def _determine_unit(self, description):
+ """تحديد وحدة القياس المناسبة بناءً على وصف العمل"""
+ for work_type, unit in self.units.items():
+ if work_type in description:
+ return unit
+
+ # افتراضي إذا لم يتم العثور على وحدة مناسبة
+ return "وحدة"
\ No newline at end of file
diff --git a/modules/services/risk_analyzer.py b/modules/services/risk_analyzer.py
new file mode 100644
index 0000000000000000000000000000000000000000..55eb9bfc21ca5cad78c518053bda36399bda70e1
--- /dev/null
+++ b/modules/services/risk_analyzer.py
@@ -0,0 +1,219 @@
+"""
+خدمة تحليل المخاطر في المستندات
+"""
+
+import re
+import pandas as pd
+import numpy as np
+from nltk.tokenize import sent_tokenize
+import config
+
+class RiskAnalyzer:
+ """تحليل المخاطر في المستندات"""
+
+ def __init__(self):
+ # قائمة بالمصطلحات التي تشير إلى المخاطر
+ self.risk_indicators = {
+ 'مخاطر مالية': [
+ 'غرامة', 'عقوبة', 'تعويض', 'دفعة', 'ضمان', 'تأخير', 'سعر',
+ 'تكلفة', 'زيادة', 'تمويل', 'استرداد', 'مصادرة', 'كفالة',
+ 'مستحقات', 'فاتورة', 'سداد', 'دفع', 'مطالبة', 'تقلبات'
+ ],
+ 'مخاطر زمنية': [
+ 'مدة', 'فترة', 'تاريخ', 'موعد', 'تأخير', 'جدول زمني', 'تمديد',
+ 'تسليم', 'تسريع', 'إنجاز', 'تنفيذ', 'انتهاء', 'بدء', 'تعليق'
+ ],
+ 'مخاطر فنية': [
+ 'مواصفات', 'معايير', 'اختبار', 'فحص', 'جودة', 'عيب', 'خلل',
+ 'تقنية', 'فني', 'تصميم', 'أداء', 'مخططات', 'تشغيل', 'صيانة'
+ ],
+ 'مخاطر إدارية': [
+ 'مراسلات', 'اجتماع', 'تنسيق', 'تواصل', 'إشراف', 'إدارة',
+ 'تغيير', 'تعديل', 'موافقة', 'رفض', 'تفويض', 'صلاحية'
+ ],
+ 'مخاطر تنظيمية': [
+ 'لائحة', 'تصريح', 'ترخيص', 'قانون', 'نظام', 'حكومي', 'بلدية',
+ 'تشريع', 'امتثال', 'تعميم', 'شهادة', 'موافقة'
+ ],
+ 'مخاطر سوقية': [
+ 'توريد', 'مورد', 'سوق', 'منافسة', 'مواد', 'نقص', 'تقلب', 'أسعار',
+ 'استيراد', 'تصدير', 'جمارك', 'نقل', 'تخزين'
+ ],
+ }
+
+ # قائمة بالمصطلحات التي تشير إلى تأثير المخاطر
+ self.impact_indicators = {
+ 'عالي': [
+ 'كبير', 'خطير', 'جسيم', 'كلي', 'مرتفع', 'عالي', 'ضخم', 'هام',
+ 'جوهري', 'أساسي', 'رئيسي'
+ ],
+ 'متوسط': [
+ 'متوسط', 'معتدل', 'وسط', 'مقبول', 'عادي', 'معقول'
+ ],
+ 'منخفض': [
+ 'صغير', 'قليل', 'ضئيل', 'بسيط', 'منخفض', 'هامشي', 'محدود',
+ 'طفيف', 'غير مؤثر'
+ ]
+ }
+
+ # قائمة بالمصطلحات التي تشير إلى احتمالية المخاطر
+ self.probability_indicators = {
+ 'مؤكد': [
+ 'مؤكد', 'حتمي', 'قطعي', 'دائماً', 'يجب', 'ملزم', 'إلزامي',
+ 'مطلوب'
+ ],
+ 'محتمل': [
+ 'محتمل', 'ممكن', 'قد', 'ربما', 'يمكن', 'متوقع'
+ ],
+ 'غير محتمل': [
+ 'نادر', 'بعيد', 'استثنائي', 'غير متوقع', 'غير محتمل', 'ضئيل'
+ ]
+ }
+
+ # استراتيجيات معالجة المخاطر
+ self.mitigation_strategies = {
+ 'مخاطر مالية': [
+ "تخصيص مبلغ احتياطي",
+ "التفاوض مع العميل لتخفيف الشروط المالية",
+ "تحديد سقف للغرامات",
+ "التخطيط للتدفق النقدي",
+ "تأمين خط ائتمان احتياطي"
+ ],
+ 'مخاطر زمنية': [
+ "زيادة فريق العمل",
+ "استخدام موارد إضافية",
+ "وضع خطة عمل بديلة",
+ "استباق التأخيرات المحتملة",
+ "تقديم طلب تمديد مسبق"
+ ],
+ 'مخاطر فنية': [
+ "طلب توضيح من العميل",
+ "استشارة خبراء متخصصين",
+ "إجراء اختبارات إضافية",
+ "توثيق المراسلات الفنية",
+ "تعيين مسؤول ضبط جودة"
+ ],
+ 'مخاطر إدارية': [
+ "تحسين آليات التواصل",
+ "توثيق جميع المراسلات",
+ "وضع خطة اتصال واضحة",
+ "عقد اجتماعات دورية",
+ "تعيين مدير مشروع متفرغ"
+ ],
+ 'مخاطر تنظيمية': [
+ "التخطيط المسبق للمتطلبات التنظيمية",
+ "التواصل مع الجهات المعنية",
+ "الاستعانة بمستشار قانوني",
+ "متابعة التغييرات التنظيمية",
+ "تجهيز الوثائق المطلوبة مبكراً"
+ ],
+ 'مخاطر سوقية': [
+ "تثبيت أسعار المواد مع الموردين",
+ "البحث عن موردين بدلاء",
+ "شراء المواد الرئيسية مبكراً",
+ "إبرام عقود توريد طويلة الأجل",
+ "مراقبة تقلبات السوق"
+ ]
+ }
+
+ def analyze_risks(self, text):
+ """تحليل المخاطر في النص المعطى"""
+ if not text:
+ return pd.DataFrame()
+
+ # تقسيم النص إلى جمل
+ sentences = sent_tokenize(text)
+
+ # تحليل المخاطر في كل جملة
+ risks = []
+ risk_id = 1
+
+ for sentence in sentences:
+ # تحديد نوع المخاطرة إذا وجدت
+ risk_category = self._determine_risk_category(sentence)
+
+ if risk_category:
+ # تحديد التأثير والاحتمالية
+ impact = self._determine_impact(sentence)
+ probability = self._determine_probability(sentence)
+
+ # اختيار استراتيجية المعالجة
+ mitigation = np.random.choice(self.mitigation_strategies.get(risk_category, ["مراجعة فريق المخاطر"]))
+
+ # إضافة المخاطرة إلى القائمة
+ risks.append({
+ 'رقم المخاطرة': f"R{risk_id:02d}",
+ 'وصف المخاطرة': sentence.strip(),
+ 'الفئة': risk_category,
+ 'التأثير': impact,
+ 'الاحتمالية': probability,
+ 'استراتيجية المعالجة': mitigation
+ })
+
+ risk_id += 1
+
+ # تحويل القائمة إلى DataFrame
+ risks_df = pd.DataFrame(risks)
+
+ # التأكد من وجود بيانات
+ if risks_df.empty:
+ # إنشاء DataFrame فارغ بالأعمدة المطلوبة
+ risks_df = pd.DataFrame(columns=[
+ 'رقم المخاطرة', 'وصف المخاطرة', 'الفئة',
+ 'التأثير', 'الاحتمالية', 'استراتيجية المعالجة'
+ ])
+
+ return risks_df
+
+ def _determine_risk_category(self, text):
+ """تحديد فئة المخاطرة بناءً على محتوى النص"""
+ # البحث عن الكلمات المفتاحية في النص
+ scores = {}
+
+ for category, indicators in self.risk_indicators.items():
+ score = sum(1 for indicator in indicators if indicator in text.lower())
+ scores[category] = score
+
+ # اختيار الفئة ذات الدرجة الأعلى إذا وجدت
+ if max(scores.values(), default=0) > 0:
+ return max(scores.items(), key=lambda x: x[1])[0]
+ else:
+ return None
+
+ def _determine_impact(self, text):
+ """تحديد تأثير المخاطرة بناءً على محتوى النص"""
+ # البحث عن الكلمات المفتاحية في النص
+ scores = {}
+
+ for impact, indicators in self.impact_indicators.items():
+ score = sum(1 for indicator in indicators if indicator in text.lower())
+ scores[impact] = score
+
+ # اختيار التأثير ذو الدرجة الأعلى
+ if max(scores.values(), default=0) > 0:
+ return max(scores.items(), key=lambda x: x[1])[0]
+ else:
+ # اختيار عشوائي مع ترجيح أكبر للتأثير المتوسط
+ return np.random.choice(
+ ["عالي", "متوسط", "منخفض"],
+ p=[0.3, 0.5, 0.2]
+ )
+
+ def _determine_probability(self, text):
+ """تحديد احتمالية المخاطرة بناءً على محتوى النص"""
+ # البحث عن الكلمات المفتاحية في النص
+ scores = {}
+
+ for probability, indicators in self.probability_indicators.items():
+ score = sum(1 for indicator in indicators if indicator in text.lower())
+ scores[probability] = score
+
+ # اختيار الاحتمالية ذات الدرجة الأعلى
+ if max(scores.values(), default=0) > 0:
+ return max(scores.items(), key=lambda x: x[1])[0]
+ else:
+ # اختيار عشوائي مع ترجيح أكبر للاحتمالية المتوسطة
+ return np.random.choice(
+ ["مؤكد", "محتمل", "غير محتمل"],
+ p=[0.2, 0.6, 0.2]
+ )
\ No newline at end of file
diff --git a/modules/services/specs_analyzer.py b/modules/services/specs_analyzer.py
new file mode 100644
index 0000000000000000000000000000000000000000..a1a128a36b9da7207e8c5196c5c7685dc2c8741f
--- /dev/null
+++ b/modules/services/specs_analyzer.py
@@ -0,0 +1,364 @@
+"""
+خدمة تحليل المواصفات من المستندات
+"""
+
+import re
+import pandas as pd
+import numpy as np
+import nltk
+from nltk.tokenize import sent_tokenize
+import config
+
+class SpecificationsAnalyzer:
+ """تحليل المواصفات الفنية في المستندات"""
+
+ def __init__(self):
+ # تحميل موارد NLTK إذا لم تكن موجودة
+ try:
+ nltk.data.find('tokenizers/punkt')
+ except LookupError:
+ nltk.download('punkt')
+
+ # فئات المواصفات الرئيسية
+ self.specification_categories = {
+ 'الخرسانة': [
+ 'خرسانة', 'اسمنت', 'رتبة', 'مقاومة', 'ضغط', 'شك', 'معالجة',
+ 'صب', 'قالب', 'قوالب', 'تسليح', 'خلطة', 'ركام', 'حصى'
+ ],
+ 'حديد التسليح': [
+ 'حديد', 'تسليح', 'قضبان', 'شد', 'جهد خضوع', 'درجة', 'قطر',
+ 'ربط', 'غطاء خرساني', 'تشكيل', 'ثني', 'شبكة'
+ ],
+ 'العزل المائي': [
+ 'عزل', 'مائي', 'رطوبة', 'بيتومين', 'لفائف', 'رولات', 'طبقة',
+ 'رش', 'تسرب', 'مانع تسرب', 'مقاومة الماء', 'حرارة'
+ ],
+ 'العزل الحراري': [
+ 'عزل', 'حراري', 'صوف صخري', 'صوف زجاجي', 'فوم', 'بوليسترين',
+ 'موصلية', 'انتقال الحرارة', 'بولي يوريثان'
+ ],
+ 'أعمال البلاط': [
+ 'بلاط', 'سيراميك', 'بورسلين', 'رخام', 'جرانيت', 'ترويبة',
+ 'لاصق', 'مونة', 'تركيب', 'مسافات', 'أبعاد'
+ ],
+ 'أعمال الدهان': [
+ 'دهان', 'طلاء', 'وجه تأسيس', 'وجه نهائي', 'رش', 'فرشاة',
+ 'رولة', 'معجون', 'مائي', 'زيتي', 'لامع', 'مطفي'
+ ],
+ 'المواد الكهربائية': [
+ 'كهرباء', 'أسلاك', 'كابلات', 'لوحات', 'مفاتيح', 'تمديدات',
+ 'جهد', 'قدرة', 'توزيع', 'تأريض', 'قواطع', 'تيار'
+ ],
+ 'أعمال السباكة': [
+ 'سباكة', 'مواسير', 'صرف', 'تغذية', 'مياه', 'بي في سي',
+ 'نحاس', 'حديد', 'خزان', 'مضخة', 'صمام', 'محبس'
+ ],
+ 'أعمال التكييف': [
+ 'تكييف', 'تبريد', 'تدفئة', 'مجاري هواء', 'دكت', 'مناولة',
+ 'تهوية', 'وحدة', 'مكيف', 'فلتر', 'مروحة'
+ ]
+ }
+
+ # المواصفات القياسية المعروفة
+ self.standard_specs = {
+ 'ASTM': {
+ 'C150': 'اسمنت بورتلاندي',
+ 'A615': 'حديد تسليح',
+ 'D6164': 'عزل مائي بيتوميني',
+ 'C33': 'ركام الخرسانة',
+ 'C494': 'إضافات الخرسانة',
+ 'C979': 'صبغات الخرسانة',
+ 'C578': 'عزل البوليسترين'
+ },
+ 'AASHTO': {
+ 'M85': 'اسمنت بورتلاندي',
+ 'M31': 'حديد تسليح',
+ 'M320': 'بيتومين للطرق'
+ },
+ 'IEC': {
+ '60502': 'كابلات الطاقة',
+ '60364': 'تمديدات كهربائية',
+ '61439': 'لوحات توزيع الطاقة'
+ },
+ 'BS': {
+ '8500': 'الخرسانة',
+ '4449': 'حديد التسليح',
+ '6700': 'أنظمة المياه',
+ '5950': 'المنشآت الفولاذية'
+ },
+ 'EN': {
+ '197-1': 'الاسمنت',
+ '10080': 'حديد التسليح',
+ '13162': 'العزل الحراري'
+ },
+ 'كود البناء السعودي': {
+ 'SBC 201': 'الأحمال',
+ 'SBC 304': 'الخرسانة الإنشائية',
+ 'SBC 305': 'المباني المعدنية',
+ 'SBC 501': 'السباكة',
+ 'SBC 401': 'الكهرباء',
+ 'SBC 601': 'البناء الصديق للبيئة'
+ }
+ }
+
+ def analyze_specifications(self, text):
+ """تحليل المواصفات الفنية من النص"""
+ if not text:
+ return {}, [], pd.DataFrame()
+
+ # تقسيم النص إلى جمل
+ sentences = sent_tokenize(text)
+
+ # استخراج المواصفات حسب الفئة
+ specs = {}
+ for category, keywords in self.specification_categories.items():
+ specs[category] = self._extract_category_specs(sentences, keywords, category)
+
+ # استخراج المتطلبات الخاصة
+ special_requirements = self._extract_special_requirements(sentences)
+
+ # استخراج متطلبات المحتوى المحلي
+ local_content = self._extract_local_content(sentences)
+
+ return specs, special_requirements, local_content
+
+ def _extract_category_specs(self, sentences, keywords, category):
+ """استخراج مواصفات فئة محددة من الجمل"""
+ category_specs = {}
+
+ # البحث عن الجمل التي تحتوي على الكلمات المفتاحية للفئة
+ category_sentences = [s for s in sentences if any(k in s.lower() for k in keywords)]
+
+ if not category_sentences:
+ return category_specs
+
+ # استخراج المواصفات حسب نوع الفئة
+ if category == 'الخرسانة':
+ # البحث عن قوة الضغط
+ for s in category_sentences:
+ if any(term in s.lower() for term in ['قوة', 'مقاومة', 'ضغط']):
+ match = re.search(r'(\d+)\s*(?:نيوتن|ميجا باسكال|نيوتن/مم²|MPa|N/mm)', s)
+ if match:
+ category_specs['قوة الضغط'] = f"{match.group(1)} نيوتن/مم²"
+
+ # البحث عن نسبة الماء للأسمنت
+ if any(term in s.lower() for term in ['نسبة', 'ماء', 'اسمنت']):
+ match = re.search(r'(\d+(?:\.\d+)?)\s*(?:%|نسبة)', s)
+ if match:
+ category_specs['نسبة الماء للأسمنت'] = f"{match.group(1)} كحد أقصى"
+
+ # البحث عن المعالجة
+ if 'معالجة' in s.lower():
+ match = re.search(r'(\d+)\s*(?:يوم|أيام)', s)
+ if match:
+ category_specs['المعالجة'] = f"لا تقل عن {match.group(1)} أيام"
+
+ # البحث عن المواصفات المرجعية
+ for std_org, std_codes in self.standard_specs.items():
+ for std_code, std_desc in std_codes.items():
+ if std_code in s and (std_org in s or category in std_desc.lower()):
+ category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}"
+
+ elif category == 'حديد التسليح':
+ # البحث عن نوع الحديد
+ for s in category_sentences:
+ if any(term in s.lower() for term in ['درجة', 'جهد', 'خضوع', 'grade']):
+ match = re.search(r'(?:درجة|جريد|Grade)\s*(\d+)', s, re.IGNORECASE)
+ if match:
+ category_specs['نوع الحديد'] = f"عالي المقاومة للشد (Grade {match.group(1)})"
+
+ # البحث عن إجهاد الخضوع
+ if any(term in s.lower() for term in ['إجهاد', 'خضوع', 'شد']):
+ match = re.search(r'(\d+)\s*(?:نيوتن|ميجا باسكال|نيوتن/مم²|MPa|N/mm)', s)
+ if match:
+ category_specs['إجهاد الخضوع'] = f"{match.group(1)} نيوتن/مم²"
+
+ # البحث عن المواصفات المرجعية
+ for std_org, std_codes in self.standard_specs.items():
+ for std_code, std_desc in std_codes.items():
+ if std_code in s and (std_org in s or category in std_desc.lower()):
+ category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}"
+
+ elif category == 'العزل المائي':
+ # البحث عن نوع العزل
+ for s in category_sentences:
+ if any(term in s.lower() for term in ['نوع', 'بيتومين', 'بوليستر', 'رول']):
+ if 'بيتومين' in s.lower() and 'بوليستر' in s.lower():
+ category_specs['النوع'] = 'أغشية بيتومينية مدعمة بالبوليستر'
+ elif 'بيتومين' in s.lower():
+ category_specs['النوع'] = 'أغشية بيتومينية'
+ elif 'pvc' in s.lower():
+ category_specs['النوع'] = 'أغشية PVC'
+
+ # البحث عن السماكة
+ if any(term in s.lower() for term in ['سماكة', 'سمك', 'مم']):
+ match = re.search(r'(\d+(?:\.\d+)?)\s*(?:مم|mm)', s, re.IGNORECASE)
+ if match:
+ category_specs['السماكة'] = f"{match.group(1)} مم"
+
+ # البحث عن مقاومة درجة الحرارة
+ if any(term in s.lower() for term in ['حرارة', 'درجة', 'مقاومة']):
+ match = re.search(r'(\d+)\s*(?:درجة|°)', s)
+ if match:
+ category_specs['مقاومة درجة الحرارة'] = f"حتى {match.group(1)} درجة مئوية"
+
+ # البحث عن المواصفات المرجعية
+ for std_org, std_codes in self.standard_specs.items():
+ for std_code, std_desc in std_codes.items():
+ if std_code in s and (std_org in s or 'عزل' in std_desc.lower()):
+ category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}"
+
+ elif category == 'المواد الكهربائية':
+ # البحث عن نوع الكابلات
+ for s in category_sentences:
+ if any(term in s.lower() for term in ['كابل', 'سلك', 'نحاس', 'ألمنيوم']):
+ if 'نحاس' in s.lower() and 'xlpe' in s.lower():
+ category_specs['الكابلات'] = 'نحاس معزول XLPE'
+ elif 'نحاس' in s.lower() and 'pvc' in s.lower():
+ category_specs['الكابلات'] = 'نحاس معزول PVC'
+ elif 'نحاس' in s.lower():
+ category_specs['الكابلات'] = 'نحاس معزول'
+ elif 'ألمنيوم' in s.lower():
+ category_specs['الكابلات'] = 'ألمنيوم معزول'
+
+ # البحث عن المواصفات المرجعية
+ for std_org, std_codes in self.standard_specs.items():
+ for std_code, std_desc in std_codes.items():
+ if std_code in s and (std_org in s or 'كهربا' in std_desc.lower()):
+ category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}"
+
+ # إذا لم يتم العثور على مواصفات محددة، أضف مواصفات افتراضية للفئات الرئيسية
+ if not category_specs and category in ['الخرسانة', 'حديد التسليح', 'العزل المائي', 'المواد الكهربائية']:
+ if category == 'الخرسانة':
+ category_specs = {
+ 'قوة الضغط': '30 نيوتن/مم²',
+ 'نسبة الماء للأسمنت': '0.45 كحد أقصى',
+ 'المعالجة': 'لا تقل عن 7 أيام',
+ 'المواصفات المرجعية': 'ASTM C150'
+ }
+ elif category == 'حديد التسليح':
+ category_specs = {
+ 'نوع الحديد': 'عالي المقاومة للشد (Grade 60)',
+ 'إجهاد الخضوع': '420 نيوتن/مم²',
+ 'المواصفات المرجعية': 'ASTM A615'
+ }
+ elif category == 'العزل المائي':
+ category_specs = {
+ 'النوع': 'أغشية بيتومينية مدعمة بالبوليستر',
+ 'السماكة': '4 مم',
+ 'مقاومة درجة الحرارة': 'حتى 100 درجة مئوية',
+ 'المواصفات المرجعية': 'ASTM D6164'
+ }
+ elif category == 'المواد الكهربائية':
+ category_specs = {
+ 'الكابلات': 'نحاس معزول XLPE',
+ 'المواصفات المرجعية': 'IEC 60502'
+ }
+
+ return category_specs
+
+ def _extract_special_requirements(self, sentences):
+ """استخراج المتطلبات الخاصة من الجمل"""
+ special_requirements = []
+
+ # الكلمات المفتاحية التي تشير إلى متطلبات خاصة
+ special_keywords = [
+ 'يجب', 'ضرورة', 'يلزم', 'اشتراط', 'متطلب', 'إلزامي',
+ 'اعتماد', 'موافقة', 'تقديم', 'تأكيد', 'ضمان', 'توافق'
+ ]
+
+ # استخراج الجمل التي تحتوي على الكلمات المفتاحية
+ for s in sentences:
+ if any(keyword in s.lower() for keyword in special_keywords):
+ # تنظيف الجملة
+ req = s.strip()
+
+ # التأكد من أن الجملة تبدأ بيجب أو إذا لم تكن كذلك أضف "يجب" في البداية
+ if not any(req.startswith(start) for start in ['يجب', 'ضرورة', 'يلزم']):
+ req = f"يجب {req}"
+
+ # التأكد من أن الجملة تنتهي بنقطة
+ if not req.endswith('.'):
+ req = f"{req}."
+
+ # إضافة المتطلب إلى القائمة إذا لم يكن موجوداً بالفعل
+ if req not in special_requirements:
+ special_requirements.append(req)
+
+ # إضافة متطلبات افتراضية إذا لم يتم العثور على متطلبات
+ if not special_requirements:
+ special_requirements = [
+ "يجب أن تكون جميع المواد معتمدة من المهندس المشرف قبل التوريد.",
+ "يجب تقديم عينات لجميع المواد المستخدمة للاعتماد.",
+ "يجب تقديم شهادات ضمان لمدة سنة لجميع الأعمال المنفذة.",
+ "يجب الالتزام بكود البناء السعودي في جميع الأعمال.",
+ "يجب توفير اختبارات ضبط الجودة لأعمال الخرسانة.",
+ "يجب الالتزام بنسبة المحتوى المحلي لا تقل عن 70%."
+ ]
+
+ return special_requirements
+
+ def _extract_local_content(self, sentences):
+ """استخراج متطلبات المحتوى المحلي من الجمل"""
+ local_content_df = pd.DataFrame()
+
+ # الكلمات المفتاحية للمحتوى المحلي
+ lc_keywords = ['محتوى محلي', 'منتج وطني', 'صناعة محلية', 'توطين']
+
+ # استخراج الجمل التي تحتوي على كلمات مفتاحية للمحتوى المحلي
+ lc_sentences = [s for s in sentences if any(k in s.lower() for k in lc_keywords)]
+
+ # إذا وجدت جمل متعلقة بالمحتوى المحلي
+ if lc_sentences:
+ lc_data = []
+
+ # البحث عن نسب محددة في الجمل
+ for s in lc_sentences:
+ # البحث عن نسب مئوية
+ percentages = re.findall(r'(\d+)(?:\.\d+)?%', s)
+
+ if percentages:
+ # محاولة استخراج الفئة من الجملة
+ if 'عمال' in s.lower() or 'قوى' in s.lower() or 'موظف' in s.lower():
+ lc_data.append({
+ 'الفئة': 'القوى العاملة',
+ 'النسبة المطلوبة': f"{percentages[0]}%",
+ 'الملاحظات': 'تشمل العمالة والمهندسين والإداريين'
+ })
+ elif 'منتج' in s.lower() or 'صناع' in s.lower() or 'مواد' in s.lower() or 'معدات' in s.lower():
+ lc_data.append({
+ 'الفئة': 'المنتجات',
+ 'النسبة المطلوبة': f"{percentages[0]}%",
+ 'الملاحظات': 'تشمل المواد والمعدات المصنعة محلياً'
+ })
+ elif 'خدم' in s.lower() or 'نقل' in s.lower() or 'تأمين' in s.lower():
+ lc_data.append({
+ 'الفئة': 'الخدمات',
+ 'النسبة المطلوبة': f"{percentages[0]}%",
+ 'الملاحظات': 'تشمل خدمات النقل والتأمين والاستشارات'
+ })
+ else:
+ # إذا لم يتم تحديد الفئة، اعتبرها إجمالي
+ lc_data.append({
+ 'الفئة': 'إجمالي المشروع',
+ 'النسبة المطلوبة': f"{percentages[0]}%",
+ 'الملاحظات': 'نسبة المحتوى المحلي الإجمالية للمشروع'
+ })
+
+ # تحويل البيانات إلى DataFrame
+ if lc_data:
+ local_content_df = pd.DataFrame(lc_data)
+
+ # إذا لم يتم العثور على متطلبات محتوى محلي، استخدم بيانات افتراضية
+ if local_content_df.empty:
+ local_content_df = pd.DataFrame({
+ 'الفئة': ['القوى العاملة', 'المنتجات', 'الخدمات'],
+ 'النسبة المطلوبة': ['80%', '70%', '60%'],
+ 'الملاحظات': [
+ 'تشمل العمالة والمهندسين والإداريين',
+ 'تشمل المواد والمعدات المصنعة محلياً',
+ 'تشمل خدمات النقل والتأمين والاستشارات'
+ ]
+ })
+
+ return local_content_df
\ No newline at end of file
diff --git a/modules/services/text_extractor.py b/modules/services/text_extractor.py
new file mode 100644
index 0000000000000000000000000000000000000000..e042bda0c9777a5319c3160c4b21d4864e8ff5ba
--- /dev/null
+++ b/modules/services/text_extractor.py
@@ -0,0 +1,90 @@
+"""
+خدمة استخراج النصوص من المستندات
+"""
+
+import os
+import PyPDF2
+import docx
+import pandas as pd
+from pathlib import Path
+import config
+
+class TextExtractor:
+ """استخراج النصوص من المستندات المختلفة"""
+
+ def __init__(self):
+ pass
+
+ def extract_from_pdf(self, file_path):
+ """استخراج النص من ملف PDF"""
+ text = ""
+
+ try:
+ with open(file_path, 'rb') as file:
+ pdf_reader = PyPDF2.PdfReader(file)
+ for page_num in range(len(pdf_reader.pages)):
+ page = pdf_reader.pages[page_num]
+ text += page.extract_text() + "\n\n"
+ except Exception as e:
+ print(f"خطأ في استخراج النص من PDF: {str(e)}")
+ return ""
+
+ return text
+
+ def extract_from_docx(self, file_path):
+ """استخراج النص من ملف Word"""
+ text = ""
+
+ try:
+ doc = docx.Document(file_path)
+ for para in doc.paragraphs:
+ text += para.text + "\n"
+ except Exception as e:
+ print(f"خطأ في استخراج النص من DOCX: {str(e)}")
+ return ""
+
+ return text
+
+ def extract_from_excel(self, file_path):
+ """استخراج البيانات من ملف Excel"""
+ try:
+ # قراءة جميع الصفحات
+ excel_data = pd.read_excel(file_path, sheet_name=None)
+
+ # تجميع البيانات من جميع الصفحات
+ text = ""
+ for sheet_name, sheet_data in excel_data.items():
+ text += f"صفحة: {sheet_name}\n"
+ text += sheet_data.to_string(index=False) + "\n\n"
+ except Exception as e:
+ print(f"خطأ في استخراج النص من Excel: {str(e)}")
+ return ""
+
+ return text
+
+ def extract_from_text(self, file_path):
+ """استخراج النص من ملف نصي"""
+ try:
+ with open(file_path, 'r', encoding='utf-8') as file:
+ text = file.read()
+ except Exception as e:
+ print(f"خطأ في استخراج النص من الملف النصي: {str(e)}")
+ return ""
+
+ return text
+
+ def extract_text(self, file_path):
+ """استخراج النص من أي نوع ملف مدعوم"""
+ file_ext = Path(file_path).suffix.lower()
+
+ if file_ext == '.pdf':
+ return self.extract_from_pdf(file_path)
+ elif file_ext in ['.docx', '.doc']:
+ return self.extract_from_docx(file_path)
+ elif file_ext in ['.xlsx', '.xls']:
+ return self.extract_from_excel(file_path)
+ elif file_ext == '.txt':
+ return self.extract_from_text(file_path)
+ else:
+ print(f"نوع الملف غير مدعوم: {file_ext}")
+ return ""
\ No newline at end of file
diff --git a/modules/voice_narration/__init__.py b/modules/voice_narration/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1267f2ed042ede80e3b2f414c207a29153d69bd5
--- /dev/null
+++ b/modules/voice_narration/__init__.py
@@ -0,0 +1 @@
+# ملف تهيئة وحدة الترجمة الصوتية متعددة اللغات
\ No newline at end of file
diff --git a/modules/voice_narration/voice_narration_app.py b/modules/voice_narration/voice_narration_app.py
new file mode 100644
index 0000000000000000000000000000000000000000..65374e3adee4067da02770c7e3c59d1387ec7ce2
--- /dev/null
+++ b/modules/voice_narration/voice_narration_app.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+وحدة تطبيق الترجمة الصوتية متعددة اللغات لتفاصيل المشروع
+"""
+
+import os
+import sys
+import streamlit as st
+import pandas as pd
+import numpy as np
+
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
+
+# استيراد مكونات الترجمة الصوتية
+from modules.voice_narration.voice_over_system import VoiceOverSystem
+
+
+class VoiceNarrationApp:
+ """وحدة تطبيق الترجمة الصوتية متعددة اللغات"""
+
+ def __init__(self):
+ """تهيئة وحدة تطبيق الترجمة الصوتية متعددة اللغات"""
+ self.voice_over_system = VoiceOverSystem()
+
+ def render(self):
+ """عرض واجهة وحدة تطبيق الترجمة الصوتية متعددة اللغات"""
+ st.markdown("
نظام الترجمة الصوتية متعددة اللغات لتفاصيل المشروع
", unsafe_allow_html=True)
+
+ st.markdown("""
+
+ يتيح لك نظام الترجمة الصوتية متعددة اللغات تحويل النصوص والمستندات إلى ملفات صوتية بلغات متعددة،
+ مما يساعد في توصيل معلومات المشاريع والعقود والمناقصات بشكل أفضل للأشخاص من خلفيات لغوية مختلفة.
+
+ """, unsafe_allow_html=True)
+
+ # عرض نظام الترجمة الصوتية
+ self.voice_over_system.render()
+
+
+# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
+if __name__ == "__main__":
+ st.set_page_config(
+ page_title="الترجمة الصوتية متعددة اللغات | WAHBi AI",
+ page_icon="🎙️",
+ layout="wide",
+ initial_sidebar_state="expanded"
+ )
+
+ app = VoiceNarrationApp()
+ app.render()
\ No newline at end of file
diff --git a/modules/voice_narration/voice_over_system.py b/modules/voice_narration/voice_over_system.py
new file mode 100644
index 0000000000000000000000000000000000000000..ccdf36e7dcd837fb8df7c2174594281e6c33974e
--- /dev/null
+++ b/modules/voice_narration/voice_over_system.py
@@ -0,0 +1,1916 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+وحدة الترجمة الصوتية متعددة اللغات لتفاصيل المشروع
+تتيح هذه الوحدة تحويل محتوى المشروع النصي إلى مقاطع صوتية بلغات متعددة للتسهيل على المستخدمين
+"""
+
+import os
+import sys
+import streamlit as st
+import pandas as pd
+import numpy as np
+import json
+import base64
+import tempfile
+import time
+import datetime
+import logging
+from typing import List, Dict, Any, Tuple, Optional, Union
+import io
+from io import BytesIO
+import re
+
+# إضافة مسار النظام للوصول للملفات المشتركة
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
+
+# استيراد مكونات واجهة المستخدم
+from utils.components.header import render_header
+from utils.components.credits import render_credits
+from utils.helpers import format_number, format_currency, styled_button
+
+
+class VoiceOverSystem:
+ """فئة نظام الترجمة الصوتية متعددة اللغات"""
+
+ def __init__(self):
+ """تهيئة نظام الترجمة الصوتية"""
+ # تهيئة مجلدات حفظ البيانات
+ self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/voice_narration"))
+ os.makedirs(self.data_dir, exist_ok=True)
+
+ # إعداد الكاش المحلي للملفات الصوتية
+ self.cache_dir = os.path.join(self.data_dir, "cache")
+ os.makedirs(self.cache_dir, exist_ok=True)
+
+ # تهيئة سجل الأصوات المخزنة في الكاش
+ self.cache_index_file = os.path.join(self.data_dir, "voice_cache_index.json")
+ self.cache_index = self._load_cache_index()
+
+ # تعيين اللغات المدعومة
+ self.supported_languages = {
+ "ar": "العربية",
+ "en": "الإنجليزية",
+ "fr": "الفرنسية",
+ "es": "الإسبانية",
+ "de": "الألمانية",
+ "it": "الإيطالية",
+ "zh": "الصينية",
+ "ja": "اليابانية",
+ "ru": "الروسية",
+ "tr": "التركية"
+ }
+
+ # تعيين الأصوات لكل لغة
+ self.voices_by_language = {
+ "ar": [
+ {"id": "ar-female-1", "name": "فاطمة", "gender": "أنثى"},
+ {"id": "ar-male-1", "name": "محمد", "gender": "ذكر"},
+ {"id": "ar-female-2", "name": "نور", "gender": "أنثى"},
+ {"id": "ar-male-2", "name": "أحمد", "gender": "ذكر"}
+ ],
+ "en": [
+ {"id": "en-female-1", "name": "Sarah", "gender": "أنثى"},
+ {"id": "en-male-1", "name": "John", "gender": "ذكر"},
+ {"id": "en-female-2", "name": "Emily", "gender": "أنثى"},
+ {"id": "en-male-2", "name": "Robert", "gender": "ذكر"}
+ ],
+ "fr": [
+ {"id": "fr-female-1", "name": "Marie", "gender": "أنثى"},
+ {"id": "fr-male-1", "name": "Jean", "gender": "ذكر"}
+ ],
+ "es": [
+ {"id": "es-female-1", "name": "Maria", "gender": "أنثى"},
+ {"id": "es-male-1", "name": "Carlos", "gender": "ذكر"}
+ ],
+ "de": [
+ {"id": "de-female-1", "name": "Hannah", "gender": "أنثى"},
+ {"id": "de-male-1", "name": "Max", "gender": "ذكر"}
+ ],
+ "it": [
+ {"id": "it-female-1", "name": "Sofia", "gender": "أنثى"},
+ {"id": "it-male-1", "name": "Marco", "gender": "ذكر"}
+ ],
+ "zh": [
+ {"id": "zh-female-1", "name": "Li Wei", "gender": "أنثى"},
+ {"id": "zh-male-1", "name": "Zhang Wei", "gender": "ذكر"}
+ ],
+ "ja": [
+ {"id": "ja-female-1", "name": "Yuki", "gender": "أنثى"},
+ {"id": "ja-male-1", "name": "Hiroshi", "gender": "ذكر"}
+ ],
+ "ru": [
+ {"id": "ru-female-1", "name": "Olga", "gender": "أنثى"},
+ {"id": "ru-male-1", "name": "Ivan", "gender": "ذكر"}
+ ],
+ "tr": [
+ {"id": "tr-female-1", "name": "Ayşe", "gender": "أنثى"},
+ {"id": "tr-male-1", "name": "Mehmet", "gender": "ذكر"}
+ ]
+ }
+
+ # إعدادات الصوت الافتراضية
+ if "voice_settings" not in st.session_state:
+ st.session_state.voice_settings = {
+ "primary_language": "ar",
+ "secondary_language": "en",
+ "primary_voice": "ar-female-1",
+ "secondary_voice": "en-female-1",
+ "speaking_rate": 1.0,
+ "pitch": 0.0,
+ "auto_translate": True,
+ "include_subtitles": True,
+ "emphasis_keywords": True
+ }
+
+ # تحميل تاريخ التحويلات الصوتية
+ self.voice_history_file = os.path.join(self.data_dir, "voice_history.json")
+ self.voice_history = self._load_voice_history()
+
+ # تسجيل الأحداث
+ logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ handlers=[
+ logging.FileHandler(os.path.join(self.data_dir, "voice_narration.log")),
+ logging.StreamHandler()
+ ]
+ )
+ self.logger = logging.getLogger("voice_over_system")
+
+ def render(self):
+ """عرض واجهة نظام الترجمة الصوتية متعددة اللغات"""
+ render_header("نظام الترجمة الصوتية متعددة اللغات")
+
+ # تبويبات الوحدة
+ tabs = st.tabs([
+ "إنشاء ترجمة صوتية",
+ "مدير الترجمات الصوتية",
+ "إعدادات الصوت",
+ "ترجمة مستندات كاملة",
+ "إحصائيات ومقاييس"
+ ])
+
+ # تبويب إنشاء ترجمة صوتية
+ with tabs[0]:
+ self._render_create_voice_over()
+
+ # تبويب مدير الترجمات الصوتية
+ with tabs[1]:
+ self._render_voice_over_manager()
+
+ # تبويب إعدادات الصوت
+ with tabs[2]:
+ self._render_voice_settings()
+
+ # تبويب ترجمة مستندات كاملة
+ with tabs[3]:
+ self._render_document_narration()
+
+ # تبويب إحصائيات ومقاييس
+ with tabs[4]:
+ self._render_voice_over_analytics()
+
+ # عرض حقوق النشر
+ render_credits()
+
+ def _render_create_voice_over(self):
+ """عرض واجهة إنشاء ترجمة صوتية"""
+ st.markdown("""
+
+
🎙️ إنشاء ترجمة صوتية
+
إنشاء ترجمة صوتية لنص معين بلغات متعددة.
+
+ """, unsafe_allow_html=True)
+
+ # اختيار نوع المحتوى
+ content_type = st.radio(
+ "نوع المحتوى",
+ options=["نص حر", "بيانات مشروع", "ملخص مناقصة", "بنود عقد"],
+ horizontal=True,
+ key="voice_content_type"
+ )
+
+ # مقدار النص الذي سيتم عرضه بناءً على نوع المحتوى
+ if content_type == "نص حر":
+ content_text = st.text_area(
+ "النص المراد تحويله إلى صوت",
+ height=150,
+ placeholder="أدخل النص الذي ترغب في تحويله إلى صوت هنا...",
+ key="voice_content_text"
+ )
+
+ title = st.text_input(
+ "عنوان الملف الصوتي",
+ placeholder="عنوان لتسهيل الوصول للملف الصوتي لاحقاً",
+ key="voice_title"
+ )
+
+ elif content_type == "بيانات مشروع":
+ # عرض المشاريع المتاحة في النظام
+ projects = self._get_projects()
+
+ if projects:
+ selected_project_id = st.selectbox(
+ "اختر المشروع",
+ options=[p["id"] for p in projects],
+ format_func=lambda x: next((p["name"] for p in projects if p["id"] == x), ""),
+ key="voice_project_id"
+ )
+
+ # العثور على المشروع المحدد
+ selected_project = next((p for p in projects if p["id"] == selected_project_id), None)
+
+ if selected_project:
+ # نعرض بيانات المشروع
+ st.subheader(f"بيانات المشروع: {selected_project['name']}")
+
+ project_details = f"""
+ اسم المشروع: {selected_project['name']}
+ رقم المشروع: {selected_project['id']}
+ الحالة: {selected_project.get('status', 'غير محدد')}
+ الموقع: {selected_project.get('location', 'غير محدد')}
+ تاريخ البدء: {selected_project.get('start_date', 'غير محدد')}
+ تاريخ الانتهاء المتوقع: {selected_project.get('expected_end_date', 'غير محدد')}
+ الميزانية: {selected_project.get('budget', 'غير محدد')}
+
+ وصف المشروع: {selected_project.get('description', 'لا يوجد وصف متاح')}
+ """
+
+ st.text_area(
+ "تفاصيل المشروع (يمكنك تعديلها قبل التحويل إلى صوت)",
+ value=project_details,
+ height=250,
+ key="voice_project_details"
+ )
+
+ content_text = st.session_state.voice_project_details
+ title = f"ملخص مشروع {selected_project['name']}"
+ else:
+ st.warning("لم يتم العثور على المشروع المحدد")
+ content_text = ""
+ title = ""
+ else:
+ st.info("لا توجد مشاريع متاحة حالياً")
+ content_text = ""
+ title = ""
+
+ elif content_type == "ملخص مناقصة":
+ # عرض المناقصات المتاحة في النظام
+ tenders = self._get_tenders()
+
+ if tenders:
+ selected_tender_id = st.selectbox(
+ "اختر المناقصة",
+ options=[t["id"] for t in tenders],
+ format_func=lambda x: next((t["name"] for t in tenders if t["id"] == x), ""),
+ key="voice_tender_id"
+ )
+
+ # العثور على المناقصة المحددة
+ selected_tender = next((t for t in tenders if t["id"] == selected_tender_id), None)
+
+ if selected_tender:
+ # نعرض بيانات المناقصة
+ st.subheader(f"بيانات المناقصة: {selected_tender['name']}")
+
+ tender_details = f"""
+ اسم المناقصة: {selected_tender['name']}
+ رقم المناقصة: {selected_tender['id']}
+ الجهة المالكة: {selected_tender.get('owner', 'غير محدد')}
+ تاريخ الطرح: {selected_tender.get('issue_date', 'غير محدد')}
+ تاريخ التسليم: {selected_tender.get('submission_date', 'غير محدد')}
+ القيمة التقديرية: {selected_tender.get('estimated_value', 'غير محدد')}
+
+ وصف المناقصة: {selected_tender.get('description', 'لا يوجد وصف متاح')}
+ """
+
+ st.text_area(
+ "تفاصيل المناقصة (يمكنك تعديلها قبل التحويل إلى صوت)",
+ value=tender_details,
+ height=250,
+ key="voice_tender_details"
+ )
+
+ content_text = st.session_state.voice_tender_details
+ title = f"ملخص مناقصة {selected_tender['name']}"
+ else:
+ st.warning("لم يتم العثور على المناقصة المحددة")
+ content_text = ""
+ title = ""
+ else:
+ st.info("لا توجد مناقصات متاحة حالياً")
+ content_text = ""
+ title = ""
+
+ elif content_type == "بنود عقد":
+ # عرض العقود المتاحة في النظام
+ contracts = self._get_contracts()
+
+ if contracts:
+ selected_contract_id = st.selectbox(
+ "اختر العقد",
+ options=[c["id"] for c in contracts],
+ format_func=lambda x: next((c["name"] for c in contracts if c["id"] == x), ""),
+ key="voice_contract_id"
+ )
+
+ # العثور على العقد المحدد
+ selected_contract = next((c for c in contracts if c["id"] == selected_contract_id), None)
+
+ if selected_contract:
+ # نعرض بيانات العقد
+ st.subheader(f"بيانات العقد: {selected_contract['name']}")
+
+ # العثور على بنود العقد
+ contract_clauses = selected_contract.get("clauses", [])
+
+ if contract_clauses:
+ # السماح للمستخدم باختيار البنود التي يريد تحويلها
+ selected_clauses = st.multiselect(
+ "اختر البنود المراد تحويلها إلى صوت",
+ options=list(range(len(contract_clauses))),
+ format_func=lambda i: f"البند {i+1}: {contract_clauses[i]['title']}",
+ key="voice_contract_clauses"
+ )
+
+ if selected_clauses:
+ # تجميع النصوص المختارة
+ clauses_text = ""
+ for i in selected_clauses:
+ clauses_text += f"البند {i+1}: {contract_clauses[i]['title']}\n"
+ clauses_text += f"{contract_clauses[i]['content']}\n\n"
+
+ st.text_area(
+ "نص البنود المختارة (يمكنك تعديلها قبل التحويل إلى صوت)",
+ value=clauses_text,
+ height=250,
+ key="voice_contract_text"
+ )
+
+ content_text = st.session_state.voice_contract_text
+ title = f"بنود من عقد {selected_contract['name']}"
+ else:
+ st.info("الرجاء اختيار بند واحد على الأقل")
+ content_text = ""
+ title = ""
+ else:
+ st.info("لا توجد بنود متاحة لهذا العقد")
+ content_text = ""
+ title = ""
+ else:
+ st.warning("لم يتم العثور على العقد المحدد")
+ content_text = ""
+ title = ""
+ else:
+ st.info("لا توجد عقود متاحة حالياً")
+ content_text = ""
+ title = ""
+
+ # إعدادات اللغة للنص المدخل
+ st.markdown("### إعدادات اللغة")
+ col1, col2 = st.columns(2)
+
+ with col1:
+ source_language = st.selectbox(
+ "لغة النص المصدر",
+ options=list(self.supported_languages.keys()),
+ format_func=lambda x: self.supported_languages[x],
+ index=list(self.supported_languages.keys()).index(st.session_state.voice_settings["primary_language"]),
+ key="voice_source_language"
+ )
+
+ voice_id = st.selectbox(
+ "الصوت",
+ options=[v["id"] for v in self.voices_by_language[source_language]],
+ format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[source_language] if v["id"] == x), ""),
+ index=0,
+ key="voice_source_voice"
+ )
+
+ with col2:
+ target_language = st.selectbox(
+ "لغة الترجمة (اختياري)",
+ options=["none"] + list(self.supported_languages.keys()),
+ format_func=lambda x: "بدون ترجمة" if x == "none" else self.supported_languages[x],
+ index=0,
+ key="voice_target_language"
+ )
+
+ if target_language != "none":
+ target_voice_id = st.selectbox(
+ "صوت الترجمة",
+ options=[v["id"] for v in self.voices_by_language[target_language]],
+ format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[target_language] if v["id"] == x), ""),
+ index=0,
+ key="voice_target_voice"
+ )
+ else:
+ target_voice_id = None
+
+ # خيارات متقدمة
+ with st.expander("خيارات متقدمة"):
+ advanced_col1, advanced_col2 = st.columns(2)
+
+ with advanced_col1:
+ speaking_rate = st.slider(
+ "سرعة النطق",
+ min_value=0.5,
+ max_value=2.0,
+ value=st.session_state.voice_settings["speaking_rate"],
+ step=0.1,
+ key="voice_speaking_rate"
+ )
+
+ include_subtitles = st.checkbox(
+ "تضمين النص مع الصوت",
+ value=st.session_state.voice_settings["include_subtitles"],
+ key="voice_include_subtitles"
+ )
+
+ with advanced_col2:
+ pitch = st.slider(
+ "درجة الصوت",
+ min_value=-10.0,
+ max_value=10.0,
+ value=st.session_state.voice_settings["pitch"],
+ step=1.0,
+ key="voice_pitch"
+ )
+
+ emphasize_keywords = st.checkbox(
+ "تمييز الكلمات المهمة",
+ value=st.session_state.voice_settings["emphasis_keywords"],
+ key="voice_emphasize_keywords"
+ )
+
+ # شروط إنشاء الترجمة الصوتية
+ create_voice_over = False
+
+ if content_text:
+ # زر إنشاء الترجمة الصوتية
+ if styled_button("إنشاء الترجمة الصوتية", key="create_voice_over_btn", type="primary", icon="🎙️"):
+ if title:
+ create_voice_over = True
+ else:
+ st.warning("الرجاء إدخال عنوان للملف الصوتي")
+ else:
+ st.info("الرجاء إدخال أو اختيار محتوى للترجمة الصوتية")
+
+ # إنشاء الترجمة الصوتية
+ if create_voice_over:
+ with st.spinner("جاري إنشاء الترجمة الصوتية..."):
+ try:
+ # تحقق من وجود الملف في الكاش
+ cache_key = self._generate_cache_key(
+ content_text,
+ source_language,
+ voice_id,
+ speaking_rate,
+ pitch
+ )
+
+ cached_file = self._get_from_cache(cache_key)
+
+ if cached_file:
+ st.success("تم استرجاع الترجمة الصوتية من الكاش")
+ audio_file = cached_file
+ audio_duration = self._get_audio_duration(audio_file)
+ else:
+ # إنشاء الترجمة الصوتية
+ audio_file, audio_duration = self._generate_voice_over(
+ content_text,
+ source_language,
+ voice_id,
+ speaking_rate,
+ pitch
+ )
+
+ # حفظ الملف في الكاش
+ self._add_to_cache(cache_key, audio_file)
+
+ # تسجيل الترجمة الصوتية في التاريخ
+ voice_over_id = self._add_voice_to_history(
+ title=title,
+ content=content_text,
+ source_language=source_language,
+ voice_id=voice_id,
+ duration=audio_duration,
+ audio_file=os.path.basename(audio_file),
+ content_type=content_type
+ )
+
+ # ترجمة المحتوى إذا تم اختيار لغة ترجمة
+ if target_language != "none":
+ with st.spinner(f"جاري الترجمة إلى {self.supported_languages[target_language]}..."):
+ # ترجمة النص
+ translated_text = self._translate_text(
+ content_text,
+ source_language,
+ target_language
+ )
+
+ # تحقق من وجود الملف المترجم في الكاش
+ translated_cache_key = self._generate_cache_key(
+ translated_text,
+ target_language,
+ target_voice_id,
+ speaking_rate,
+ pitch
+ )
+
+ cached_translated_file = self._get_from_cache(translated_cache_key)
+
+ if cached_translated_file:
+ st.success("تم استرجاع الترجمة الصوتية المترجمة من الكاش")
+ translated_audio_file = cached_translated_file
+ translated_audio_duration = self._get_audio_duration(translated_audio_file)
+ else:
+ # إنشاء الترجمة الصوتية للنص المترجم
+ translated_audio_file, translated_audio_duration = self._generate_voice_over(
+ translated_text,
+ target_language,
+ target_voice_id,
+ speaking_rate,
+ pitch
+ )
+
+ # حفظ الملف المترجم في الكاش
+ self._add_to_cache(translated_cache_key, translated_audio_file)
+
+ # تسجيل الترجمة الصوتية المترجمة في التاريخ
+ translated_voice_over_id = self._add_voice_to_history(
+ title=f"{title} ({self.supported_languages[target_language]})",
+ content=translated_text,
+ source_language=target_language,
+ voice_id=target_voice_id,
+ duration=translated_audio_duration,
+ audio_file=os.path.basename(translated_audio_file),
+ content_type=content_type,
+ is_translation=True,
+ original_id=voice_over_id
+ )
+
+ # عرض الترجمة الصوتية المترجمة
+ st.subheader(f"الترجمة الصوتية بـ{self.supported_languages[target_language]}")
+
+ # عرض النص المترجم إذا تم اختيار ذلك
+ if include_subtitles:
+ st.markdown(f"**النص المترجم:**\n{translated_text}")
+
+ # عرض مشغل الصوت
+ self._display_audio_player(translated_audio_file)
+
+ # عرض الترجمة الصوتية
+ st.subheader(f"الترجمة الصوتية بـ{self.supported_languages[source_language]}")
+
+ # عرض النص إذا تم اختيار ذلك
+ if include_subtitles:
+ st.markdown(f"**النص:**\n{content_text}")
+
+ # عرض مشغل الصوت
+ self._display_audio_player(audio_file)
+
+ # زر تنزيل الملف الصوتي
+ with open(audio_file, "rb") as f:
+ audio_bytes = f.read()
+
+ st.download_button(
+ label="تنزيل الملف الصوتي",
+ data=audio_bytes,
+ file_name=f"{title}.mp3",
+ mime="audio/mpeg",
+ key="download_voice_over"
+ )
+
+ st.success("تم إنشاء الترجمة الصوتية بنجاح!")
+
+ except Exception as e:
+ st.error(f"حدث خطأ أثناء إنشاء الترجمة الصوتية: {str(e)}")
+ self.logger.error(f"خطأ في إنشاء الترجمة الصوتية: {str(e)}")
+
+ def _render_voice_over_manager(self):
+ """عرض واجهة مدير الترجمات الصوتية"""
+ st.markdown("""
+
+
🎧 مدير الترجمات الصوتية
+
استعراض وإدارة الترجمات الصوتية المخزنة.
+
+ """, unsafe_allow_html=True)
+
+ # تحديث تاريخ الترجمات الصوتية
+ self.voice_history = self._load_voice_history()
+
+ # التحقق من وجود ترجمات صوتية
+ if not self.voice_history:
+ st.info("لا توجد ترجمات صوتية مخزنة.")
+ return
+
+ # أزرار التحكم
+ col1, col2 = st.columns(2)
+
+ with col1:
+ # فلترة حسب نوع المحتوى
+ content_types = ["الكل"] + list(set(item.get("content_type", "نص حر") for item in self.voice_history))
+ filter_content_type = st.selectbox(
+ "فلترة حسب نوع المحتوى",
+ options=content_types,
+ key="filter_content_type"
+ )
+
+ with col2:
+ # فلترة حسب اللغة
+ languages = ["الكل"] + [self.supported_languages.get(item.get("source_language", "ar"), "العربية") for item in self.voice_history]
+ filter_language = st.selectbox(
+ "فلترة حسب اللغة",
+ options=list(set(languages)),
+ key="filter_language"
+ )
+
+ # فلترة العناصر
+ filtered_history = self.voice_history
+
+ if filter_content_type != "الكل":
+ filtered_history = [item for item in filtered_history if item.get("content_type", "نص حر") == filter_content_type]
+
+ if filter_language != "الكل":
+ filtered_history = [
+ item for item in filtered_history
+ if self.supported_languages.get(item.get("source_language", "ar"), "العربية") == filter_language
+ ]
+
+ # عرض الترجمات الصوتية
+ for voice_item in filtered_history:
+ with st.expander(f"{voice_item['title']} ({voice_item.get('created_at', 'تاريخ غير معروف')})", expanded=False):
+ # تفاصيل الترجمة الصوتية
+ item_col1, item_col2 = st.columns([3, 1])
+
+ with item_col1:
+ st.markdown(f"**النوع:** {voice_item.get('content_type', 'نص حر')}")
+ st.markdown(f"**اللغة:** {self.supported_languages.get(voice_item.get('source_language', 'ar'), 'العربية')}")
+ st.markdown(f"**المدة:** {voice_item.get('duration', 0):.2f} ثانية")
+
+ # عرض مشغل الصوت
+ audio_file_path = os.path.join(self.data_dir, voice_item.get('audio_file', ''))
+ if os.path.exists(audio_file_path):
+ self._display_audio_player(audio_file_path)
+ else:
+ st.warning("ملف الصوت غير متوفر")
+
+ with item_col2:
+ # عرض النص
+ if st.button("عرض النص", key=f"show_text_{voice_item.get('id', '')}"):
+ st.text_area(
+ "نص الترجمة الصوتية",
+ value=voice_item.get('content', ''),
+ height=150,
+ key=f"text_{voice_item.get('id', '')}",
+ disabled=True
+ )
+
+ # تنزيل الملف الصوتي
+ audio_file_path = os.path.join(self.data_dir, voice_item.get('audio_file', ''))
+ if os.path.exists(audio_file_path):
+ with open(audio_file_path, "rb") as f:
+ audio_bytes = f.read()
+
+ st.download_button(
+ label="تنزيل الملف الصوتي",
+ data=audio_bytes,
+ file_name=f"{voice_item['title']}.mp3",
+ mime="audio/mpeg",
+ key=f"download_{voice_item.get('id', '')}"
+ )
+
+ # حذف الترجمة الصوتية
+ if st.button("حذف", key=f"delete_{voice_item.get('id', '')}", type="primary"):
+ if self._delete_voice_from_history(voice_item.get('id', '')):
+ st.success("تم حذف الترجمة الصوتية بنجاح!")
+ st.rerun()
+ else:
+ st.error("حدث خطأ أثناء حذف الترجمة الصوتية")
+
+ def _render_voice_settings(self):
+ """عرض واجهة إعدادات الصوت"""
+ st.markdown("""
+
+
⚙️ إعدادات الصوت
+
تخصيص إعدادات الترجمة الصوتية الافتراضية.
+
+ """, unsafe_allow_html=True)
+
+ # إعدادات اللغة
+ st.markdown("### إعدادات اللغة")
+
+ lang_col1, lang_col2 = st.columns(2)
+
+ with lang_col1:
+ # اللغة الأساسية
+ primary_language = st.selectbox(
+ "اللغة الأساسية",
+ options=list(self.supported_languages.keys()),
+ format_func=lambda x: self.supported_languages[x],
+ index=list(self.supported_languages.keys()).index(st.session_state.voice_settings["primary_language"]),
+ key="settings_primary_language"
+ )
+
+ # الصوت الأساسي
+ primary_voice = st.selectbox(
+ "الصوت الأساسي",
+ options=[v["id"] for v in self.voices_by_language[primary_language]],
+ format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[primary_language] if v["id"] == x), ""),
+ index=0,
+ key="settings_primary_voice"
+ )
+
+ with lang_col2:
+ # اللغة الثانوية
+ secondary_language = st.selectbox(
+ "اللغة الثانوية",
+ options=list(self.supported_languages.keys()),
+ format_func=lambda x: self.supported_languages[x],
+ index=list(self.supported_languages.keys()).index(st.session_state.voice_settings["secondary_language"]),
+ key="settings_secondary_language"
+ )
+
+ # الصوت الثانوي
+ secondary_voice = st.selectbox(
+ "الصوت الثانوي",
+ options=[v["id"] for v in self.voices_by_language[secondary_language]],
+ format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[secondary_language] if v["id"] == x), ""),
+ index=0,
+ key="settings_secondary_voice"
+ )
+
+ # إعدادات جودة الصوت
+ st.markdown("### إعدادات جودة الصوت")
+
+ quality_col1, quality_col2 = st.columns(2)
+
+ with quality_col1:
+ # سرعة النطق
+ speaking_rate = st.slider(
+ "سرعة النطق الافتراضية",
+ min_value=0.5,
+ max_value=2.0,
+ value=st.session_state.voice_settings["speaking_rate"],
+ step=0.1,
+ key="settings_speaking_rate"
+ )
+
+ with quality_col2:
+ # درجة الصوت
+ pitch = st.slider(
+ "درجة الصوت الافتراضية",
+ min_value=-10.0,
+ max_value=10.0,
+ value=st.session_state.voice_settings["pitch"],
+ step=1.0,
+ key="settings_pitch"
+ )
+
+ # إعدادات أخرى
+ st.markdown("### إعدادات أخرى")
+
+ other_col1, other_col2 = st.columns(2)
+
+ with other_col1:
+ # الترجمة التلقائية
+ auto_translate = st.checkbox(
+ "ترجمة تلقائية إلى اللغة الثانوية",
+ value=st.session_state.voice_settings["auto_translate"],
+ key="settings_auto_translate"
+ )
+
+ # تضمين النص مع الصوت
+ include_subtitles = st.checkbox(
+ "تضمين النص مع الصوت افتراضياً",
+ value=st.session_state.voice_settings["include_subtitles"],
+ key="settings_include_subtitles"
+ )
+
+ with other_col2:
+ # تمييز الكلمات المهمة
+ emphasis_keywords = st.checkbox(
+ "تمييز الكلمات المهمة تلقائياً",
+ value=st.session_state.voice_settings["emphasis_keywords"],
+ key="settings_emphasis_keywords"
+ )
+
+ # زر حفظ الإعدادات
+ if styled_button("حفظ الإعدادات", key="save_voice_settings", type="primary", icon="💾"):
+ # تحديث الإعدادات
+ st.session_state.voice_settings = {
+ "primary_language": primary_language,
+ "secondary_language": secondary_language,
+ "primary_voice": primary_voice,
+ "secondary_voice": secondary_voice,
+ "speaking_rate": speaking_rate,
+ "pitch": pitch,
+ "auto_translate": auto_translate,
+ "include_subtitles": include_subtitles,
+ "emphasis_keywords": emphasis_keywords
+ }
+
+ # حفظ الإعدادات
+ self._save_voice_settings()
+
+ st.success("تم حفظ الإعدادات بنجاح!")
+
+ # إعدادات متقدمة
+ with st.expander("إعدادات متقدمة", expanded=False):
+ st.markdown("### إعدادات الكاش")
+
+ cache_size = self._get_cache_size()
+ st.markdown(f"حجم الكاش الحالي: {cache_size / (1024 * 1024):.2f} ميجابايت")
+
+ if styled_button("مسح الكاش", key="clear_cache", type="danger", icon="🗑️"):
+ if self._clear_cache():
+ st.success("تم مسح الكاش بنجاح!")
+ else:
+ st.error("حدث خطأ أثناء مسح الكاش")
+
+ st.markdown("### إعدادات API")
+
+ # نموذج API للترجمة الصوتية
+ api_model = st.selectbox(
+ "نموذج API للترجمة الصوتية",
+ options=["local", "huggingface", "google", "amazon", "microsoft"],
+ format_func=lambda x: {
+ "local": "محلي (عرض توضيحي)",
+ "huggingface": "Hugging Face",
+ "google": "Google Cloud Text-to-Speech",
+ "amazon": "Amazon Polly",
+ "microsoft": "Microsoft Azure"
+ }[x],
+ index=0,
+ key="api_model"
+ )
+
+ # معلومات حول النموذج المحدد
+ api_info = {
+ "local": "هذا وضع العرض التوضيحي حيث يتم تشبيه الترجمة الصوتية دون الحاجة إلى اتصال API خارجي.",
+ "huggingface": "استخدام Hugging Face API لتحويل النص إلى صوت وترجمة النصوص.",
+ "google": "استخدام Google Cloud Text-to-Speech لإنتاج صوت عالي الجودة.",
+ "amazon": "استخدام Amazon Polly للترجمة الصوتية بجودة عالية ومجموعة متنوعة من الأصوات.",
+ "microsoft": "استخدام Microsoft Azure Speech Services للترجمة الصوتية والترجمة."
+ }
+
+ st.markdown(f"**معلومات:** {api_info[api_model]}")
+
+ if api_model != "local":
+ api_key = st.text_input(
+ f"مفتاح API لـ {api_model}",
+ type="password",
+ key=f"{api_model}_api_key"
+ )
+
+ if styled_button("حفظ مفتاح API", key="save_api_key", type="primary"):
+ st.success(f"تم حفظ مفتاح API لـ {api_model} بنجاح!")
+
+ def _render_document_narration(self):
+ """عرض واجهة ترجمة مستندات كاملة"""
+ st.markdown("""
+
+
📄 ترجمة مستندات كاملة
+
تحويل مستندات كاملة إلى ملفات صوتية وتقسيمها إلى فصول أو أقسام.
+
+ """, unsafe_allow_html=True)
+
+ # اختيار المستند
+ documents = self._get_documents()
+
+ if documents:
+ selected_document_id = st.selectbox(
+ "اختر المستند",
+ options=[d["id"] for d in documents],
+ format_func=lambda x: next((d["name"] for d in documents if d["id"] == x), ""),
+ key="narration_document_id"
+ )
+
+ # العثور على المستند المحدد
+ selected_document = next((d for d in documents if d["id"] == selected_document_id), None)
+
+ if selected_document:
+ # عرض تفاصيل المستند
+ st.subheader(f"معلومات المستند: {selected_document['name']}")
+
+ doc_col1, doc_col2 = st.columns(2)
+
+ with doc_col1:
+ st.markdown(f"**النوع:** {selected_document.get('type', 'غير محدد')}")
+ st.markdown(f"**عدد الصفحات:** {selected_document.get('page_count', 'غير محدد')}")
+
+ with doc_col2:
+ st.markdown(f"**حجم المستند:** {selected_document.get('file_size', 'غير محدد')}")
+ st.markdown(f"**تاريخ الرفع:** {selected_document.get('upload_date', 'غير محدد')}")
+
+ # خيارات الترجمة الصوتية
+ st.markdown("### خيارات الترجمة الصوتية")
+
+ options_col1, options_col2 = st.columns(2)
+
+ with options_col1:
+ # اللغة
+ narration_language = st.selectbox(
+ "لغة الترجمة الصوتية",
+ options=list(self.supported_languages.keys()),
+ format_func=lambda x: self.supported_languages[x],
+ index=list(self.supported_languages.keys()).index(st.session_state.voice_settings["primary_language"]),
+ key="narration_language"
+ )
+
+ # الصوت
+ narration_voice = st.selectbox(
+ "الصوت",
+ options=[v["id"] for v in self.voices_by_language[narration_language]],
+ format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[narration_language] if v["id"] == x), ""),
+ index=0,
+ key="narration_voice"
+ )
+
+ # تقسيم المستند
+ narration_split = st.selectbox(
+ "تقسيم المستند",
+ options=["لا تقسيم", "حسب الصفحات", "حسب العناوين", "حسب الفصول"],
+ key="narration_split"
+ )
+
+ with options_col2:
+ # سرعة النطق
+ narration_speaking_rate = st.slider(
+ "سرعة النطق",
+ min_value=0.5,
+ max_value=2.0,
+ value=st.session_state.voice_settings["speaking_rate"],
+ step=0.1,
+ key="narration_speaking_rate"
+ )
+
+ # درجة الصوت
+ narration_pitch = st.slider(
+ "درجة الصوت",
+ min_value=-10.0,
+ max_value=10.0,
+ value=st.session_state.voice_settings["pitch"],
+ step=1.0,
+ key="narration_pitch"
+ )
+
+ # تضمين الفهرس
+ narration_include_toc = st.checkbox(
+ "تضمين فهرس صوتي",
+ value=True,
+ key="narration_include_toc"
+ )
+
+ # خيارات إضافية
+ with st.expander("خيارات إضافية", expanded=False):
+ # تجاهل الصفحات
+ narration_skip_pages = st.text_input(
+ "تجاهل الصفحات (أرقام مفصولة بفواصل)",
+ placeholder="مثال: 1,2,5-7",
+ key="narration_skip_pages"
+ )
+
+ # إضافة مقدمة
+ narration_intro = st.text_area(
+ "مقدمة خاصة (سيتم إضافتها في بداية الترجمة الصوتية)",
+ placeholder="مقدمة اختيارية...",
+ key="narration_intro"
+ )
+
+ # إضافة خاتمة
+ narration_outro = st.text_area(
+ "خاتمة خاصة (سيتم إضافتها في نهاية الترجمة الصوتية)",
+ placeholder="خاتمة اختيارية...",
+ key="narration_outro"
+ )
+
+ # زر إنشاء الترجمة الصوتية للمستند
+ if styled_button("إنشاء الترجمة الصوتية للمستند", key="create_document_narration", type="primary", icon="🎙️"):
+ # تحقق من وجود قسم للترجمة الصوتية
+ narration_folder = os.path.join(self.data_dir, "document_narrations", str(selected_document_id))
+ os.makedirs(narration_folder, exist_ok=True)
+
+ # التقدم المستمر في إنشاء الترجمة الصوتية
+ progress_bar = st.progress(0)
+ status_text = st.empty()
+
+ # صنع ترجمة صوتية وهمية (للعرض التوضيحي)
+ total_sections = 5 # عدد أقسام افتراضي
+
+ for i in range(total_sections + 1):
+ # تحديث شريط التقدم
+ progress = i / total_sections
+ progress_bar.progress(progress)
+
+ if i == 0:
+ status_text.text("جاري تحليل المستند...")
+ elif i == 1:
+ status_text.text("جاري استخراج النص...")
+ elif i < total_sections:
+ status_text.text(f"جاري إنشاء الترجمة الصوتية للقسم {i}...")
+ else:
+ status_text.text("جاري تجميع الملفات الصوتية النهائية...")
+
+ time.sleep(1) # محاكاة العمل
+
+ # اكتمال العملية
+ progress_bar.progress(1.0)
+ status_text.text("تم إنشاء الترجمة الصوتية بنجاح!")
+
+ # إظهار نتائج وهمية
+ st.subheader("الترجمة الصوتية للمستند")
+
+ # عرض المقاطع الصوتية (وهمية)
+ for i in range(1, total_sections):
+ with st.expander(f"القسم {i}: العنوان الافتراضي {i}", expanded=i==1):
+ # محاكاة وجود ملف صوتي
+ st.audio("https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3")
+
+ # عرض معلومات القسم
+ st.markdown(f"**المدة:** {i + 2} دقائق")
+ st.markdown(f"**عدد الكلمات:** {i * 100} كلمة")
+
+ # زر تنزيل الملف الكامل
+ st.download_button(
+ label="تنزيل الترجمة الصوتية الكاملة",
+ data=b"Mock audio file", # بيانات وهمية باللغة الإنجليزية
+ file_name=f"{selected_document['name']}_narration.mp3",
+ mime="audio/mpeg",
+ key="download_full_narration"
+ )
+ else:
+ st.warning("لم يتم العثور على المستند المحدد")
+ else:
+ st.info("لا توجد مستندات متاحة حالياً")
+
+ # اختيار رفع مستند جديد
+ st.markdown("### رفع مستند جديد للترجمة الصوتية")
+
+ uploaded_file = st.file_uploader(
+ "اختر ملف للرفع (PDF, DOCX, TXT)",
+ type=["pdf", "docx", "txt"],
+ key="narration_upload_file"
+ )
+
+ if uploaded_file:
+ file_name = uploaded_file.name
+
+ # عرض معلومات الملف
+ st.markdown(f"**اسم الملف:** {file_name}")
+ st.markdown(f"**حجم الملف:** {uploaded_file.size / 1024:.2f} كيلوبايت")
+
+ document_name = st.text_input(
+ "اسم المستند",
+ value=file_name,
+ key="narration_document_name"
+ )
+
+ if styled_button("رفع المستند", key="upload_document", type="primary", icon="📤"):
+ st.success(f"تم رفع المستند '{document_name}' بنجاح!")
+ st.info("يمكنك الآن اختيار المستند لإنشاء ترجمة صوتية له.")
+ st.rerun()
+
+ def _render_voice_over_analytics(self):
+ """عرض إحصائيات ومقاييس الترجمات الصوتية"""
+ st.markdown("""
+
+
📊 إحصائيات ومقاييس
+
إحصائيات ومقاييس استخدام نظام الترجمة الصوتية.
+
+ """, unsafe_allow_html=True)
+
+ # تحديث تاريخ الترجمات الصوتية
+ self.voice_history = self._load_voice_history()
+
+ # التحقق من وجود ترجمات صوتية
+ if not self.voice_history:
+ st.info("لا توجد ترجمات صوتية مخزنة.")
+ return
+
+ # إحصائيات عامة
+ st.markdown("### إحصائيات عامة")
+
+ # تحضير البيانات
+ total_voices = len(self.voice_history)
+ total_duration = sum(item.get("duration", 0) for item in self.voice_history)
+ total_languages = len(set(item.get("source_language", "ar") for item in self.voice_history))
+
+ # عرض الإحصائيات
+ stats_col1, stats_col2, stats_col3 = st.columns(3)
+
+ with stats_col1:
+ st.metric("إجمالي الترجمات الصوتية", total_voices)
+
+ with stats_col2:
+ st.metric("إجمالي المدة", f"{total_duration:.2f} ثانية")
+
+ with stats_col3:
+ st.metric("عدد اللغات المستخدمة", total_languages)
+
+ # رسم بياني لتوزيع الترجمات الصوتية حسب اللغة
+ st.markdown("### توزيع الترجمات الصوتية حسب اللغة")
+
+ language_counts = {}
+ for item in self.voice_history:
+ lang = item.get("source_language", "ar")
+ lang_name = self.supported_languages.get(lang, "غير معروف")
+ language_counts[lang_name] = language_counts.get(lang_name, 0) + 1
+
+ # تحويل إلى DataFrame
+ language_df = pd.DataFrame({
+ "اللغة": list(language_counts.keys()),
+ "العدد": list(language_counts.values())
+ })
+
+ # رسم بياني دائري
+ import plotly.express as px
+
+ fig1 = px.pie(
+ language_df,
+ values="العدد",
+ names="اللغة",
+ title="توزيع الترجمات الصوتية حسب اللغة",
+ color_discrete_sequence=px.colors.qualitative.Pastel
+ )
+
+ fig1.update_layout(
+ title_font_size=20,
+ font_family="Arial",
+ font_size=14,
+ height=400
+ )
+
+ st.plotly_chart(fig1, use_container_width=True)
+
+ # رسم بياني لتوزيع الترجمات الصوتية حسب نوع المحتوى
+ st.markdown("### توزيع الترجمات الصوتية حسب نوع المحتوى")
+
+ content_type_counts = {}
+ for item in self.voice_history:
+ content_type = item.get("content_type", "نص حر")
+ content_type_counts[content_type] = content_type_counts.get(content_type, 0) + 1
+
+ # تحويل إلى DataFrame
+ content_df = pd.DataFrame({
+ "نوع المحتوى": list(content_type_counts.keys()),
+ "العدد": list(content_type_counts.values())
+ })
+
+ # رسم بياني شريطي
+ fig2 = px.bar(
+ content_df,
+ x="نوع المحتوى",
+ y="العدد",
+ title="توزيع الترجمات الصوتية حسب نوع المحتوى",
+ color="نوع المحتوى",
+ color_discrete_sequence=px.colors.qualitative.Pastel
+ )
+
+ fig2.update_layout(
+ title_font_size=20,
+ font_family="Arial",
+ font_size=14,
+ height=400
+ )
+
+ st.plotly_chart(fig2, use_container_width=True)
+
+ # رسم بياني لتوزيع الترجمات الصوتية حسب التاريخ
+ st.markdown("### توزيع الترجمات الصوتية حسب التاريخ")
+
+ # استخراج التواريخ
+ dates = []
+ for item in self.voice_history:
+ created_at = item.get("created_at", "")
+ if created_at:
+ try:
+ date = datetime.datetime.strptime(created_at.split(" ")[0], "%Y-%m-%d").date()
+ dates.append(date)
+ except (ValueError, IndexError):
+ continue
+
+ if dates:
+ # عد التكرارات
+ date_counts = {}
+ for date in dates:
+ date_str = date.strftime("%Y-%m-%d")
+ date_counts[date_str] = date_counts.get(date_str, 0) + 1
+
+ # تحويل إلى DataFrame
+ date_df = pd.DataFrame({
+ "التاريخ": list(date_counts.keys()),
+ "العدد": list(date_counts.values())
+ })
+
+ # ترتيب حسب التاريخ
+ date_df["التاريخ"] = pd.to_datetime(date_df["التاريخ"])
+ date_df = date_df.sort_values("التاريخ")
+
+ # رسم بياني خطي
+ fig3 = px.line(
+ date_df,
+ x="التاريخ",
+ y="العدد",
+ title="توزيع الترجمات الصوتية حسب التاريخ",
+ markers=True
+ )
+
+ fig3.update_layout(
+ title_font_size=20,
+ font_family="Arial",
+ font_size=14,
+ height=400
+ )
+
+ st.plotly_chart(fig3, use_container_width=True)
+ else:
+ st.info("لا توجد بيانات تاريخ كافية لعرض الرسم البياني")
+
+ # تصدير البيانات
+ st.markdown("### تصدير البيانات")
+
+ export_col1, export_col2 = st.columns(2)
+
+ with export_col1:
+ if styled_button("تصدير CSV", key="export_voice_csv", type="primary", icon="📄"):
+ # تحويل البيانات إلى DataFrame
+ export_df = pd.DataFrame(self.voice_history)
+
+ # تنزيل الملف
+ csv_data = export_df.to_csv(index=False)
+
+ st.download_button(
+ label="تنزيل ملف CSV",
+ data=csv_data,
+ file_name=f"voice_over_history_{datetime.datetime.now().strftime('%Y%m%d')}.csv",
+ mime="text/csv",
+ key="download_voice_csv"
+ )
+
+ with export_col2:
+ if styled_button("تصدير JSON", key="export_voice_json", type="primary", icon="📄"):
+ # تحويل البيانات إلى JSON
+ json_data = json.dumps(self.voice_history, indent=2)
+
+ st.download_button(
+ label="تنزيل ملف JSON",
+ data=json_data,
+ file_name=f"voice_over_history_{datetime.datetime.now().strftime('%Y%m%d')}.json",
+ mime="application/json",
+ key="download_voice_json"
+ )
+
+ def _generate_voice_over(self, text, language, voice_id, speaking_rate=1.0, pitch=0.0):
+ """
+ إنشاء ترجمة صوتية (محاكاة)
+
+ المعلمات:
+ text: النص المراد تحويله إلى صوت
+ language: رمز اللغة
+ voice_id: معرف الصوت
+ speaking_rate: سرعة النطق
+ pitch: درجة الصوت
+
+ الإرجاع:
+ مسار الملف الصوتي ومدته
+ """
+ try:
+ # في الوضع العادي، سنستخدم API لتحويل النص إلى صوت
+ # هنا نستخدم ملف صوتي وهمي للعرض التوضيحي
+
+ # إنشاء ملف مؤقت لتمثيل الصوت
+ temp_file = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
+ temp_file.close()
+
+ # نسخ ملف صوتي وهمي (يمكن استبداله بإنشاء فعلي للصوت)
+ audio_file = os.path.join(self.data_dir, f"voice_{language}_{voice_id}_{int(time.time())}.mp3")
+
+ # محاكاة إنشاء ملف صوتي باستخدام صوت بسيط
+ # هنا يمكن استبدال هذا بمكالمة API حقيقية لتحويل النص إلى صوت
+ import wave
+ import struct
+
+ # إنشاء بيانات صوتية بسيطة
+ duration = len(text) * 0.1 # تقدير المدة بناءً على طول النص
+ sample_rate = 44100 # معدل العينات
+
+ # محاكاة تأثير سرعة النطق على المدة
+ duration = duration / speaking_rate
+
+ # إنشاء ملف WAV مؤقت
+ wav_file = temp_file.name.replace(".mp3", ".wav")
+
+ with wave.open(wav_file, "w") as f:
+ f.setnchannels(1) # أحادي القناة
+ f.setsampwidth(2) # 16 بت
+ f.setframerate(sample_rate)
+
+ # محاكاة صوت بسيط (موجة جيبية)
+ for i in range(int(duration * sample_rate)):
+ # تأثير درجة الصوت
+ value = 32767 * 0.3 * np.sin(2 * np.pi * (440 + pitch * 20) * i / sample_rate)
+ f.writeframes(struct.pack('h', int(value)))
+
+ # تحويل WAV إلى MP3 (في التطبيق الفعلي)
+ # هنا نفترض أن التحويل تم بنجاح
+ import shutil
+ shutil.copy(wav_file, audio_file)
+
+ # تنظيف الملفات المؤقتة
+ try:
+ os.remove(wav_file)
+ os.remove(temp_file.name)
+ except:
+ pass
+
+ # تسجيل العملية
+ self.logger.info(f"تم إنشاء ترجمة صوتية للنص (طول: {len(text)}) باللغة: {language}")
+
+ return audio_file, duration
+
+ except Exception as e:
+ self.logger.error(f"خطأ في إنشاء الترجمة الصوتية: {str(e)}")
+ raise e
+
+ def _translate_text(self, text, source_language, target_language):
+ """
+ ترجمة نص من لغة إلى أخرى (محاكاة)
+
+ المعلمات:
+ text: النص المراد ترجمته
+ source_language: رمز اللغة المصدر
+ target_language: رمز اللغة الهدف
+
+ الإرجاع:
+ النص المترجم
+ """
+ try:
+ # في الوضع العادي، سنستخدم API للترجمة
+ # هنا نستخدم ترجمة وهمية للعرض التوضيحي
+
+ # تسجيل العملية
+ self.logger.info(f"ترجمة نص (طول: {len(text)}) من {source_language} إلى {target_language}")
+
+ # ترجمة وهمية
+ translated_prefix = {
+ "en": "This is a sample translation of the text into English.",
+ "ar": "هذه ترجمة عينة للنص إلى اللغة العربية.",
+ "fr": "Ceci est un exemple de traduction du texte en français.",
+ "es": "Esta es una traducción de muestra del texto al español.",
+ "de": "Dies ist eine Beispielübersetzung des Textes ins Deutsche.",
+ "it": "Questa è una traduzione di esempio del testo in italiano.",
+ "zh": "这是文本翻译成中文的示例。",
+ "ja": "これはテキストの日本語への翻訳例です。",
+ "ru": "Это пример перевода текста на русский язык.",
+ "tr": "Bu, metnin Türkçe çevirisinin bir örneğidir."
+ }
+
+ # إرجاع ترجمة وهمية
+ return f"{translated_prefix.get(target_language, 'Translated sample')} {text[:100]}..."
+
+ except Exception as e:
+ self.logger.error(f"خطأ في ترجمة النص: {str(e)}")
+ raise e
+
+ def _get_audio_duration(self, audio_file):
+ """
+ الحصول على مدة ملف صوتي
+
+ المعلمات:
+ audio_file: مسار الملف الصوتي
+
+ الإرجاع:
+ مدة الملف الصوتي بالثواني
+ """
+ try:
+ if audio_file.endswith(".wav"):
+ # استخدام wave للحصول على مدة ملف WAV
+ with wave.open(audio_file, "rb") as f:
+ frames = f.getnframes()
+ rate = f.getframerate()
+ duration = frames / float(rate)
+ else:
+ # محاكاة لمدة الملف الصوتي
+ size_in_bytes = os.path.getsize(audio_file)
+ duration = size_in_bytes / 16000 # تقريب بسيط
+
+ return duration
+
+ except Exception as e:
+ self.logger.error(f"خطأ في الحصول على مدة الملف الصوتي: {str(e)}")
+ return 30.0 # قيمة افتراضية
+
+ def _get_from_cache(self, cache_key):
+ """
+ البحث عن ملف في الكاش
+
+ المعلمات:
+ cache_key: مفتاح الكاش
+
+ الإرجاع:
+ مسار الملف إذا وجد، وإلا None
+ """
+ if cache_key in self.cache_index:
+ cache_file = os.path.join(self.cache_dir, self.cache_index[cache_key])
+ if os.path.exists(cache_file):
+ return cache_file
+
+ return None
+
+ def _add_to_cache(self, cache_key, file_path):
+ """
+ إضافة ملف إلى الكاش
+
+ المعلمات:
+ cache_key: مفتاح الكاش
+ file_path: مسار الملف
+
+ الإرجاع:
+ True إذا تمت الإضافة بنجاح، وإلا False
+ """
+ try:
+ # نسخ الملف إلى الكاش
+ cache_file = os.path.join(self.cache_dir, os.path.basename(file_path))
+
+ if file_path != cache_file:
+ shutil.copy(file_path, cache_file)
+
+ # تحديث فهرس الكاش
+ self.cache_index[cache_key] = os.path.basename(file_path)
+
+ # حفظ الفهرس
+ with open(self.cache_index_file, "w", encoding="utf-8") as f:
+ json.dump(self.cache_index, f, ensure_ascii=False, indent=2)
+
+ return True
+
+ except Exception as e:
+ self.logger.error(f"خطأ في إضافة الملف إلى الكاش: {str(e)}")
+ return False
+
+ def _generate_cache_key(self, text, language, voice_id, speaking_rate, pitch):
+ """
+ إنشاء مفتاح كاش للترجمة الصوتية
+
+ المعلمات:
+ text: النص
+ language: اللغة
+ voice_id: معرف الصوت
+ speaking_rate: سرعة النطق
+ pitch: درجة الصوت
+
+ الإرجاع:
+ مفتاح الكاش
+ """
+ import hashlib
+
+ # إنشاء نص للتجزئة
+ cache_text = f"{text}|{language}|{voice_id}|{speaking_rate}|{pitch}"
+
+ # إنشاء تجزئة MD5
+ hash_obj = hashlib.md5(cache_text.encode())
+
+ return hash_obj.hexdigest()
+
+ def _load_cache_index(self):
+ """
+ تحميل فهرس الكاش
+
+ الإرجاع:
+ قاموس فهرس الكاش
+ """
+ if os.path.exists(self.cache_index_file):
+ try:
+ with open(self.cache_index_file, "r", encoding="utf-8") as f:
+ return json.load(f)
+ except Exception as e:
+ self.logger.error(f"خطأ في تحميل فهرس الكاش: {str(e)}")
+
+ return {}
+
+ def _get_cache_size(self):
+ """
+ الحصول على حجم الكاش بالبايت
+
+ الإرجاع:
+ حجم الكاش بالبايت
+ """
+ total_size = 0
+
+ for filename in os.listdir(self.cache_dir):
+ file_path = os.path.join(self.cache_dir, filename)
+ if os.path.isfile(file_path):
+ total_size += os.path.getsize(file_path)
+
+ return total_size
+
+ def _clear_cache(self):
+ """
+ مسح كاش الترجمات الصوتية
+
+ الإرجاع:
+ True إذا تم المسح بنجاح، وإلا False
+ """
+ try:
+ # حذف جميع الملفات في الكاش
+ for filename in os.listdir(self.cache_dir):
+ file_path = os.path.join(self.cache_dir, filename)
+ if os.path.isfile(file_path):
+ os.remove(file_path)
+
+ # إعادة تعيين فهرس الكاش
+ self.cache_index = {}
+
+ # حفظ الفهرس الفارغ
+ with open(self.cache_index_file, "w", encoding="utf-8") as f:
+ json.dump(self.cache_index, f, ensure_ascii=False, indent=2)
+
+ return True
+
+ except Exception as e:
+ self.logger.error(f"خطأ في مسح الكاش: {str(e)}")
+ return False
+
+ def _add_voice_to_history(self, title, content, source_language, voice_id, duration, audio_file, content_type="نص حر", is_translation=False, original_id=None):
+ """
+ إضافة ترجمة صوتية إلى التاريخ
+
+ المعلمات:
+ title: عنوان الترجمة الصوتية
+ content: محتوى النص
+ source_language: اللغة المصدر
+ voice_id: معرف الصوت
+ duration: مدة الترجمة الصوتية
+ audio_file: اسم ملف الترجمة الصوتية
+ content_type: نوع المحتوى
+ is_translation: هل هي ترجمة لنص آخر
+ original_id: معرف النص الأصلي
+
+ الإرجاع:
+ معرف الترجمة الصوتية
+ """
+ try:
+ # إنشاء معرف فريد
+ voice_id = f"voice_{int(time.time())}_{len(self.voice_history)}"
+
+ # إنشاء كائن الترجمة الصوتية
+ voice_item = {
+ "id": voice_id,
+ "title": title,
+ "content": content,
+ "source_language": source_language,
+ "voice_id": voice_id,
+ "duration": duration,
+ "audio_file": audio_file,
+ "content_type": content_type,
+ "created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "is_translation": is_translation,
+ "original_id": original_id
+ }
+
+ # إضافة إلى التاريخ
+ self.voice_history.append(voice_item)
+
+ # حفظ التاريخ
+ self._save_voice_history()
+
+ return voice_id
+
+ except Exception as e:
+ self.logger.error(f"خطأ في إضافة الترجمة الصوتية إلى التاريخ: {str(e)}")
+ return None
+
+ def _delete_voice_from_history(self, voice_id):
+ """
+ حذف ترجمة صوتية من التاريخ
+
+ المعلمات:
+ voice_id: معرف الترجمة الصوتية
+
+ الإرجاع:
+ True إذا تم الحذف بنجاح، وإلا False
+ """
+ try:
+ # البحث عن الترجمة الصوتية
+ for i, item in enumerate(self.voice_history):
+ if item.get("id") == voice_id:
+ # حذف الملف الصوتي
+ audio_file = os.path.join(self.data_dir, item.get("audio_file", ""))
+ if os.path.exists(audio_file):
+ os.remove(audio_file)
+
+ # حذف العنصر من التاريخ
+ del self.voice_history[i]
+
+ # حفظ التاريخ
+ self._save_voice_history()
+
+ return True
+
+ return False
+
+ except Exception as e:
+ self.logger.error(f"خطأ في حذف الترجمة الصوتية من التاريخ: {str(e)}")
+ return False
+
+ def _load_voice_history(self):
+ """
+ تحميل تاريخ الترجمات الصوتية
+
+ الإرجاع:
+ قائمة تاريخ الترجمات الصوتية
+ """
+ if os.path.exists(self.voice_history_file):
+ try:
+ with open(self.voice_history_file, "r", encoding="utf-8") as f:
+ return json.load(f)
+ except Exception as e:
+ self.logger.error(f"خطأ في تحميل تاريخ الترجمات الصوتية: {str(e)}")
+
+ return []
+
+ def _save_voice_history(self):
+ """
+ حفظ تاريخ الترجمات الصوتية
+
+ الإرجاع:
+ True إذا تم الحفظ بنجاح، وإلا False
+ """
+ try:
+ # التأكد من وجود المجلد
+ os.makedirs(os.path.dirname(self.voice_history_file), exist_ok=True)
+
+ # حفظ التاريخ
+ with open(self.voice_history_file, "w", encoding="utf-8") as f:
+ json.dump(self.voice_history, f, ensure_ascii=False, indent=2)
+
+ return True
+
+ except Exception as e:
+ self.logger.error(f"خطأ في حفظ تاريخ الترجمات الصوتية: {str(e)}")
+ return False
+
+ def _save_voice_settings(self):
+ """
+ حفظ إعدادات الترجمة الصوتية
+
+ الإرجاع:
+ True إذا تم الحفظ بنجاح، وإلا False
+ """
+ try:
+ # التأكد من وجود المجلد
+ os.makedirs(self.data_dir, exist_ok=True)
+
+ # حفظ الإعدادات
+ settings_file = os.path.join(self.data_dir, "voice_settings.json")
+
+ with open(settings_file, "w", encoding="utf-8") as f:
+ json.dump(st.session_state.voice_settings, f, ensure_ascii=False, indent=2)
+
+ return True
+
+ except Exception as e:
+ self.logger.error(f"خطأ في حفظ إعدادات الترجمة الصوتية: {str(e)}")
+ return False
+
+ def _display_audio_player(self, audio_file):
+ """
+ عرض مشغل الصوت
+
+ المعلمات:
+ audio_file: مسار الملف الصوتي
+ """
+ if os.path.exists(audio_file):
+ # قراءة الملف الصوتي
+ with open(audio_file, "rb") as f:
+ audio_bytes = f.read()
+
+ # عرض مشغل الصوت
+ st.audio(audio_bytes, format="audio/mp3")
+ else:
+ st.warning("الملف الصوتي غير متوفر")
+
+ def _get_projects(self):
+ """
+ الحصول على قائمة المشاريع
+
+ الإرجاع:
+ قائمة المشاريع
+ """
+ # في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات
+ # هنا نستخدم بيانات وهمية للعرض التوضيحي
+ return [
+ {
+ "id": "PRJ001",
+ "name": "مشروع تطوير البنية التحتية لمنطقة الرياض",
+ "status": "قيد التنفيذ",
+ "location": "الرياض",
+ "start_date": "2025-01-15",
+ "expected_end_date": "2026-06-30",
+ "budget": "15,000,000 ريال",
+ "description": "مشروع تطوير البنية التحتية في منطقة الرياض، ويشمل إنشاء طرق جديدة وتطوير شبكات الصرف الصحي وتحسين شبكات المياه والكهرباء."
+ },
+ {
+ "id": "PRJ002",
+ "name": "إنشاء مجمع سكني في جدة",
+ "status": "جديد",
+ "location": "جدة",
+ "start_date": "2025-04-01",
+ "expected_end_date": "2027-03-31",
+ "budget": "25,000,000 ريال",
+ "description": "مشروع إنشاء مجمع سكني في مدينة جدة، ويتكون من 50 فيلا و 100 شقة سكنية، بالإضافة إلى مرافق خدمية ومناطق ترفيهية."
+ },
+ {
+ "id": "PRJ003",
+ "name": "توسعة مستشفى الملك فهد",
+ "status": "قيد التنفيذ",
+ "location": "الدمام",
+ "start_date": "2024-10-15",
+ "expected_end_date": "2026-02-28",
+ "budget": "18,500,000 ريال",
+ "description": "مشروع توسعة مستشفى الملك فهد في مدينة الدمام، ويشمل إضافة مبنى جديد للعيادات الخارجية وزيادة عدد أسرّة المستشفى."
+ }
+ ]
+
+ def _get_tenders(self):
+ """
+ الحصول على قائمة المناقصات
+
+ الإرجاع:
+ قائمة المناقصات
+ """
+ # في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات
+ # هنا نستخدم بيانات وهمية للعرض التوضيحي
+ return [
+ {
+ "id": "TND001",
+ "name": "مناقصة تطوير طريق الملك عبدالله",
+ "owner": "وزارة النقل",
+ "issue_date": "2025-02-10",
+ "submission_date": "2025-03-15",
+ "estimated_value": "12,000,000 ريال",
+ "description": "مناقصة لتطوير وتوسعة طريق الملك عبدالله بطول 15 كم، وتشمل الأعمال إنشاء مسارات جديدة وتحسين البنية التحتية للطريق."
+ },
+ {
+ "id": "TND002",
+ "name": "مناقصة إنشاء مدرسة ثانوية",
+ "owner": "وزارة التعليم",
+ "issue_date": "2025-01-20",
+ "submission_date": "2025-02-25",
+ "estimated_value": "8,500,000 ريال",
+ "description": "مناقصة لإنشاء مدرسة ثانوية جديدة في حي النزهة بمدينة الرياض، وتشمل الأعمال إنشاء مبنى المدرسة والمرافق التابعة لها."
+ },
+ {
+ "id": "TND003",
+ "name": "مناقصة صيانة وتأهيل محطات تحلية المياه",
+ "owner": "المؤسسة العامة لتحلية المياه المالحة",
+ "issue_date": "2025-03-01",
+ "submission_date": "2025-04-15",
+ "estimated_value": "22,000,000 ريال",
+ "description": "مناقصة لصيانة وتأهيل محطات تحلية المياه في المنطقة الشرقية، وتشمل الأعمال استبدال المعدات القديمة وتطوير أنظمة التحكم."
+ }
+ ]
+
+ def _get_contracts(self):
+ """
+ الحصول على قائمة العقود
+
+ الإرجاع:
+ قائمة العقود
+ """
+ # في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات
+ # هنا نستخدم بيانات وهمية للعرض التوضيحي
+ return [
+ {
+ "id": "CNT001",
+ "name": "عقد إنشاء مجمع سكني",
+ "client": "شركة الرياض للتطوير العقاري",
+ "start_date": "2025-04-15",
+ "end_date": "2027-04-14",
+ "value": "28,500,000 ريال",
+ "clauses": [
+ {
+ "title": "نطاق الأعمال",
+ "content": "يشمل نطاق الأعمال في هذا العقد إنشاء مجمع سكني مكون من 40 فيلا و 80 شقة سكنية، بالإضافة إلى المرافق الخدمية والترفيهية."
+ },
+ {
+ "title": "مدة التنفيذ",
+ "content": "مدة تنفيذ المشروع 24 شهراً تبدأ من تاريخ استلام الموقع، ويمكن تمديد المدة في حال وجود ظروف قاهرة يتفق عليها الطرفان."
+ },
+ {
+ "title": "قيمة العقد وطريقة الدفع",
+ "content": "قيمة العقد الإجمالية هي 28,500,000 ريال سعودي، ويتم السداد على دفعات شهرية بناءً على نسبة الإنجاز في المشروع."
+ },
+ {
+ "title": "الضمانات",
+ "content": "يلتزم المقاول بتقديم ضمان بنكي بقيمة 5% من قيمة العقد لضمان حسن التنفيذ، وضمان صيانة لمدة سنة بعد الانتهاء من المشروع."
+ },
+ {
+ "title": "الغرامات والجزاءات",
+ "content": "في حال تأخر المقاول عن تسليم المشروع في الموعد المحدد، يتم فرض غرامة تأخير بنسبة 0.1% من قيمة العقد عن كل يوم تأخير، بحد أقصى 10% من قيمة العقد."
+ }
+ ]
+ },
+ {
+ "id": "CNT002",
+ "name": "عقد توريد وتركيب أنظمة تكييف",
+ "client": "شركة التطوير العقاري المحدودة",
+ "start_date": "2025-03-01",
+ "end_date": "2025-08-31",
+ "value": "4,200,000 ريال",
+ "clauses": [
+ {
+ "title": "نطاق التوريد",
+ "content": "يشمل نطاق التوريد في هذا العقد توفير وتركيب 120 وحدة تكييف مركزي للمبنى الإداري الجديد، بالإضافة إلى خدمات الصيانة لمدة عام."
+ },
+ {
+ "title": "مواصفات الأجهزة",
+ "content": "يجب أن تكون جميع الأجهزة الموردة من إحدى العلامات التجارية المعتمدة (كارير، دايكن، أو ميتسوبيشي)، وأن تكون مطابقة للمواصفات الفنية المرفقة بالعقد."
+ },
+ {
+ "title": "مدة التوريد والتركيب",
+ "content": "يلتزم المورد بتوريد وتركيب جميع الأجهزة خلال مدة لا تتجاوز 6 أشهر من تاريخ توقيع العقد."
+ },
+ {
+ "title": "الضمان",
+ "content": "يقدم المورد ضماناً لجميع الأجهزة لمدة 3 سنوات من تاريخ التشغيل، ويشمل الضمان جميع أعمال الصيانة وقطع الغيار."
+ }
+ ]
+ }
+ ]
+
+ def _get_documents(self):
+ """
+ الحصول على قائمة المستندات
+
+ الإرجاع:
+ قائمة المستندات
+ """
+ # في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات
+ # هنا نستخدم بيانات وهمية للعرض التوضيحي
+ return [
+ {
+ "id": "DOC001",
+ "name": "كراسة شروط مناقصة تطوير طريق الملك عبدالله",
+ "type": "كراسة شروط",
+ "page_count": 85,
+ "file_size": "2.4 ميجابايت",
+ "upload_date": "2025-02-10"
+ },
+ {
+ "id": "DOC002",
+ "name": "عقد إنشاء مجمع سكني",
+ "type": "عقد",
+ "page_count": 42,
+ "file_size": "1.8 ميجابايت",
+ "upload_date": "2025-04-12"
+ },
+ {
+ "id": "DOC003",
+ "name": "تقرير دراسة جدوى مشروع توسعة مستشفى",
+ "type": "تقرير",
+ "page_count": 65,
+ "file_size": "3.1 ميجابايت",
+ "upload_date": "2025-01-25"
+ }
+ ]
+
+
+# تطبيق وحدة الترجمة الصوتية متعددة اللغات
+class VoiceNarrationApp:
+ """وحدة تطبيق الترجمة الصوتية متعددة اللغات"""
+
+ def __init__(self):
+ """تهيئة وحدة تطبيق الترجمة الصوتية متعددة اللغات"""
+ self.voice_over_system = VoiceOverSystem()
+
+ def render(self):
+ """عرض واجهة وحدة تطبيق الترجمة الصوتية متعددة اللغات"""
+ st.markdown("
نظام الترجمة الصوتية متعددة اللغات
", unsafe_allow_html=True)
+
+ st.markdown("""
+
+ يتيح لك نظام الترجمة الصوتية متعددة اللغات تحويل النصوص والمستندات إلى ملفات صوتية بلغات متعددة،
+ مما يساعد في توصيل المعلومات بشكل أفضل للأشخاص من خلفيات لغوية مختلفة.
+
+ نظام WAHBi AI هو نظام متكامل لتحليل العقود والمناقصات باستخدام تقنيات الذكاء الاصطناعي المتقدمة.
+ تم تطوير النظام خصيصًا لشركة شبه الجزيرة للمقاولات لتمكينها من تحليل وثائق المناقصات، وتسعير المشاريع، وتقييم المخاطر،
+ وإدارة الموارد بكفاءة عالية.
+
+
+
المميزات الرئيسية
+
+
تحليل المستندات ذكيًا - استخراج البنود والكميات والمخاطر من المستندات تلقائيًا
+
حاسبة تكاليف البناء المتكاملة - حساب التكاليف التفصيلية للمواد الخام والمعدات والعمالة والمصاريف الإدارية وهوامش الربح
+
تحليل الأسعار غير المتزنة - تحليل ومقارنة الأسعار مع متوسطات السوق لتحقيق أقصى استفادة
+
تتبع المعدات والموارد - متابعة وتنظيم موارد الشركة وتخصيصها للمشاريع المختلفة
+
تحليل المخاطر - تحديد المخاطر المحتملة في المشاريع وتقييم تأثيرها واقتراح إجراءات التخفيف
+
مؤشرات أداء المشاريع - متابعة الأداء المالي والفني للمشاريع وقياس الإنجاز
+
حاسبة المحتوى المحلي - حساب وتحسين نسبة المحتوى المحلي في المشاريع لتلبية متطلبات المملكة
+
التنبؤ بأسعار المواد - استخدام نماذج التعلم الآلي للتنبؤ بتغيرات الأسعار المستقبلية
+
مؤقت مواعيد التسليم - متابعة مواعيد بدء الدراسة ومواعيد تسليم العروض للمشاريع
+
دعم اللغة العربية - واجهة مستخدم باللغة العربية مع إمكانية التبديل للغة الإنجليزية
+
توافق متعدد الأجهزة - يعمل على جميع الأجهزة (أيفون، أندرويد، أجهزة لوحية، حواسيب مكتبية)
+
+
+
معلومات الاتصال
+
+
العنوان: المملكة العربية السعودية - الرياض - حي الملز - شارع السبالة
+
البريد الإلكتروني: info@peninsula-contracting.com
+
هاتف: +966 12 345 6789
+
الموقع الإلكتروني: www.peninsula-contracting.com
+
+
+
فريق التطوير
+
+ تم تطوير هذا النظام بواسطة فريق متخصص من المهندسين والمطورين بقيادة م. بدر وهبي،
+ بالتعاون مع فريق التسعير والمشاريع في شركة شبه الجزيرة للمقاولات.
+
", unsafe_allow_html=True)
+
+ # بيانات المواعيد
+ deadlines = [
+ {
+ "name": "مناقصة توسعة مستشفى الملك فهد",
+ "submission_date": "15 أبريل 2025",
+ "days_left": 15,
+ "start_date": "1 مارس 2025"
+ },
+ {
+ "name": "مناقصة إنشاء مبنى كلية الطب",
+ "submission_date": "30 مارس 2025",
+ "days_left": 0,
+ "start_date": "15 فبراير 2025"
+ },
+ {
+ "name": "مناقصة طريق الدائري الشمالي",
+ "submission_date": "10 مايو 2025",
+ "days_left": 40,
+ "start_date": "5 مارس 2025"
+ }
+ ]
+
+ for i, deadline in enumerate(deadlines):
+ # تحديد لون المؤقت بناءً على عدد الأيام المتبقية
+ color_class = "danger" if deadline["days_left"] <= 5 else "warning" if deadline["days_left"] <= 15 else "success"
+
+ # عرض معلومات الموعد والمؤقت
+ st.markdown(f"""
+
+
{deadline["name"]}
+
+
+ بدء الدراسة: {deadline["start_date"]}
+
+
+ تاريخ التسليم: {deadline["submission_date"]}
+
+
+
+
+
+
+
+ متبقي: {deadline["days_left"]} أيام
+
+
+
+
+
+
+ """, unsafe_allow_html=True)
+
+ # عرض مؤقت تفصيلي للمناقصة الأولى
+ st.markdown("
العد التنازلي للتسليم
", unsafe_allow_html=True)
+
+ st.markdown("""
+
+
+
15
+
يوم
+
+
+
08
+
ساعة
+
+
+
45
+
دقيقة
+
+
+
20
+
ثانية
+
+
+ """, unsafe_allow_html=True)
\ No newline at end of file
diff --git a/utils/components/credits.py b/utils/components/credits.py
new file mode 100644
index 0000000000000000000000000000000000000000..64ce1c3673de201c75c09c19319e81e84d7a7055
--- /dev/null
+++ b/utils/components/credits.py
@@ -0,0 +1,115 @@
+"""
+مكون عرض معلومات فريق التطوير
+"""
+
+import streamlit as st
+
+# تصدير واضح للدالة لجعلها متاحة للاستيراد
+__all__ = ['render_credits', 'display_credits']
+
+
+def render_credits():
+ """
+ عرض معلومات فريق التطوير (واجهة للاستخدام مع وحدات النظام المختلفة)
+ """
+ display_credits()
+
+
+def display_credits():
+ """
+ عرض معلومات فريق التطوير
+ """
+ # تعريف بيانات الفريق
+ team_members = [
+ {
+ "name": "م بدر وهبي",
+ "role": "مدير المشروع",
+ "image": "badr.jpg",
+ "bio": "مهندس برمجيات ذو خبرة 15 عامًا في تطوير أنظمة المقاولات والتسعير"
+ },
+ {
+ "name": "م تامر الجوهري",
+ "role": "مهندسة الذكاء الاصطناعي",
+ "image": "tamer.jpg",
+ "bio": "متخصصة في معالجة اللغة العربية الطبيعية وتحليل البيانات"
+ },
+ {
+ "name": "م اسلام عيسي",
+ "role": "أخصائي تطوير التسعير",
+ "image": "Islam.jpg",
+ "bio": "خبير في أنظمة التسعير المتقدمة والتحليل المالي للمشاريع"
+ }
+ ]
+
+ # عرض معلومات الفريق في صفوف
+ # كل صف يحتوي على 3 أعضاء
+ cols_per_row = 3
+
+ for i in range(0, len(team_members), cols_per_row):
+ # إنشاء الأعمدة
+ cols = st.columns(cols_per_row)
+
+ # عرض الأعضاء في هذا الصف
+ for j in range(cols_per_row):
+ idx = i + j
+ if idx < len(team_members):
+ member = team_members[idx]
+
+ with cols[j]:
+ # عرض صورة العضو (استخدام صورة افتراضية إذا لم تكن متوفرة)
+ try:
+ st.image(f"static/images/team/{member['image']}", width=150)
+ except:
+ # استخدام الأحرف الأولى من الاسم كصورة افتراضية
+ initials = ''.join([name[0] for name in member['name'].split() if name.startswith('م.') == False])
+ st.markdown(f"""
+
+ {initials}
+
+ """, unsafe_allow_html=True)
+
+ # عرض معلومات العضو
+ st.markdown(f"""
+
+
{member['name']}
+
{member['role']}
+
{member['bio']}
+
+ """, unsafe_allow_html=True)
+
+ # إضافة أسلوب CSS للعرض
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
\ No newline at end of file
diff --git a/utils/components/header.py b/utils/components/header.py
new file mode 100644
index 0000000000000000000000000000000000000000..57ff09d01258bbe590009a922232525962ddd647
--- /dev/null
+++ b/utils/components/header.py
@@ -0,0 +1,135 @@
+"""
+مكون الهيدر لنظام واهبي لتحليل العقود والمناقصات
+Header component for WAHBI Tender Analysis System
+"""
+
+import streamlit as st
+import os
+import base64
+
+def render_header(subtitle=None):
+ """
+ عرض شريط العنوان في التطبيق مع الشعار
+ """
+ # الحصول على مسار ملف الشعار
+ logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "logo.svg")
+
+ # إذا كان ملف الشعار موجود، قم بعرضه
+ if os.path.exists(logo_path):
+ with open(logo_path, "r", encoding="utf-8") as f:
+ svg_content = f.read()
+
+ # استخدام HTML مباشرة لعرض الشعار SVG بشكل صحيح
+ st.markdown(f"""
+
+
+ {svg_content}
+
+
+
نظام واهبي للذكاء الاصطناعي
+
{subtitle if subtitle else "لتحليل العقود والمناقصات"}
+
+
+ """, unsafe_allow_html=True)
+ else:
+ # إذا لم يكن ملف الشعار موجود، عرض نسخة نصية بدون الشعار
+ st.markdown(f"""
+
+
نظام واهبي للذكاء الاصطناعي
+
{subtitle if subtitle else "لتحليل العقود والمناقصات"}
+
+ """, unsafe_allow_html=True)
+ print(f"تحذير: لم يتم العثور على ملف الشعار في المسار: {logo_path}")
+
+def render_app_header():
+ """
+ عرض هيدر التطبيق الرئيسي مع الشعار والعنوان
+ """
+ render_header("النظام المتكامل لتحليل العقود والمناقصات باستخدام الذكاء الاصطناعي")
+
+def render_section_header(title, description=None, icon=None):
+ """
+ عرض عنوان قسم مع وصف اختياري وأيقونة
+ """
+ icon_html = f'' if icon else ''
+
+ st.markdown(f"""
+
+
+ {icon_html}{title}
+
+ {f'
{description}
' if description else ''}
+
+ """, unsafe_allow_html=True)
+
+def render_page_title(title, description=None, icon=None):
+ """
+ عرض عنوان صفحة مع وصف اختياري وأيقونة
+ """
+ st.markdown(f"""
+
+ {f'' if icon else ''}{title}
+
+ {f'
{description}
' if description else ''}
+ """, unsafe_allow_html=True)
+
+def render_breadcrumbs(items):
+ """
+ عرض مسار التنقل في الصفحة
+
+ المعلمات:
+ items (list): قائمة بالعناصر، كل عنصر هو قاموس يحتوي على مفتاحين: "label" و"url" (اختياري)
+ """
+ breadcrumbs_html = ""
+ for i, item in enumerate(items):
+ if i > 0:
+ breadcrumbs_html += f''
+
+ if "url" in item and item["url"]:
+ breadcrumbs_html += f'{item["label"]}'
+ else:
+ breadcrumbs_html += f'{item["label"]}'
+
+ st.markdown(f"""
+
+ """, unsafe_allow_html=True)
\ No newline at end of file
diff --git a/utils/components/sidebar.py b/utils/components/sidebar.py
new file mode 100644
index 0000000000000000000000000000000000000000..409e9ffc177ae6153f6b72a0c408ea910e2bcdc1
--- /dev/null
+++ b/utils/components/sidebar.py
@@ -0,0 +1,176 @@
+"""
+مكون الشريط الجانبي لنظام واهبي لتحليل العقود والمناقصات
+Sidebar component for WAHBI Tender Analysis System
+"""
+
+import streamlit as st
+import os
+from pathlib import Path
+import streamlit_option_menu as option_menu
+import json
+
+def get_user_info():
+ """
+ استرجاع معلومات المستخدم الحالي (يُستخدم كمثال بسيط)
+ """
+ # في بيئة الإنتاج، هذه المعلومات يجب أن تأتي من نظام المصادقة
+ return {
+ "name": "محمد أحمد",
+ "role": "محلل عقود",
+ "image": None
+ }
+
+def render_sidebar():
+ """
+ عرض الشريط الجانبي الرئيسي للتطبيق
+ """
+ with st.sidebar:
+ # عرض معلومات المستخدم
+ user = get_user_info()
+
+ # مربع معلومات المستخدم
+ st.markdown(f"""
+
+ """, unsafe_allow_html=True)
+
+def get_sidebar_selection():
+ """
+ الحصول على العنصر المحدد في القائمة الجانبية
+ """
+ return st.session_state.get("sidebar_selected", "الرئيسية")
+
+def render_module_sidebar(module_name, options=[]):
+ """
+ عرض شريط جانبي مخصص للوحدة
+
+ المعلمات:
+ module_name (str): اسم الوحدة
+ options (list): قائمة بالخيارات المتاحة في الوحدة
+ """
+ with st.sidebar:
+ # عنوان الوحدة
+ st.markdown(f"""
+
+ {module_name}
+
+ """, unsafe_allow_html=True)
+
+ # إذا تم توفير خيارات للوحدة
+ if options:
+ selected = option_menu.option_menu(
+ menu_title=None,
+ options=options,
+ menu_icon=None,
+ default_index=0,
+ styles={
+ "container": {"padding": "0!important", "background-color": "transparent", "direction": "rtl"},
+ "icon": {"color": "#1E88E5", "font-size": "1rem", "float": "right", "margin-left": "10px"},
+ "nav-link": {
+ "font-size": "0.9rem",
+ "text-align": "right",
+ "direction": "rtl",
+ "--hover-color": "#E3F2FD",
+ "margin-bottom": "0.2rem",
+ "padding-right": "15px",
+ "padding": "0.5rem"
+ },
+ "nav-link-selected": {"background-color": "#1E88E5", "color": "white", "text-align": "right"},
+ }
+ )
+
+ # تخزين الخيار المحدد
+ st.session_state[f"{module_name}_selected"] = selected
+
+ return selected
+
+ # زر للعودة إلى القائمة الرئيسية
+ if st.button("العودة للقائمة الرئيسية", key=f"back_btn_{module_name}"):
+ st.session_state["sidebar_selected"] = "الرئيسية"
+ st.rerun()
+
+def get_module_selection(module_name):
+ """
+ الحصول على العنصر المحدد في قائمة الوحدة
+
+ المعلمات:
+ module_name (str): اسم الوحدة
+ """
+ return st.session_state.get(f"{module_name}_selected", None)
\ No newline at end of file
diff --git a/utils/components/system_innovation.py b/utils/components/system_innovation.py
new file mode 100644
index 0000000000000000000000000000000000000000..072bcdce4ede7d07f11ba97edbad49a662d5765b
--- /dev/null
+++ b/utils/components/system_innovation.py
@@ -0,0 +1,81 @@
+"""
+مكون عرض ابتكارات النظام
+"""
+
+import streamlit as st
+
+
+def display_innovations():
+ """
+ عرض ابتكارات النظام
+ """
+ # تعريف الابتكارات الرئيسية
+ innovations = [
+ {
+ "title": "تحليل المستندات بالذكاء الاصطناعي",
+ "description": "استخراج البنود والكميات والمخاطر من المستندات تلقائيًا باستخدام تقنيات الذكاء الاصطناعي ومعالجة اللغة الطبيعية، مما يوفر الوقت والجهد ويقلل من الأخطاء البشرية.",
+ "icon": "📄"
+ },
+ {
+ "title": "تقنية التسعير غير المتزن",
+ "description": "آلية متطورة لتحليل وتطبيق استراتيجيات التسعير غير المتزن، مع ضمان الحفاظ على القيمة الإجمالية للعرض، وزيادة فرص الربحية وتحسين التدفق النقدي.",
+ "icon": "💰"
+ },
+ {
+ "title": "حاسبة المحتوى المحلي الذكية",
+ "description": "حساب وتحسين نسبة المحتوى المحلي في المشاريع بطريقة آلية، مع اقتراح بدائل محلية للمنتجات والخدمات المستوردة لتحقيق متطلبات المحتوى المحلي.",
+ "icon": "🏭"
+ },
+ {
+ "title": "نظام التنبؤ بالأسعار",
+ "description": "التنبؤ بأسعار المواد والخدمات باستخدام خوارزميات التعلم الآلي والبيانات التاريخية، مما يساعد في اتخاذ قرارات التسعير بدقة أكبر.",
+ "icon": "📊"
+ },
+ {
+ "title": "تحليل المخاطر الاستباقي",
+ "description": "تحديد وتحليل المخاطر المحتملة في المشاريع بشكل استباقي، مع توفير استراتيجيات المعالجة المناسبة لكل مخاطرة وتقدير تأثيرها على التكلفة.",
+ "icon": "⚠️"
+ }
+ ]
+
+ # عرض الابتكارات في صفوف
+ col1, col2 = st.columns(2)
+
+ for i, innovation in enumerate(innovations):
+ # توزيع الابتكارات على عمودين
+ current_col = col1 if i % 2 == 0 else col2
+
+ with current_col:
+ st.markdown(f"""
+
', unsafe_allow_html=True)
+ return clicked
+
+def filter_dataframe(df, column, value):
+ """ترشيح إطار البيانات"""
+ if value == "الكل":
+ return df
+ return df[df[column] == value]
+
+def get_file_extension(filename):
+ """استخراج امتداد الملف"""
+ if not filename:
+ return ""
+ return os.path.splitext(filename)[-1].lower()
+
+def extract_numbers_from_text(text):
+ """استخراج الأرقام من النص
+
+ Args:
+ text (str): النص المراد استخراج الأرقام منه
+
+ Returns:
+ list: قائمة بالأرقام المستخرجة
+ """
+ if not text:
+ return []
+
+ # نمط للعثور على الأرقام (صحيحة أو عشرية) في النص
+ pattern = r'[-+]?\d*\.\d+|\d+'
+
+ # استخراج جميع الأرقام من النص
+ numbers = re.findall(pattern, text)
+
+ # تحويل النصوص المستخرجة إلى أرقام (صحيحة أو عشرية)
+ converted_numbers = []
+ for num in numbers:
+ if '.' in num:
+ converted_numbers.append(float(num))
+ else:
+ converted_numbers.append(int(num))
+
+ return converted_numbers
\ No newline at end of file
diff --git a/utils/pdf_handler.py b/utils/pdf_handler.py
new file mode 100644
index 0000000000000000000000000000000000000000..d2e141276d13808469c4bc601c48138445788824
--- /dev/null
+++ b/utils/pdf_handler.py
@@ -0,0 +1,847 @@
+"""
+معالج ملفات PDF
+"""
+
+import os
+import io
+import re
+import PyPDF2
+import fitz # PyMuPDF
+import pdfplumber
+import numpy as np
+from PIL import Image
+import pytesseract
+import pandas as pd
+import traceback
+from reportlab.lib.pagesizes import A4, landscape
+from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image as ReportLabImage
+from reportlab.lib import colors
+from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
+from reportlab.pdfbase import pdfmetrics
+from reportlab.pdfbase.ttfonts import TTFont
+import matplotlib.pyplot as plt
+
+from utils.helpers import create_directory_if_not_exists, extract_numbers_from_text
+
+# محاولة تسجيل الخطوط العربية إذا كانت متوفرة
+try:
+ font_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "Amiri-Regular.ttf")
+ bold_font_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "Amiri-Bold.ttf")
+
+ if os.path.exists(font_path) and os.path.exists(bold_font_path):
+ pdfmetrics.registerFont(TTFont('Arabic', font_path))
+ pdfmetrics.registerFont(TTFont('ArabicBold', bold_font_path))
+except Exception as e:
+ print(f"تعذر تحميل الخطوط العربية: {str(e)}. سيتم استخدام الخطوط الافتراضية.")
+
+
+def extract_text_from_pdf(file_path, method='pymupdf'):
+ """
+ استخراج النص من ملف PDF
+
+ المعلمات:
+ file_path: مسار ملف PDF
+ method: طريقة الاستخراج ('pymupdf', 'pypdf2', 'pdfplumber')
+
+ الإرجاع:
+ نص مستخرج من ملف PDF
+ """
+ try:
+ # التحقق من وجود الملف
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"الملف غير موجود: {file_path}")
+
+ # استخراج النص حسب الطريقة المطلوبة
+ if method.lower() == 'pymupdf':
+ return _extract_text_with_pymupdf(file_path)
+ elif method.lower() == 'pypdf2':
+ return _extract_text_with_pypdf2(file_path)
+ elif method.lower() == 'pdfplumber':
+ return _extract_text_with_pdfplumber(file_path)
+ else:
+ # استخدام PyMuPDF كطريقة افتراضية
+ return _extract_text_with_pymupdf(file_path)
+
+ except Exception as e:
+ error_msg = f"خطأ في استخراج النص من ملف PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def _extract_text_with_pymupdf(file_path):
+ """استخراج النص باستخدام PyMuPDF"""
+ document = fitz.open(file_path)
+ text = ""
+
+ for page_number in range(len(document)):
+ page = document.load_page(page_number)
+ text += page.get_text("text") + "\n\n"
+
+ document.close()
+ return text
+
+
+def _extract_text_with_pypdf2(file_path):
+ """استخراج النص باستخدام PyPDF2"""
+ with open(file_path, 'rb') as file:
+ reader = PyPDF2.PdfReader(file)
+ text = ""
+
+ for page_number in range(len(reader.pages)):
+ page = reader.pages[page_number]
+ text += page.extract_text() + "\n\n"
+
+ return text
+
+
+def _extract_text_with_pdfplumber(file_path):
+ """استخراج النص باستخدام pdfplumber"""
+ with pdfplumber.open(file_path) as pdf:
+ text = ""
+
+ for page in pdf.pages:
+ text += page.extract_text() + "\n\n"
+
+ return text
+
+
+def extract_tables_from_pdf(file_path, page_numbers=None):
+ """
+ استخراج الجداول من ملف PDF
+
+ المعلمات:
+ file_path: مسار ملف PDF
+ page_numbers: قائمة بأرقام الصفحات للاستخراج منها (افتراضي: None لجميع الصفحات)
+
+ الإرجاع:
+ قائمة من DataFrames تمثل الجداول المستخرجة
+ """
+ try:
+ # التحقق من وجود الملف
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"الملف غير موجود: {file_path}")
+
+ # استخراج الجداول باستخدام pdfplumber
+ tables = []
+
+ with pdfplumber.open(file_path) as pdf:
+ # تحديد الصفحات المراد استخراج الجداول منها
+ if page_numbers is None:
+ pages_to_extract = range(len(pdf.pages))
+ else:
+ pages_to_extract = [p-1 for p in page_numbers if 1 <= p <= len(pdf.pages)]
+
+ # استخراج الجداول من كل صفحة
+ for page_idx in pages_to_extract:
+ page = pdf.pages[page_idx]
+ page_tables = page.extract_tables()
+
+ if page_tables:
+ for table in page_tables:
+ if table: # التحقق من أن الجدول ليس فارغًا
+ # تحويل الجدول إلى DataFrame
+ df = pd.DataFrame(table[1:], columns=table[0])
+
+ # تنظيف البيانات
+ df = df.applymap(lambda x: x.strip() if isinstance(x, str) else x)
+
+ # إضافة إلى قائمة الجداول
+ tables.append(df)
+
+ return tables
+
+ except Exception as e:
+ error_msg = f"خطأ في استخراج الجداول من ملف PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def extract_images_from_pdf(file_path, output_dir=None, prefix='image'):
+ """
+ استخراج الصور من ملف PDF
+
+ المعلمات:
+ file_path: مسار ملف PDF
+ output_dir: دليل الإخراج (افتراضي: None للإرجاع كقائمة من الصور)
+ prefix: بادئة أسماء ملفات الصور
+
+ الإرجاع:
+ قائمة من مسارات الصور المستخرجة إذا تم تحديد دليل الإخراج، وإلا قائمة من الصور
+ """
+ try:
+ # التحقق من وجود الملف
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"الملف غير موجود: {file_path}")
+
+ # إنشاء دليل الإخراج إذا تم تحديده
+ if output_dir:
+ create_directory_if_not_exists(output_dir)
+
+ # استخراج الصور باستخدام PyMuPDF
+ document = fitz.open(file_path)
+ images = []
+ image_paths = []
+
+ for page_idx in range(len(document)):
+ page = document.load_page(page_idx)
+
+ # استخراج الصور من الصفحة
+ image_list = page.get_images(full=True)
+
+ for img_idx, img_info in enumerate(image_list):
+ xref = img_info[0]
+ base_image = document.extract_image(xref)
+ image_bytes = base_image["image"]
+
+ # إنشاء كائن الصورة
+ image = Image.open(io.BytesIO(image_bytes))
+
+ if output_dir:
+ # حفظ الصورة في الدليل المحدد
+ image_filename = f"{prefix}_{page_idx+1}_{img_idx+1}.{base_image['ext']}"
+ image_path = os.path.join(output_dir, image_filename)
+ image.save(image_path)
+ image_paths.append(image_path)
+ else:
+ # إضافة الصورة إلى القائمة
+ images.append(image)
+
+ document.close()
+
+ return image_paths if output_dir else images
+
+ except Exception as e:
+ error_msg = f"خطأ في استخراج الصور من ملف PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def extract_text_from_image(image, lang='ara+eng'):
+ """
+ استخراج النص من صورة باستخدام OCR
+
+ المعلمات:
+ image: كائن الصورة أو مسار الصورة
+ lang: لغة النص (افتراضي: 'ara+eng' للعربية والإنجليزية)
+
+ الإرجاع:
+ النص المستخرج من الصورة
+ """
+ try:
+ # إذا كان مسار صورة، قم بفتحها
+ if isinstance(image, str):
+ image = Image.open(image)
+
+ # استخراج النص باستخدام pytesseract
+ text = pytesseract.image_to_string(image, lang=lang)
+
+ return text
+
+ except Exception as e:
+ error_msg = f"خطأ في استخراج النص من الصورة: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ return ""
+
+
+def ocr_pdf(file_path, lang='ara+eng'):
+ """
+ تنفيذ OCR على ملف PDF
+
+ المعلمات:
+ file_path: مسار ملف PDF
+ lang: لغة النص (افتراضي: 'ara+eng' للعربية والإنجليزية)
+
+ الإرجاع:
+ النص المستخرج من ملف PDF
+ """
+ try:
+ # التحقق من وجود الملف
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"الملف غير موجود: {file_path}")
+
+ # فتح ملف PDF
+ document = fitz.open(file_path)
+ text = ""
+
+ for page_idx in range(len(document)):
+ page = document.load_page(page_idx)
+
+ # تحويل الصفحة إلى صورة
+ pix = page.get_pixmap(matrix=fitz.Matrix(300/72, 300/72))
+ img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
+
+ # استخراج النص من الصورة
+ page_text = extract_text_from_image(img, lang=lang)
+ text += page_text + "\n\n"
+
+ document.close()
+
+ return text
+
+ except Exception as e:
+ error_msg = f"خطأ في تنفيذ OCR على ملف PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def search_in_pdf(file_path, search_text):
+ """
+ البحث عن نص في ملف PDF
+
+ المعلمات:
+ file_path: مسار ملف PDF
+ search_text: النص المراد البحث عنه
+
+ الإرجاع:
+ قائمة من النتائج {page_number, text_snippet, matched_text}
+ """
+ try:
+ # التحقق من وجود الملف
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"الملف غير موجود: {file_path}")
+
+ # استخراج النص من ملف PDF
+ document = fitz.open(file_path)
+ results = []
+
+ for page_idx in range(len(document)):
+ page = document.load_page(page_idx)
+ page_text = page.get_text("text")
+
+ # البحث عن النص
+ if search_text.lower() in page_text.lower():
+ # استخراج المقتطفات التي تحتوي على النص المطلوب
+ lines = page_text.split('\n')
+ for line in lines:
+ if search_text.lower() in line.lower():
+ results.append({
+ 'page_number': page_idx + 1,
+ 'text_snippet': line,
+ 'matched_text': search_text
+ })
+
+ document.close()
+
+ return results
+
+ except Exception as e:
+ error_msg = f"خطأ في البحث في ملف PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def extract_quantities_from_pdf(file_path):
+ """
+ استخراج الكميات من ملف PDF
+
+ المعلمات:
+ file_path: مسار ملف PDF
+
+ الإرجاع:
+ DataFrame يحتوي على البنود والكميات المستخرجة
+ """
+ try:
+ # استخراج النص والجداول
+ text = extract_text_from_pdf(file_path)
+ tables = extract_tables_from_pdf(file_path)
+
+ quantities = []
+
+ # استخراج الكميات من الجداول
+ for table in tables:
+ # البحث عن أعمدة تحتوي على "الكمية" أو "الوحدة" أو "البند"
+ quantity_cols = [col for col in table.columns if any(term in col.lower() for term in ['كمية', 'عدد', 'الكمية'])]
+ unit_cols = [col for col in table.columns if any(term in col.lower() for term in ['وحدة', 'الوحدة'])]
+ item_cols = [col for col in table.columns if any(term in col.lower() for term in ['بند', 'وصف', 'البند', 'العمل'])]
+
+ if quantity_cols and (unit_cols or item_cols):
+ quantity_col = quantity_cols[0]
+ unit_col = unit_cols[0] if unit_cols else None
+ item_col = item_cols[0] if item_cols else None
+
+ # استخراج الكميات
+ for _, row in table.iterrows():
+ if pd.notna(row[quantity_col]) and (item_col is None or pd.notna(row[item_col])):
+ quantity_value = extract_numbers_from_text(row[quantity_col])
+ quantity = quantity_value[0] if quantity_value else None
+
+ quantities.append({
+ 'البند': row[item_col] if item_col else "غير محدد",
+ 'الوحدة': row[unit_col] if unit_col else "غير محدد",
+ 'الكمية': quantity
+ })
+
+ # استخراج الكميات من النص
+ lines = text.split('\n')
+ for line in lines:
+ # البحث عن الخطوط التي تحتوي على أرقام ووحدات قياس
+ if re.search(r'\d+(?:,\d+)*(?:\.\d+)?', line) and any(unit in line for unit in ['م2', 'م3', 'متر', 'طن', 'كجم', 'عدد']):
+ numbers = extract_numbers_from_text(line)
+ if numbers:
+ # استخراج وحدة القياس
+ unit_match = re.search(r'\b(م2|م3|متر مربع|متر مكعب|م\.ط|طن|كجم|عدد|قطعة)\b', line)
+ unit = unit_match.group(1) if unit_match else "غير محدد"
+
+ quantities.append({
+ 'البند': line,
+ 'الوحدة': unit,
+ 'الكمية': numbers[0]
+ })
+
+ # إنشاء DataFrame
+ if quantities:
+ quantities_df = pd.DataFrame(quantities)
+ return quantities_df
+ else:
+ return pd.DataFrame(columns=['البند', 'الوحدة', 'الكمية'])
+
+ except Exception as e:
+ error_msg = f"خطأ في استخراج الكميات من ملف PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def merge_pdfs(input_paths, output_path):
+ """
+ دمج ملفات PDF متعددة في ملف واحد
+
+ المعلمات:
+ input_paths: قائمة من مسارات ملفات PDF المراد دمجها
+ output_path: مسار ملف PDF الناتج
+
+ الإرجاع:
+ True في حالة النجاح
+ """
+ try:
+ # التحقق من وجود الملفات
+ for file_path in input_paths:
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"الملف غير موجود: {file_path}")
+
+ # إنشاء مجلد الإخراج إذا لم يكن موجودًا
+ output_dir = os.path.dirname(output_path)
+ create_directory_if_not_exists(output_dir)
+
+ # دمج ملفات PDF
+ merger = PyPDF2.PdfMerger()
+
+ for file_path in input_paths:
+ merger.append(file_path)
+
+ merger.write(output_path)
+ merger.close()
+
+ return True
+
+ except Exception as e:
+ error_msg = f"خطأ في دمج ملفات PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def split_pdf(input_path, output_dir, prefix='page'):
+ """
+ تقسيم ملف PDF إلى ملفات منفصلة لكل صفحة
+
+ المعلمات:
+ input_path: مسار ملف PDF المراد تقسيمه
+ output_dir: دليل الإخراج
+ prefix: بادئة أسماء ملفات الإخراج
+
+ الإرجاع:
+ قائمة من مسارات ملفات PDF الناتجة
+ """
+ try:
+ # التحقق من وجود الملف
+ if not os.path.exists(input_path):
+ raise FileNotFoundError(f"الملف غير موجود: {input_path}")
+
+ # إنشاء دليل الإخراج
+ create_directory_if_not_exists(output_dir)
+
+ # قراءة ملف PDF
+ with open(input_path, 'rb') as file:
+ reader = PyPDF2.PdfReader(file)
+ output_files = []
+
+ # تقسيم كل صفحة إلى ملف منفصل
+ for page_idx in range(len(reader.pages)):
+ writer = PyPDF2.PdfWriter()
+ writer.add_page(reader.pages[page_idx])
+
+ output_filename = f"{prefix}_{page_idx+1}.pdf"
+ output_path = os.path.join(output_dir, output_filename)
+
+ with open(output_path, 'wb') as output_file:
+ writer.write(output_file)
+
+ output_files.append(output_path)
+
+ return output_files
+
+ except Exception as e:
+ error_msg = f"خطأ في تقسيم ملف PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def export_pricing_to_pdf(data, output_path, title="تحليل الأسعار", description=""):
+ """
+ تصدير بيانات التسعير إلى ملف PDF
+
+ المعلمات:
+ data: DataFrame يحتوي على بيانات التسعير
+ output_path: مسار ملف PDF الناتج
+ title: عنوان التقرير
+ description: وصف إضافي للتقرير
+
+ الإرجاع:
+ مسار ملف PDF الناتج
+ """
+ try:
+ # التحقق من أن البيانات ليست فارغة
+ if data is None or len(data) == 0:
+ raise ValueError("البيانات فارغة")
+
+ # إنشاء دليل الإخراج إذا لم يكن موجودًا
+ output_dir = os.path.dirname(output_path)
+ create_directory_if_not_exists(output_dir)
+
+ # إعداد أنماط النصوص
+ styles = getSampleStyleSheet()
+
+ # محاولة استخدام الخطوط العربية إذا كانت متوفرة
+ try:
+ # تسجيل الخطوط العربية إذا لم تكن مسجلة
+ if 'Arabic' not in pdfmetrics.getRegisteredFontNames():
+ pdfmetrics.registerFont(TTFont('Arabic', 'utils/fonts/Amiri-Regular.ttf'))
+
+ if 'ArabicBold' not in pdfmetrics.getRegisteredFontNames():
+ pdfmetrics.registerFont(TTFont('ArabicBold', 'utils/fonts/Amiri-Bold.ttf'))
+
+ # إنشاء أنماط للنصوص العربية
+ title_style = ParagraphStyle(
+ name='ArabicTitleStyle',
+ fontName='ArabicBold',
+ fontSize=16,
+ alignment=1, # وسط
+ leading=18
+ )
+
+ normal_style = ParagraphStyle(
+ name='ArabicStyle',
+ fontName='Arabic',
+ fontSize=12,
+ alignment=1, # وسط
+ leading=14
+ )
+
+ except:
+ # استخدام الخطوط الافتراضية إذا تعذر استخدام الخطوط العربية
+ title_style = styles['Title']
+ normal_style = styles['Normal']
+
+ # إنشاء وثيقة PDF
+ doc = SimpleDocTemplate(
+ output_path,
+ pagesize=landscape(A4),
+ rightMargin=30,
+ leftMargin=30,
+ topMargin=30,
+ bottomMargin=30
+ )
+
+ # قائمة العناصر في الوثيقة
+ elements = []
+
+ # إضافة العنوان
+ elements.append(Paragraph(title, title_style))
+ elements.append(Spacer(1, 20))
+
+ # إضافة الوصف إذا كان موجودًا
+ if description:
+ elements.append(Paragraph(description, normal_style))
+ elements.append(Spacer(1, 20))
+
+ # تحويل DataFrame إلى قائمة لاستخدامها في الجدول
+ data_list = [data.columns.tolist()] + data.values.tolist()
+
+ # إنشاء الجدول
+ table = Table(data_list, repeatRows=1)
+
+ # إضافة تنسيق للجدول
+ table_style = TableStyle([
+ ('BACKGROUND', (0, 0), (-1, 0), colors.lightblue),
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
+ ('FONTNAME', (0, 0), (-1, 0), 'ArabicBold' if 'ArabicBold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'),
+ ('FONTSIZE', (0, 0), (-1, 0), 12),
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
+ ('BACKGROUND', (0, 1), (-1, -1), colors.white),
+ ('GRID', (0, 0), (-1, -1), 1, colors.black),
+ ('FONTNAME', (0, 1), (-1, -1), 'Arabic' if 'Arabic' in pdfmetrics.getRegisteredFontNames() else 'Helvetica'),
+ ('FONTSIZE', (0, 1), (-1, -1), 10),
+ ])
+
+ # إضافة تلوين للصفوف المتناوبة
+ for i in range(1, len(data_list)):
+ if i % 2 == 0:
+ table_style.add('BACKGROUND', (0, i), (-1, i), colors.lightgrey)
+
+ table.setStyle(table_style)
+ elements.append(table)
+
+ # إضافة ملخص إحصائي إذا كانت البيانات تحتوي على أعمدة رقمية
+ numeric_columns = data.select_dtypes(include=['number']).columns
+ if len(numeric_columns) > 0:
+ elements.append(Spacer(1, 30))
+ elements.append(Paragraph("ملخص إحصائي", title_style))
+ elements.append(Spacer(1, 10))
+
+ # إنشاء ملخص إحصائي
+ summary = data[numeric_columns].describe().reset_index()
+ summary_list = [['الإحصاءات'] + list(numeric_columns)] + summary.values.tolist()
+
+ # إنشاء جدول الملخص
+ summary_table = Table(summary_list, repeatRows=1)
+ summary_table.setStyle(TableStyle([
+ ('BACKGROUND', (0, 0), (-1, 0), colors.lightblue),
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
+ ('FONTNAME', (0, 0), (-1, 0), 'ArabicBold' if 'ArabicBold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'),
+ ('FONTSIZE', (0, 0), (-1, 0), 12),
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
+ ('BACKGROUND', (0, 1), (-1, -1), colors.white),
+ ('GRID', (0, 0), (-1, -1), 1, colors.black),
+ ('FONTNAME', (0, 1), (-1, -1), 'Arabic' if 'Arabic' in pdfmetrics.getRegisteredFontNames() else 'Helvetica'),
+ ('FONTSIZE', (0, 1), (-1, -1), 10),
+ ]))
+ elements.append(summary_table)
+
+ # إنشاء مخططات للأعمدة الرقمية الرئيسية
+ if len(numeric_columns) > 0 and len(data) > 1:
+ for col in numeric_columns[:2]: # أخذ أول عمودين رقميين فقط
+ plt.figure(figsize=(8, 4))
+ plt.barh(range(len(data)), data[col])
+ plt.yticks(range(len(data)), data.iloc[:, 0] if len(data.columns) > 0 else range(len(data)))
+ plt.xlabel(col)
+ plt.title(f"مخطط {col}")
+ plt.tight_layout()
+
+ # حفظ المخطط كصورة في الذاكرة
+ img_data = io.BytesIO()
+ plt.savefig(img_data, format='png')
+ img_data.seek(0)
+ plt.close()
+
+ # إضافة المخطط إلى الوثيقة
+ elements.append(Spacer(1, 20))
+ img = ReportLabImage(img_data, width=500, height=250)
+ elements.append(img)
+
+ # بناء الوثيقة
+ doc.build(elements)
+
+ return output_path
+
+ except Exception as e:
+ error_msg = f"خطأ في تصدير بيانات التسعير إلى PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def export_pricing_with_analysis_to_pdf(items_data, analysis_data, output_path, title="تحليل الأسعار مع تحليل المكونات", project_info=None):
+ """
+ تصدير بيانات التسعير مع تحليل المكونات إلى ملف PDF
+
+ المعلمات:
+ items_data: DataFrame يحتوي على بيانات البنود
+ analysis_data: قاموس يحتوي على تحليل أسعار البنود {item_id: DataFrame}
+ output_path: مسار ملف PDF الناتج
+ title: عنوان التقرير
+ project_info: معلومات المشروع (قاموس)
+
+ الإرجاع:
+ مسار ملف PDF الناتج
+ """
+ try:
+ # التحقق من أن البيانات ليست فارغة
+ if items_data is None or len(items_data) == 0:
+ raise ValueError("بيانات البنود فارغة")
+
+ # إنشاء دليل الإخراج إذا لم يكن موجودًا
+ output_dir = os.path.dirname(output_path)
+ create_directory_if_not_exists(output_dir)
+
+ # إعداد أنماط النصوص
+ styles = getSampleStyleSheet()
+
+ # محاولة استخدام الخطوط العربية إذا كانت متوفرة
+ try:
+ # تسجيل الخطوط العربية إذا لم تكن مسجلة
+ if 'Arabic' not in pdfmetrics.getRegisteredFontNames():
+ pdfmetrics.registerFont(TTFont('Arabic', 'utils/fonts/Amiri-Regular.ttf'))
+
+ if 'ArabicBold' not in pdfmetrics.getRegisteredFontNames():
+ pdfmetrics.registerFont(TTFont('ArabicBold', 'utils/fonts/Amiri-Bold.ttf'))
+
+ # إنشاء أنماط للنصوص العربية
+ title_style = ParagraphStyle(
+ name='ArabicTitleStyle',
+ fontName='ArabicBold',
+ fontSize=16,
+ alignment=1, # وسط
+ leading=18
+ )
+
+ normal_style = ParagraphStyle(
+ name='ArabicStyle',
+ fontName='Arabic',
+ fontSize=12,
+ alignment=1, # وسط
+ leading=14
+ )
+
+ subtitle_style = ParagraphStyle(
+ name='ArabicSubtitleStyle',
+ fontName='ArabicBold',
+ fontSize=14,
+ alignment=1, # وسط
+ leading=16
+ )
+
+ except:
+ # استخدام الخطوط الافتراضية إذا تعذر استخدام الخطوط العربية
+ title_style = styles['Title']
+ normal_style = styles['Normal']
+ subtitle_style = styles['Heading2']
+
+ # إنشاء وثيقة PDF
+ doc = SimpleDocTemplate(
+ output_path,
+ pagesize=landscape(A4),
+ rightMargin=30,
+ leftMargin=30,
+ topMargin=30,
+ bottomMargin=30
+ )
+
+ # قائمة العناصر في الوثيقة
+ elements = []
+
+ # إضافة العنوان
+ elements.append(Paragraph(title, title_style))
+ elements.append(Spacer(1, 20))
+
+ # إضافة معلومات المشروع إذا كانت موجودة
+ if project_info:
+ project_info_text = f"اسم المشروع: {project_info.get('اسم_المشروع', '')} | "
+ project_info_text += f"وصف المشروع: {project_info.get('وصف_المشروع', '')}"
+ elements.append(Paragraph(project_info_text, normal_style))
+ elements.append(Spacer(1, 20))
+
+ # إضافة جدول البنود
+ elements.append(Paragraph("جدول البنود", subtitle_style))
+ elements.append(Spacer(1, 10))
+
+ # تحويل DataFrame إلى قائمة لاستخدامها في الجدول
+ items_list = [items_data.columns.tolist()] + items_data.values.tolist()
+
+ # إنشاء الجدول
+ items_table = Table(items_list, repeatRows=1)
+
+ # إضافة تنسيق للجدول
+ items_table_style = TableStyle([
+ ('BACKGROUND', (0, 0), (-1, 0), colors.lightblue),
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
+ ('FONTNAME', (0, 0), (-1, 0), 'ArabicBold' if 'ArabicBold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'),
+ ('FONTSIZE', (0, 0), (-1, 0), 12),
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
+ ('BACKGROUND', (0, 1), (-1, -1), colors.white),
+ ('GRID', (0, 0), (-1, -1), 1, colors.black),
+ ('FONTNAME', (0, 1), (-1, -1), 'Arabic' if 'Arabic' in pdfmetrics.getRegisteredFontNames() else 'Helvetica'),
+ ('FONTSIZE', (0, 1), (-1, -1), 10),
+ ])
+
+ # إضافة تلوين للصفوف المتناوبة
+ for i in range(1, len(items_list)):
+ if i % 2 == 0:
+ items_table_style.add('BACKGROUND', (0, i), (-1, i), colors.lightgrey)
+
+ items_table.setStyle(items_table_style)
+ elements.append(items_table)
+
+ # إضافة تحليل الأسعار لكل بند
+ if analysis_data:
+ elements.append(Spacer(1, 30))
+ elements.append(Paragraph("تحليل مكونات الأسعار", subtitle_style))
+
+ for item_id, analysis_df in analysis_data.items():
+ # الحصول على معلومات البند
+ item_info = items_data[items_data['رقم البند'] == item_id].iloc[0] if 'رقم البند' in items_data.columns and len(items_data[items_data['رقم البند'] == item_id]) > 0 else None
+
+ if item_info is not None:
+ item_title = f"تحليل مكونات البند: {item_id} - {item_info.get('وصف البند', '')}"
+ else:
+ item_title = f"تحليل مكونات البند: {item_id}"
+
+ elements.append(Spacer(1, 20))
+ elements.append(Paragraph(item_title, subtitle_style))
+ elements.append(Spacer(1, 10))
+
+ # تحويل DataFrame إلى قائمة لاستخدامها في الجدول
+ analysis_list = [analysis_df.columns.tolist()] + analysis_df.values.tolist()
+
+ # إنشاء الجدول
+ analysis_table = Table(analysis_list, repeatRows=1)
+
+ # إضافة تنسيق للجدول
+ analysis_table_style = TableStyle([
+ ('BACKGROUND', (0, 0), (-1, 0), colors.lightblue),
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
+ ('FONTNAME', (0, 0), (-1, 0), 'ArabicBold' if 'ArabicBold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'),
+ ('FONTSIZE', (0, 0), (-1, 0), 12),
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
+ ('BACKGROUND', (0, 1), (-1, -1), colors.white),
+ ('GRID', (0, 0), (-1, -1), 1, colors.black),
+ ('FONTNAME', (0, 1), (-1, -1), 'Arabic' if 'Arabic' in pdfmetrics.getRegisteredFontNames() else 'Helvetica'),
+ ('FONTSIZE', (0, 1), (-1, -1), 10),
+ ])
+
+ # إضافة تلوين للصفوف المتناوبة
+ for i in range(1, len(analysis_list)):
+ if i % 2 == 0:
+ analysis_table_style.add('BACKGROUND', (0, i), (-1, i), colors.lightgrey)
+
+ analysis_table.setStyle(analysis_table_style)
+ elements.append(analysis_table)
+
+ # حساب وعرض إجمالي تحليل السعر
+ if 'الإجمالي' in analysis_df.columns and len(analysis_df) > 0:
+ total_analysis_price = analysis_df['الإجمالي'].sum()
+ total_text = f"إجمالي تكلفة البند من التحليل: {total_analysis_price:,.2f} ريال"
+ elements.append(Spacer(1, 10))
+ elements.append(Paragraph(total_text, normal_style))
+
+ # بناء الوثيقة
+ doc.build(elements)
+
+ return output_path
+
+ except Exception as e:
+ error_msg = f"خطأ في تصدير تحليل الأسعار إلى PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
diff --git a/utils/pdf_handler.py.bak b/utils/pdf_handler.py.bak
new file mode 100644
index 0000000000000000000000000000000000000000..1da94f2ef06d7df164a62c9ce55e0ce3c14631e6
--- /dev/null
+++ b/utils/pdf_handler.py.bak
@@ -0,0 +1,476 @@
+"""
+معالج ملفات PDF
+"""
+
+import os
+import io
+import re
+import PyPDF2
+import fitz # PyMuPDF
+import pdfplumber
+import numpy as np
+from PIL import Image
+import pytesseract
+import pandas as pd
+import traceback
+
+from utils.helpers import create_directory_if_not_exists, extract_numbers_from_text
+
+
+def extract_text_from_pdf(file_path, method='pymupdf'):
+ """
+ استخراج النص من ملف PDF
+
+ المعلمات:
+ file_path: مسار ملف PDF
+ method: طريقة الاستخراج ('pymupdf', 'pypdf2', 'pdfplumber')
+
+ الإرجاع:
+ نص مستخرج من ملف PDF
+ """
+ try:
+ # التحقق من وجود الملف
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"الملف غير موجود: {file_path}")
+
+ # استخراج النص حسب الطريقة المطلوبة
+ if method.lower() == 'pymupdf':
+ return _extract_text_with_pymupdf(file_path)
+ elif method.lower() == 'pypdf2':
+ return _extract_text_with_pypdf2(file_path)
+ elif method.lower() == 'pdfplumber':
+ return _extract_text_with_pdfplumber(file_path)
+ else:
+ # استخدام PyMuPDF كطريقة افتراضية
+ return _extract_text_with_pymupdf(file_path)
+
+ except Exception as e:
+ error_msg = f"خطأ في استخراج النص من ملف PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def _extract_text_with_pymupdf(file_path):
+ """استخراج النص باستخدام PyMuPDF"""
+ document = fitz.open(file_path)
+ text = ""
+
+ for page_number in range(len(document)):
+ page = document.load_page(page_number)
+ text += page.get_text("text") + "\n\n"
+
+ document.close()
+ return text
+
+
+def _extract_text_with_pypdf2(file_path):
+ """استخراج النص باستخدام PyPDF2"""
+ with open(file_path, 'rb') as file:
+ reader = PyPDF2.PdfReader(file)
+ text = ""
+
+ for page_number in range(len(reader.pages)):
+ page = reader.pages[page_number]
+ text += page.extract_text() + "\n\n"
+
+ return text
+
+
+def _extract_text_with_pdfplumber(file_path):
+ """استخراج النص باستخدام pdfplumber"""
+ with pdfplumber.open(file_path) as pdf:
+ text = ""
+
+ for page in pdf.pages:
+ text += page.extract_text() + "\n\n"
+
+ return text
+
+
+def extract_tables_from_pdf(file_path, page_numbers=None):
+ """
+ استخراج الجداول من ملف PDF
+
+ المعلمات:
+ file_path: مسار ملف PDF
+ page_numbers: قائمة بأرقام الصفحات للاستخراج منها (افتراضي: None لجميع الصفحات)
+
+ الإرجاع:
+ قائمة من DataFrames تمثل الجداول المستخرجة
+ """
+ try:
+ # التحقق من وجود الملف
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"الملف غير موجود: {file_path}")
+
+ # استخراج الجداول باستخدام pdfplumber
+ tables = []
+
+ with pdfplumber.open(file_path) as pdf:
+ # تحديد الصفحات المراد استخراج الجداول منها
+ if page_numbers is None:
+ pages_to_extract = range(len(pdf.pages))
+ else:
+ pages_to_extract = [p-1 for p in page_numbers if 1 <= p <= len(pdf.pages)]
+
+ # استخراج الجداول من كل صفحة
+ for page_idx in pages_to_extract:
+ page = pdf.pages[page_idx]
+ page_tables = page.extract_tables()
+
+ if page_tables:
+ for table in page_tables:
+ if table: # التحقق من أن الجدول ليس فارغًا
+ # تحويل الجدول إلى DataFrame
+ df = pd.DataFrame(table[1:], columns=table[0])
+
+ # تنظيف البيانات
+ df = df.applymap(lambda x: x.strip() if isinstance(x, str) else x)
+
+ # إضافة إلى قائمة الجداول
+ tables.append(df)
+
+ return tables
+
+ except Exception as e:
+ error_msg = f"خطأ في استخراج الجداول من ملف PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def extract_images_from_pdf(file_path, output_dir=None, prefix='image'):
+ """
+ استخراج الصور من ملف PDF
+
+ المعلمات:
+ file_path: مسار ملف PDF
+ output_dir: دليل الإخراج (افتراضي: None للإرجاع كقائمة من الصور)
+ prefix: بادئة أسماء ملفات الصور
+
+ الإرجاع:
+ قائمة من مسارات الصور المستخرجة إذا تم تحديد دليل الإخراج، وإلا قائمة من الصور
+ """
+ try:
+ # التحقق من وجود الملف
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"الملف غير موجود: {file_path}")
+
+ # إنشاء دليل الإخراج إذا تم تحديده
+ if output_dir:
+ create_directory_if_not_exists(output_dir)
+
+ # استخراج الصور باستخدام PyMuPDF
+ document = fitz.open(file_path)
+ images = []
+ image_paths = []
+
+ for page_idx in range(len(document)):
+ page = document.load_page(page_idx)
+
+ # استخراج الصور من الصفحة
+ image_list = page.get_images(full=True)
+
+ for img_idx, img_info in enumerate(image_list):
+ xref = img_info[0]
+ base_image = document.extract_image(xref)
+ image_bytes = base_image["image"]
+
+ # إنشاء كائن الصورة
+ image = Image.open(io.BytesIO(image_bytes))
+
+ if output_dir:
+ # حفظ الصورة في الدليل المحدد
+ image_filename = f"{prefix}_{page_idx+1}_{img_idx+1}.{base_image['ext']}"
+ image_path = os.path.join(output_dir, image_filename)
+ image.save(image_path)
+ image_paths.append(image_path)
+ else:
+ # إضافة الصورة إلى القائمة
+ images.append(image)
+
+ document.close()
+
+ return image_paths if output_dir else images
+
+ except Exception as e:
+ error_msg = f"خطأ في استخراج الصور من ملف PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def extract_text_from_image(image, lang='ara+eng'):
+ """
+ استخراج النص من صورة باستخدام OCR
+
+ المعلمات:
+ image: كائن الصورة أو مسار الصورة
+ lang: لغة النص (افتراضي: 'ara+eng' للعربية والإنجليزية)
+
+ الإرجاع:
+ النص المستخرج من الصورة
+ """
+ try:
+ # إذا كان مسار صورة، قم بفتحها
+ if isinstance(image, str):
+ image = Image.open(image)
+
+ # استخراج النص باستخدام pytesseract
+ text = pytesseract.image_to_string(image, lang=lang)
+
+ return text
+
+ except Exception as e:
+ error_msg = f"خطأ في استخراج النص من الصورة: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ return ""
+
+
+def ocr_pdf(file_path, lang='ara+eng'):
+ """
+ تنفيذ OCR على ملف PDF
+
+ المعلمات:
+ file_path: مسار ملف PDF
+ lang: لغة النص (افتراضي: 'ara+eng' للعربية والإنجليزية)
+
+ الإرجاع:
+ النص المستخرج من ملف PDF
+ """
+ try:
+ # التحقق من وجود الملف
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"الملف غير موجود: {file_path}")
+
+ # فتح ملف PDF
+ document = fitz.open(file_path)
+ text = ""
+
+ for page_idx in range(len(document)):
+ page = document.load_page(page_idx)
+
+ # تحويل الصفحة إلى صورة
+ pix = page.get_pixmap(matrix=fitz.Matrix(300/72, 300/72))
+ img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
+
+ # استخراج النص من الصورة
+ page_text = extract_text_from_image(img, lang=lang)
+ text += page_text + "\n\n"
+
+ document.close()
+
+ return text
+
+ except Exception as e:
+ error_msg = f"خطأ في تنفيذ OCR على ملف PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def search_in_pdf(file_path, search_text):
+ """
+ البحث عن نص في ملف PDF
+
+ المعلمات:
+ file_path: مسار ملف PDF
+ search_text: النص المراد البحث عنه
+
+ الإرجاع:
+ قائمة من النتائج {page_number, text_snippet, matched_text}
+ """
+ try:
+ # التحقق من وجود الملف
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"الملف غير موجود: {file_path}")
+
+ # استخراج النص من ملف PDF
+ document = fitz.open(file_path)
+ results = []
+
+ for page_idx in range(len(document)):
+ page = document.load_page(page_idx)
+ page_text = page.get_text("text")
+
+ # البحث عن النص
+ if search_text.lower() in page_text.lower():
+ # استخراج المقتطفات التي تحتوي على النص المطلوب
+ lines = page_text.split('\n')
+ for line in lines:
+ if search_text.lower() in line.lower():
+ results.append({
+ 'page_number': page_idx + 1,
+ 'text_snippet': line,
+ 'matched_text': search_text
+ })
+
+ document.close()
+
+ return results
+
+ except Exception as e:
+ error_msg = f"خطأ في البحث في ملف PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def extract_quantities_from_pdf(file_path):
+ """
+ استخراج الكميات من ملف PDF
+
+ المعلمات:
+ file_path: مسار ملف PDF
+
+ الإرجاع:
+ DataFrame يحتوي على البنود والكميات المستخرجة
+ """
+ try:
+ # استخراج النص والجداول
+ text = extract_text_from_pdf(file_path)
+ tables = extract_tables_from_pdf(file_path)
+
+ quantities = []
+
+ # استخراج الكميات من الجداول
+ for table in tables:
+ # البحث عن أعمدة تحتوي على "الكمية" أو "الوحدة" أو "البند"
+ quantity_cols = [col for col in table.columns if any(term in col.lower() for term in ['كمية', 'عدد', 'الكمية'])]
+ unit_cols = [col for col in table.columns if any(term in col.lower() for term in ['وحدة', 'الوحدة'])]
+ item_cols = [col for col in table.columns if any(term in col.lower() for term in ['بند', 'وصف', 'البند', 'العمل'])]
+
+ if quantity_cols and (unit_cols or item_cols):
+ quantity_col = quantity_cols[0]
+ unit_col = unit_cols[0] if unit_cols else None
+ item_col = item_cols[0] if item_cols else None
+
+ # استخراج الكميات
+ for _, row in table.iterrows():
+ if pd.notna(row[quantity_col]) and (item_col is None or pd.notna(row[item_col])):
+ quantity_value = extract_numbers_from_text(row[quantity_col])
+ quantity = quantity_value[0] if quantity_value else None
+
+ quantities.append({
+ 'البند': row[item_col] if item_col else "غير محدد",
+ 'الوحدة': row[unit_col] if unit_col else "غير محدد",
+ 'الكمية': quantity
+ })
+
+ # استخراج الكميات من النص
+ lines = text.split('\n')
+ for line in lines:
+ # البحث عن الخطوط التي تحتوي على أرقام ووحدات قياس
+ if re.search(r'\d+(?:,\d+)*(?:\.\d+)?', line) and any(unit in line for unit in ['م2', 'م3', 'متر', 'طن', 'كجم', 'عدد']):
+ numbers = extract_numbers_from_text(line)
+ if numbers:
+ # استخراج وحدة القياس
+ unit_match = re.search(r'\b(م2|م3|متر مربع|متر مكعب|م\.ط|طن|كجم|عدد|قطعة)\b', line)
+ unit = unit_match.group(1) if unit_match else "غير محدد"
+
+ quantities.append({
+ 'البند': line,
+ 'الوحدة': unit,
+ 'الكمية': numbers[0]
+ })
+
+ # إنشاء DataFrame
+ if quantities:
+ quantities_df = pd.DataFrame(quantities)
+ return quantities_df
+ else:
+ return pd.DataFrame(columns=['البند', 'الوحدة', 'الكمية'])
+
+ except Exception as e:
+ error_msg = f"خطأ في استخراج الكميات من ملف PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def merge_pdfs(input_paths, output_path):
+ """
+ دمج ملفات PDF متعددة في ملف واحد
+
+ المعلمات:
+ input_paths: قائمة من مسارات ملفات PDF المراد دمجها
+ output_path: مسار ملف PDF الناتج
+
+ الإرجاع:
+ True في حالة النجاح
+ """
+ try:
+ # التحقق من وجود الملفات
+ for file_path in input_paths:
+ if not os.path.exists(file_path):
+ raise FileNotFoundError(f"الملف غير موجود: {file_path}")
+
+ # إنشاء مجلد الإخراج إذا لم يكن موجودًا
+ output_dir = os.path.dirname(output_path)
+ create_directory_if_not_exists(output_dir)
+
+ # دمج ملفات PDF
+ merger = PyPDF2.PdfMerger()
+
+ for file_path in input_paths:
+ merger.append(file_path)
+
+ merger.write(output_path)
+ merger.close()
+
+ return True
+
+ except Exception as e:
+ error_msg = f"خطأ في دمج ملفات PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
+
+
+def split_pdf(input_path, output_dir, prefix='page'):
+ """
+ تقسيم ملف PDF إلى ملفات منفصلة لكل صفحة
+
+ المعلمات:
+ input_path: مسار ملف PDF المراد تقسيمه
+ output_dir: دليل الإخراج
+ prefix: بادئة أسماء ملفات الإخراج
+
+ الإرجاع:
+ قائمة من مسارات ملفات PDF الناتجة
+ """
+ try:
+ # التحقق من وجود الملف
+ if not os.path.exists(input_path):
+ raise FileNotFoundError(f"الملف غير موجود: {input_path}")
+
+ # إنشاء دليل الإخراج
+ create_directory_if_not_exists(output_dir)
+
+ # قراءة ملف PDF
+ with open(input_path, 'rb') as file:
+ reader = PyPDF2.PdfReader(file)
+ output_files = []
+
+ # تقسيم كل صفحة إلى ملف منفصل
+ for page_idx in range(len(reader.pages)):
+ writer = PyPDF2.PdfWriter()
+ writer.add_page(reader.pages[page_idx])
+
+ output_filename = f"{prefix}_{page_idx+1}.pdf"
+ output_path = os.path.join(output_dir, output_filename)
+
+ with open(output_path, 'wb') as output_file:
+ writer.write(output_file)
+
+ output_files.append(output_path)
+
+ return output_files
+
+ except Exception as e:
+ error_msg = f"خطأ في تقسيم ملف PDF: {str(e)}"
+ print(error_msg)
+ traceback.print_exc()
+ raise Exception(error_msg)
\ No newline at end of file
diff --git a/utils/session_state.py b/utils/session_state.py
new file mode 100644
index 0000000000000000000000000000000000000000..93bdbfb0b81cea9622b6ecf0b4405bb9b41cc429
--- /dev/null
+++ b/utils/session_state.py
@@ -0,0 +1,247 @@
+"""
+وحدة إدارة حالة الجلسة
+"""
+
+import streamlit as st
+from datetime import datetime
+import pandas as pd
+import config
+
+
+def initialize_session_state():
+ """
+ تهيئة متغيرات حالة الجلسة
+ """
+ # المتغيرات الرئيسية لحالة المستخدم
+ if 'is_authenticated' not in st.session_state:
+ st.session_state.is_authenticated = False
+
+ if 'user_info' not in st.session_state:
+ st.session_state.user_info = None
+
+ # المتغيرات المتعلقة بالمناقصات والمشاريع
+ if 'current_project' not in st.session_state:
+ st.session_state.current_project = None
+
+ if 'current_pricing' not in st.session_state:
+ st.session_state.current_pricing = None
+
+ if 'pricing_history' not in st.session_state:
+ st.session_state.pricing_history = []
+
+ # المتغيرات المتعلقة بالمحتوى المحلي
+ if 'local_content_products' not in st.session_state:
+ st.session_state.local_content_products = pd.DataFrame({
+ 'المنتج': [],
+ 'الكمية': [],
+ 'سعر_الوحدة': [],
+ 'التكلفة_الإجمالية': [],
+ 'نسبة_المحتوى_المحلي': []
+ })
+
+ if 'local_content_services' not in st.session_state:
+ st.session_state.local_content_services = pd.DataFrame({
+ 'الخدمة': [],
+ 'التكلفة': [],
+ 'نسبة_المحتوى_المحلي': []
+ })
+
+ if 'local_content_labor' not in st.session_state:
+ st.session_state.local_content_labor = pd.DataFrame({
+ 'فئة_العمالة': [],
+ 'العدد': [],
+ 'الراتب_الشهري': [],
+ 'المدة_بالأشهر': [],
+ 'نسبة_المحتوى_المحلي': []
+ })
+
+ # المتغيرات المتعلقة بتحليل المستندات
+ if 'current_document' not in st.session_state:
+ st.session_state.current_document = None
+
+ if 'analyzed_documents' not in st.session_state:
+ st.session_state.analyzed_documents = []
+
+ # المتغيرات المتعلقة بالتسعير
+ if 'manual_items' not in st.session_state:
+ st.session_state.manual_items = pd.DataFrame({
+ 'رقم البند': [],
+ 'وصف البند': [],
+ 'الوحدة': [],
+ 'الكمية': [],
+ 'سعر الوحدة': [],
+ 'الإجمالي': []
+ })
+
+ # المتغيرات المتعلقة بالمصادر
+ if 'resources' not in st.session_state:
+ st.session_state.resources = []
+
+ # المتغيرات المتعلقة بالإعدادات
+ if 'settings' not in st.session_state:
+ st.session_state.settings = {
+ 'ui_theme': config.UI_THEME,
+ 'locale': config.LOCALE,
+ 'enable_animations': config.ENABLE_ANIMATIONS
+ }
+
+
+def save_current_pricing():
+ """
+ حفظ التسعير الحالي في سجل التسعير
+
+ الإرجاع:
+ True في حالة النجاح، False في حالة الفشل
+ """
+ try:
+ if st.session_state.current_pricing:
+ # إضافة معلومات إضافية
+ pricing_entry = st.session_state.current_pricing.copy()
+ pricing_entry['timestamp'] = datetime.now()
+
+ # إضافة إلى سجل التسعير
+ st.session_state.pricing_history.append(pricing_entry)
+
+ return True
+ return False
+ except Exception as e:
+ print(f"خطأ في حفظ التسعير الحالي: {str(e)}")
+ return False
+
+
+def set_current_project(project_data):
+ """
+ تعيين المشروع الحالي
+
+ المعلمات:
+ project_data: بيانات المشروع
+ """
+ st.session_state.current_project = project_data
+
+
+def set_current_pricing(pricing_data):
+ """
+ تعيين التسعير الحالي
+
+ المعلمات:
+ pricing_data: بيانات التسعير
+ """
+ st.session_state.current_pricing = pricing_data
+
+
+def set_current_document(document_data):
+ """
+ تعيين المستند الحالي
+
+ المعلمات:
+ document_data: بيانات المستند
+ """
+ st.session_state.current_document = document_data
+
+
+def clear_session():
+ """
+ مسح بيانات الجلسة الحالية
+ """
+ # الاحتفاظ بحالة المصادقة والمستخدم
+ is_authenticated = st.session_state.is_authenticated
+ user_info = st.session_state.user_info
+ settings = st.session_state.settings
+
+ # مسح المتغيرات
+ st.session_state.clear()
+
+ # إعادة تعيين حالة المصادقة والمستخدم
+ st.session_state.is_authenticated = is_authenticated
+ st.session_state.user_info = user_info
+ st.session_state.settings = settings
+
+ # إعادة تهيئة متغيرات الجلسة
+ initialize_session_state()
+
+
+def export_session_state():
+ """
+ تصدير حالة الجلسة الحالية
+
+ الإرجاع:
+ قاموس يحتوي على حالة الجلسة
+ """
+ # إنشاء نسخة من حالة الجلسة
+ session_data = {}
+
+ # تخزين البيانات الرئيسية
+ if st.session_state.current_project:
+ session_data['current_project'] = st.session_state.current_project
+
+ if st.session_state.current_pricing:
+ session_data['current_pricing'] = st.session_state.current_pricing
+
+ if st.session_state.pricing_history:
+ session_data['pricing_history'] = st.session_state.pricing_history
+
+ # تحويل DataFrames إلى قوائم من القواميس
+ if 'local_content_products' in st.session_state and not st.session_state.local_content_products.empty:
+ session_data['local_content_products'] = st.session_state.local_content_products.to_dict('records')
+
+ if 'local_content_services' in st.session_state and not st.session_state.local_content_services.empty:
+ session_data['local_content_services'] = st.session_state.local_content_services.to_dict('records')
+
+ if 'local_content_labor' in st.session_state and not st.session_state.local_content_labor.empty:
+ session_data['local_content_labor'] = st.session_state.local_content_labor.to_dict('records')
+
+ # تخزين البيانات الأخرى
+ if 'manual_items' in st.session_state and not st.session_state.manual_items.empty:
+ session_data['manual_items'] = st.session_state.manual_items.to_dict('records')
+
+ if st.session_state.resources:
+ session_data['resources'] = st.session_state.resources
+
+ # إضافة بيانات الوقت
+ session_data['exported_at'] = datetime.now().isoformat()
+
+ return session_data
+
+
+def import_session_state(session_data):
+ """
+ استيراد حالة الجلسة
+
+ المعلمات:
+ session_data: قاموس يحتوي على حالة الجلسة
+
+ الإرجاع:
+ True في حالة النجاح، False في حالة الفشل
+ """
+ try:
+ # استيراد البيانات الرئيسية
+ if 'current_project' in session_data:
+ st.session_state.current_project = session_data['current_project']
+
+ if 'current_pricing' in session_data:
+ st.session_state.current_pricing = session_data['current_pricing']
+
+ if 'pricing_history' in session_data:
+ st.session_state.pricing_history = session_data['pricing_history']
+
+ # استيراد DataFrames
+ if 'local_content_products' in session_data:
+ st.session_state.local_content_products = pd.DataFrame(session_data['local_content_products'])
+
+ if 'local_content_services' in session_data:
+ st.session_state.local_content_services = pd.DataFrame(session_data['local_content_services'])
+
+ if 'local_content_labor' in session_data:
+ st.session_state.local_content_labor = pd.DataFrame(session_data['local_content_labor'])
+
+ # استيراد البيانات الأخرى
+ if 'manual_items' in session_data:
+ st.session_state.manual_items = pd.DataFrame(session_data['manual_items'])
+
+ if 'resources' in session_data:
+ st.session_state.resources = session_data['resources']
+
+ return True
+ except Exception as e:
+ print(f"خطأ في استيراد حالة الجلسة: {str(e)}")
+ return False
\ No newline at end of file