", 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 st.form(key="chat_form"):
- user_input = st.text_area("اكتب رسالتك هنا:", key="user_input", height=100)
- submit_button = st.form_submit_button("إرسال")
-
- if submit_button and user_input:
- # إضافة رسالة المستخدم إلى المحادثة
- st.session_state.chat_history.append({
- 'role': 'user',
- 'content': user_input
- })
-
- # محاكاة استجابة المساعد الذكي
- ai_responses = {
- "تكلفة": "بناءً على تحليل بيانات المشاريع السابقة، أتوقع أن تكون تكلفة هذا المشروع في حدود 15-18 مليون ريال. يمكنني تقديم تحليل تفصيلي إذا وفرت لي المزيد من المعلومات عن نطاق المشروع والمواصفات المطلوبة.",
- "مخاطر": "من أهم المخاطر المحتملة لهذا النوع من المشاريع: تأخر التوريدات، نقص العمالة الماهرة، التغييرات في نطاق العمل، والظروف الجوية غير المتوقعة. أنصح بوضع خطة إدارة مخاطر شاملة وتخصيص احتياطي للطوارئ بنسبة 10-15% من قيمة المشروع.",
- "منافس": "بناءً على تحليل المناقصات السابقة، يبدو أن المنافس الرئيسي يقدم أسعاراً أقل بنسبة 5-8% من متوسط السوق، لكنه يواجه تحديات في الالتزام بالجداول الزمنية. يمكنك التركيز على نقاط قوتك في الالتزام بالمواعيد وجودة التنفيذ في عرضك.",
- "مستند": "يمكنني تحليل مستندات المناقصة لاستخراج المعلومات الرئيسية مثل نطاق العمل، الشروط والمواصفات، الجداول الزمنية، وشروط الدفع. يرجى تحميل المستندات في تبويب تحليل المستندات.",
- "تسعير": "لتحسين استراتيجية التسعير، أنصح بتحليل هيكل التكاليف بدقة، ودراسة أسعار المنافسين، وتقييم القيمة المضافة التي تقدمها. يمكنك استخدام وحدة التسعير لإنشاء سيناريوهات تسعير مختلفة ومقارنتها.",
- "موارد": "بناءً على نطاق المشروع، أتوقع أنك ستحتاج إلى فريق من 15-20 مهندساً وفنياً، بالإضافة إلى معدات إنشائية رئيسية. يمكنك استخدام وحدة الموارد لتخطيط احتياجات المشروع بشكل تفصيلي."
- }
-
- # تحديد الاستجابة المناسبة بناءً على كلمات مفتاحية في رسالة المستخدم
- response = "أشكرك على رسالتك. يمكنني مساعدتك في إدارة المناقصات وتحليل المستندات وتقدير التكاليف وتحليل المخاطر. يرجى توضيح ما تحتاجه بالتحديد."
-
- for keyword, resp in ai_responses.items():
- if keyword in user_input:
- response = resp
- break
-
- # إضافة استجابة المساعد الذكي إلى المحادثة
- st.session_state.chat_history.append({
- 'role': 'assistant',
- 'content': response
- })
-
- # إعادة تحميل الصفحة لعرض المحادثة المحدثة
- st.rerun()
-
- # عرض اقتراحات للأسئلة
- st.markdown("### اقتراحات للأسئلة")
-
- suggestions = [
- "كيف يمكنني تقدير تكلفة مشروع إنشاء مبنى إداري؟",
- "ما هي المخاطر المحتملة لمشروع تطوير شبكة طرق؟",
- "كيف يمكنني تحليل استراتيجية المنافس الرئيسي؟",
- "كيف يمكنني تحليل مستندات المناقصة بسرعة؟",
- "ما هي أفضل استراتيجية للتسعير التنافسي؟",
- "كيف يمكنني تخطيط الموارد اللازمة للمشروع؟"
- ]
-
- col1, col2 = st.columns(2)
-
- with col1:
- for i in range(0, len(suggestions), 2):
- if st.button(suggestions[i], key=f"suggestion_{i}"):
- # إضافة السؤال المقترح إلى المحادثة
- st.session_state.chat_history.append({
- 'role': 'user',
- 'content': suggestions[i]
- })
-
- # تحديد الاستجابة المناسبة
- for keyword, resp in ai_responses.items():
- if keyword in suggestions[i].lower():
- response = resp
- break
- else:
- response = "أشكرك على سؤالك. يمكنني مساعدتك في ذلك. يرجى تقديم المزيد من التفاصيل حول احتياجاتك المحددة."
-
- # إضافة استجابة المساعد الذكي إلى المحادثة
- st.session_state.chat_history.append({
- 'role': 'assistant',
- 'content': response
- })
-
- # إعادة تحميل الصفحة لعرض المحادثة المحدثة
- st.rerun()
-
- with col2:
- for i in range(1, len(suggestions), 2):
- if st.button(suggestions[i], key=f"suggestion_{i}"):
- # إضافة السؤال المقترح إلى المحادثة
- st.session_state.chat_history.append({
- 'role': 'user',
- 'content': suggestions[i]
- })
-
- # تحديد الاستجابة المناسبة
- for keyword, resp in ai_responses.items():
- if keyword in suggestions[i].lower():
- response = resp
- break
- else:
- response = "أشكرك على سؤالك. يمكنني مساعدتك في ذلك. يرجى تقديم المزيد من التفاصيل حول احتياجاتك المحددة."
-
- # إضافة استجابة المساعد الذكي إلى المحادثة
- st.session_state.chat_history.append({
- 'role': 'assistant',
- 'content': response
- })
-
- # إعادة تحميل الصفحة لعرض المحادثة المحدثة
- st.rerun()
-
- def _render_document_analysis_tab(self):
- """عرض تبويب تحليل المستندات"""
-
- st.markdown("### تحليل المستندات باستخدام الذكاء الاصطناعي")
-
- # تحميل المستندات
- st.markdown("#### تحميل المستندات")
-
- uploaded_file = st.file_uploader("قم بتحميل مستند المناقصة (PDF, DOCX)", type=["pdf", "docx"])
-
- if uploaded_file is not None:
- if st.button("تحليل المستند"):
- # محاكاة تحليل المستند
- with st.spinner("جاري تحليل المستند..."):
- time.sleep(2) # محاكاة وقت المعالجة
- st.success("تم تحليل المستند بنجاح!")
-
- # إضافة ملخص المستند إلى قائمة الملخصات
- new_id = max([item['id'] for item in st.session_state.document_summaries], default=0) + 1
-
- st.session_state.document_summaries.append({
- 'id': new_id,
- 'title': uploaded_file.name,
- 'date': time.strftime("%Y-%m-%d"),
- 'summary': 'تم تحليل المستند واستخراج المعلومات الرئيسية منه. يتضمن المستند شروط ومواصفات المناقصة، ونطاق العمل، والجدول الزمني، وشروط الدفع.',
- 'key_points': [
- 'مدة التنفيذ: 12 شهراً',
- 'قيمة الضمان الابتدائي: 2% من قيمة العطاء',
- 'قيمة الضمان النهائي: 5% من قيمة العقد',
- 'غرامة التأخير: 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%',
- 'شروط الدفع: دفعات شهرية حسب نسبة الإنجاز'
- ],
- 'entities': {
- 'الجهة المالكة': 'وزارة الإسكان',
- 'موقع المشروع': 'الرياض',
- 'رقم المناقصة': 'T-2024-004',
- 'تاريخ الطرح': '2024-03-25',
- 'تاريخ الإقفال': '2024-05-01'
- }
- })
-
- # عرض ملخصات المستندات
- st.markdown("#### ملخصات المستندات")
-
- for summary in st.session_state.document_summaries:
- with st.expander(f"{summary['title']} - {summary['date']}"):
- st.markdown(f"**ملخص المستند:** {summary['summary']}")
-
- st.markdown("**النقاط الرئيسية:**")
- for point in summary['key_points']:
- st.markdown(f"- {point}")
-
- st.markdown("**الكيانات المستخرجة:**")
- for entity, value in summary['entities'].items():
- st.markdown(f"- **{entity}:** {value}")
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- if st.button("تصدير الملخص", key=f"export_summary_{summary['id']}"):
- st.success("تم تصدير الملخص بنجاح!")
-
- with col2:
- if st.button("إرسال إلى وحدة التسعير", key=f"send_to_pricing_{summary['id']}"):
- st.success("تم إرسال البيانات إلى وحدة التسعير بنجاح!")
-
- with col3:
- if st.button("إرسال إلى وحدة المخاطر", key=f"send_to_risk_{summary['id']}"):
- st.success("تم إرسال البيانات إلى وحدة المخاطر بنجاح!")
-
- # استخراج جدول الكميات
- st.markdown("#### استخراج جدول الكميات")
-
- boq_file = st.file_uploader("قم بتحميل جدول الكميات (PDF, XLSX)", type=["pdf", "xlsx"])
-
- if boq_file is not None:
- if st.button("استخراج جدول الكميات"):
- # محاكاة استخراج جدول الكميات
- with st.spinner("جاري استخراج جدول الكميات..."):
- time.sleep(2) # محاكاة وقت المعالجة
- st.success("تم استخراج جدول الكميات بنجاح!")
-
- # عرض جدول الكميات المستخرج
- boq_data = {
- 'الكود': ['A-001', 'A-002', 'A-003', 'B-001', 'B-002'],
- 'الوصف': [
- 'أعمال الحفر والردم',
- 'توريد وصب خرسانة عادية',
- 'توريد وصب خرسانة مسلحة للأساسات',
- 'توريد وتركيب حديد تسليح',
- 'توريد وبناء طابوق'
- ],
- 'الوحدة': ['م3', 'م3', 'م3', 'طن', 'م2'],
- 'الكمية': [2000, 300, 200, 20, 500],
- 'سعر الوحدة': [45, 350, 450, 3500, 120],
- 'الإجمالي': [90000, 105000, 90000, 70000, 60000]
- }
-
- boq_df = pd.DataFrame(boq_data)
- st.dataframe(boq_df, use_container_width=True, hide_index=True)
-
- if st.button("إرسال إلى وحدة التسعير", key="send_boq_to_pricing"):
- st.success("تم إرسال جدول الكميات إلى وحدة التسعير بنجاح!")
-
- # تحليل الشروط والمواصفات
- st.markdown("#### تحليل الشروط والمواصفات")
-
- specs_file = st.file_uploader("قم بتحميل الشروط والمواصفات (PDF, DOCX)", type=["pdf", "docx"])
-
- if specs_file is not None:
- if st.button("تحليل الشروط والمواصفات"):
- # محاكاة تحليل الشروط والمواصفات
- with st.spinner("جاري تحليل الشروط والمواصفات..."):
- time.sleep(2) # محاكاة وقت المعالجة
- st.success("تم تحليل الشروط والمواصفات بنجاح!")
-
- # عرض نتائج التحليل
- st.markdown("**الشروط الرئيسية:**")
- st.markdown("- مدة التنفيذ: 12 شهراً")
- st.markdown("- قيمة الضمان الابتدائي: 2% من قيمة العطاء")
- st.markdown("- قيمة الضمان النهائي: 5% من قيمة العقد")
- st.markdown("- غرامة التأخير: 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%")
- st.markdown("- شروط الدفع: دفعات شهرية حسب نسبة الإنجاز")
-
- st.markdown("**المواصفات الفنية الرئيسية:**")
- st.markdown("- نوع الهيكل: خرساني مسلح")
- st.markdown("- نظام التكييف: نظام مركزي")
- st.markdown("- نظام الإنارة: LED موفر للطاقة")
- st.markdown("- نظام مكافحة الحريق: نظام رش آلي")
- st.markdown("- متطلبات خاصة: نظام طاقة شمسية لتوفير 30% من احتياجات الطاقة")
-
- if st.button("إرسال إلى وحدة المخاطر", key="send_specs_to_risk"):
- st.success("تم إرسال تحليل الشروط والمواصفات إلى وحدة المخاطر بنجاح!")
-
- def _render_cost_estimation_tab(self):
- """عرض تبويب تقدير التكاليف"""
-
- st.markdown("### تقدير التكاليف باستخدام الذكاء الاصطناعي")
-
- # إدخال معلومات المشروع
- st.markdown("#### معلومات المشروع")
-
- col1, col2 = st.columns(2)
-
- with col1:
- project_type = st.selectbox(
- "نوع المشروع",
- ["مبنى إداري", "مبنى سكني", "مدرسة", "مستشفى", "طرق", "جسور", "بنية تحتية", "أخرى"]
- )
-
- project_area = st.number_input("المساحة الإجمالية (م2)", min_value=0, value=5000)
-
- project_location = st.selectbox(
- "موقع المشروع",
- ["الرياض", "جدة", "الدمام", "مكة", "المدينة", "أبها", "تبوك", "أخرى"]
- )
-
- with col2:
- project_duration = st.number_input("مدة التنفيذ (شهر)", min_value=1, value=18)
-
- project_quality = st.select_slider(
- "مستوى الجودة",
- options=["اقتصادي", "متوسط", "عالي", "ممتاز"]
- )
-
- project_complexity = st.select_slider(
- "مستوى التعقيد",
- options=["بسيط", "متوسط", "معقد", "معقد جداً"]
- )
-
- # تقدير التكاليف
- if st.button("تقدير التكاليف"):
- # محاكاة تقدير التكاليف
- with st.spinner("جاري تقدير التكاليف..."):
- time.sleep(2) # محاكاة وقت المعالجة
- st.success("تم تقدير التكاليف بنجاح!")
-
- # عرض نتائج التقدير
- st.markdown("#### نتائج تقدير التكاليف")
-
- # تحديد التكلفة التقديرية بناءً على نوع المشروع والمساحة
- base_cost_per_sqm = {
- "مبنى إداري": 3500,
- "مبنى سكني": 3000,
- "مدرسة": 3200,
- "مستشفى": 5000,
- "طرق": 1500,
- "جسور": 8000,
- "بنية تحتية": 2500,
- "أخرى": 3000
- }
-
- # تعديل التكلفة بناءً على الموقع
- location_factor = {
- "الرياض": 1.0,
- "جدة": 1.05,
- "الدمام": 0.95,
- "مكة": 1.1,
- "المدينة": 1.0,
- "أبها": 0.9,
- "تبوك": 0.85,
- "أخرى": 1.0
- }
-
- # تعديل التكلفة بناءً على مستوى الجودة
- quality_factor = {
- "اقتصادي": 0.8,
- "متوسط": 1.0,
- "عالي": 1.2,
- "ممتاز": 1.5
- }
-
- # تعديل التكلفة بناءً على مستوى التعقيد
- complexity_factor = {
- "بسيط": 0.9,
- "متوسط": 1.0,
- "معقد": 1.2,
- "معقد جداً": 1.4
- }
-
- # حساب التكلفة التقديرية
- base_cost = base_cost_per_sqm[project_type] * project_area
- adjusted_cost = base_cost * location_factor[project_location] * quality_factor[project_quality] * complexity_factor[project_complexity]
-
- # عرض التكلفة التقديرية
- col1, col2 = st.columns(2)
-
- with col1:
- st.metric("التكلفة التقديرية", f"{adjusted_cost:,.0f} ريال")
-
- with col2:
- st.metric("التكلفة لكل متر مربع", f"{adjusted_cost / project_area:,.0f} ريال/م2")
-
- # عرض تفاصيل التكاليف
- st.markdown("#### تفاصيل التكاليف")
-
- # تقسيم التكاليف إلى فئات
- cost_breakdown = {
- "الأعمال الإنشائية": 0.35,
- "الأعمال المعمارية": 0.25,
- "الأعمال الكهربائية": 0.15,
- "الأعمال الميكانيكية": 0.15,
- "أعمال الموقع والتجهيزات": 0.10
- }
-
- cost_details = {
- "الفئة": list(cost_breakdown.keys()),
- "النسبة": [f"{v * 100:.0f}%" for v in cost_breakdown.values()],
- "التكلفة": [adjusted_cost * v for v in cost_breakdown.values()]
- }
-
- cost_df = pd.DataFrame(cost_details)
- st.dataframe(cost_df, use_container_width=True, hide_index=True)
-
- # عرض رسم بياني للتكاليف
- fig = px.pie(
- cost_df,
- values="التكلفة",
- names="الفئة",
- title="توزيع التكاليف حسب الفئة"
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض تحليل التكاليف المباشرة وغير المباشرة
- st.markdown("#### تحليل التكاليف المباشرة وغير المباشرة")
-
- direct_cost = adjusted_cost * 0.85
- indirect_cost = adjusted_cost * 0.15
-
- direct_indirect_data = {
- "نوع التكلفة": ["تكاليف مباشرة", "تكاليف غير مباشرة"],
- "النسبة": ["85%", "15%"],
- "التكلفة": [direct_cost, indirect_cost]
- }
-
- direct_indirect_df = pd.DataFrame(direct_indirect_data)
- st.dataframe(direct_indirect_df, use_container_width=True, hide_index=True)
-
- # عرض رسم بياني للتكاليف المباشرة وغير المباشرة
- fig = px.bar(
- direct_indirect_df,
- x="نوع التكلفة",
- y="التكلفة",
- title="التكاليف المباشرة وغير المباشرة",
- color="نوع التكلفة",
- text_auto='.2s'
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض توصيات لتحسين التكاليف
- st.markdown("#### توصيات لتحسين التكاليف")
-
- st.markdown("1. **تحسين تصميم المشروع:** يمكن تحسين التصميم لتقليل التكاليف مع الحفاظ على الجودة.")
- st.markdown("2. **استخدام مواد بديلة:** يمكن استخدام مواد بديلة بتكلفة أقل مع الحفاظ على الجودة.")
- st.markdown("3. **تحسين جدولة المشروع:** يمكن تحسين جدولة المشروع لتقليل مدة التنفيذ وبالتالي تقليل التكاليف غير المباشرة.")
- st.markdown("4. **تحسين إدارة الموارد:** يمكن تحسين إدارة الموارد لتقليل الهدر وزيادة الإنتاجية.")
- st.markdown("5. **التفاوض مع الموردين:** يمكن التفاوض مع الموردين للحصول على أسعار أفضل.")
-
- # إرسال التقدير إلى وحدة التسعير
- if st.button("إرسال إلى وحدة التسعير", key="send_estimate_to_pricing"):
- st.success("تم إرسال تقدير التكاليف إلى وحدة التسعير بنجاح!")
-
- # مقارنة التكاليف مع المشاريع السابقة
- st.markdown("#### مقارنة التكاليف مع المشاريع السابقة")
-
- # بيانات افتراضية للمشاريع السابقة
- previous_projects_data = {
- "المشروع": ["مبنى إداري - الرياض", "مبنى إداري - جدة", "مبنى إداري - الدمام", "مبنى سكني - الرياض", "مدرسة - جدة"],
- "المساحة (م2)": [4500, 5200, 4800, 6000, 3500],
- "التكلفة الإجمالية": [16200000, 19500000, 15800000, 18500000, 11200000],
- "التكلفة لكل متر مربع": [3600, 3750, 3290, 3080, 3200]
- }
-
- previous_projects_df = pd.DataFrame(previous_projects_data)
- st.dataframe(previous_projects_df, use_container_width=True, hide_index=True)
-
- # عرض رسم بياني لمقارنة التكاليف
- fig = px.bar(
- previous_projects_df,
- x="المشروع",
- y="التكلفة لكل متر مربع",
- title="مقارنة التكلفة لكل متر مربع للمشاريع السابقة",
- color="المشروع",
- text_auto='.0f'
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- 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"
- )
-
- project_budget = st.number_input("ميزانية المشروع (ريال)", min_value=0, value=15000000)
-
- project_location = st.selectbox(
- "موقع المشروع",
- ["الرياض", "جدة", "الدمام", "مكة", "المدينة", "أبها", "تبوك", "أخرى"],
- key="risk_project_location"
- )
-
- with col2:
- project_duration = st.number_input("مدة التنفيذ (شهر)", min_value=1, value=18, key="risk_project_duration")
-
- project_complexity = st.select_slider(
- "مستوى التعقيد",
- options=["بسيط", "متوسط", "معقد", "معقد جداً"],
- key="risk_project_complexity"
- )
-
- project_experience = st.select_slider(
- "مستوى الخبرة في هذا النوع من المشاريع",
- options=["منخفض", "متوسط", "عالي", "ممتاز"]
- )
-
- # تحليل المخاطر
- if st.button("تحليل المخاطر"):
- # محاكاة تحليل المخاطر
- with st.spinner("جاري تحليل المخاطر..."):
- time.sleep(2) # محاكاة وقت المعالجة
- st.success("تم تحليل المخاطر بنجاح!")
-
- # عرض نتائج التحليل
- st.markdown("#### نتائج تحليل المخاطر")
-
- # بيانات افتراضية للمخاطر
- risks_data = {
- "المخاطرة": [
- "تأخر التوريدات",
- "نقص العمالة الماهرة",
- "التغييرات في نطاق العمل",
- "الظروف الجوية غير المتوقعة",
- "مشاكل في التصميم",
- "تأخر الدفعات",
- "مشاكل في الموقع",
- "تغيير الأنظمة واللوائح",
- "مشاكل في الجودة",
- "مشاكل في التنسيق مع الجهات الحكومية"
- ],
- "الاحتمالية": [
- "متوسطة",
- "عالية",
- "متوسطة",
- "منخفضة",
- "منخفضة",
- "متوسطة",
- "منخفضة",
- "منخفضة",
- "متوسطة",
- "عالية"
- ],
- "التأثير": [
- "عالي",
- "عالي",
- "عالي",
- "متوسط",
- "عالي",
- "متوسط",
- "متوسط",
- "عالي",
- "عالي",
- "متوسط"
- ],
- "درجة المخاطرة": [
- "عالية",
- "عالية",
- "عالية",
- "متوسطة",
- "متوسطة",
- "متوسطة",
- "منخفضة",
- "متوسطة",
- "عالية",
- "عالية"
- ]
- }
-
- risks_df = pd.DataFrame(risks_data)
- st.dataframe(risks_df, use_container_width=True, hide_index=True)
-
- # عرض مصفوفة المخاطر
- st.markdown("#### مصفوفة المخاطر")
-
- # تحويل الاحتمالية والتأثير إلى قيم عددية
- probability_map = {"منخفضة": 1, "متوسطة": 2, "عالية": 3}
- impact_map = {"منخفض": 1, "متوسط": 2, "عالي": 3}
-
- risk_matrix_data = []
-
- for i, risk in enumerate(risks_data["المخاطرة"]):
- prob = probability_map[risks_data["الاحتمالية"][i]]
- impact = impact_map[risks_data["التأثير"][i]]
- risk_matrix_data.append({
- "المخاطرة": risk,
- "الاحتمالية": prob,
- "التأثير": impact,
- "درجة المخاطرة": prob * impact
- })
-
- # إنشاء مصفوفة المخاطر
- risk_matrix = np.zeros((3, 3))
-
- for risk in risk_matrix_data:
- prob = risk["الاحتمالية"] - 1 # تعديل الفهرس ليبدأ من 0
- impact = risk["التأثير"] - 1 # تعديل الفهرس ليبدأ من 0
- risk_matrix[prob, impact] += 1
-
- # عرض مصفوفة المخاطر كرسم بياني حراري
- fig, ax = plt.subplots(figsize=(10, 8))
-
- im = ax.imshow(risk_matrix, cmap="YlOrRd")
-
- # إضافة النص إلى الخلايا
- for i in range(3):
- for j in range(3):
- text = ax.text(j, i, int(risk_matrix[i, j]), ha="center", va="center", color="black")
-
- # إضافة العناوين
- ax.set_xticks(np.arange(3))
- ax.set_yticks(np.arange(3))
- ax.set_xticklabels(["منخفض", "متوسط", "عالي"])
- ax.set_yticklabels(["منخفضة", "متوسطة", "عالية"])
-
- # إضافة العناوين الرئيسية
- ax.set_xlabel("التأثير")
- ax.set_ylabel("الاحتمالية")
- ax.set_title("مصفوفة المخاطر")
-
- # إضافة شريط الألوان
- cbar = ax.figure.colorbar(im, ax=ax)
- cbar.ax.set_ylabel("عدد المخاطر", rotation=-90, va="bottom")
-
- # عرض الرسم البياني
- st.pyplot(fig)
-
- # عرض توزيع المخاطر حسب الدرجة
- st.markdown("#### توزيع المخاطر حسب الدرجة")
-
- risk_degree_counts = {
- "منخفضة": sum(1 for degree in risks_data["درجة المخاطرة"] if degree == "منخفضة"),
- "متوسطة": sum(1 for degree in risks_data["درجة المخاطرة"] if degree == "متوسطة"),
- "عالية": sum(1 for degree in risks_data["درجة المخاطرة"] if degree == "عالية")
- }
-
- risk_degree_df = pd.DataFrame({
- "درجة المخاطرة": list(risk_degree_counts.keys()),
- "العدد": list(risk_degree_counts.values())
- })
-
- fig = px.pie(
- risk_degree_df,
- values="العدد",
- names="درجة المخاطرة",
- title="توزيع المخاطر حسب الدرجة",
- color="درجة المخاطرة",
- color_discrete_map={"منخفضة": "green", "متوسطة": "orange", "عالية": "red"}
- )
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض خطة إدارة المخاطر
- st.markdown("#### خطة إدارة المخاطر")
-
- # بيانات افتراضية لخطة إدارة المخاطر
- risk_management_data = {
- "المخاطرة": [
- "تأخر التوريدات",
- "نقص العمالة الماهرة",
- "التغييرات في نطاق العمل",
- "مشاكل في الجودة",
- "مشاكل في التنسيق مع الجهات الحكومية"
- ],
- "استراتيجية المواجهة": [
- "تخفيف",
- "تخفيف",
- "تجنب",
- "تخفيف",
- "نقل"
- ],
- "الإجراءات": [
- "التعاقد مع موردين متعددين، وضع جدول زمني للتوريدات مع هامش أمان، متابعة التوريدات بشكل دوري",
- "التعاقد مع شركات توريد عمالة موثوقة، تدريب العمالة الحالية، وضع حوافز للعمالة الماهرة",
- "توثيق نطاق العمل بشكل دقيق، وضع إجراءات للتغييرات في نطاق العمل، تحديد صلاحيات اعتماد التغييرات",
- "وضع خطة لضبط الجودة، تعيين مسؤول للجودة، إجراء اختبارات دورية للجودة",
- "التعاقد مع استشاري متخصص في التنسيق مع الجهات الحكومية، تحديد متطلبات الجهات الحكومية مسبقاً"
- ],
- "المسؤول": [
- "مدير المشتريات",
- "مدير الموارد البشرية",
- "مدير المشروع",
- "مدير الجودة",
- "مدير العلاقات الحكومية"
- ],
- "الموعد النهائي": [
- "قبل بدء المشروع بشهر",
- "قبل بدء المشروع بشهرين",
- "قبل بدء المشروع بأسبوعين",
- "مستمر طوال فترة المشروع",
- "قبل بدء المشروع بشهر"
- ]
- }
-
- risk_management_df = pd.DataFrame(risk_management_data)
- st.dataframe(risk_management_df, use_container_width=True, hide_index=True)
-
- # عرض توصيات لإدارة المخاطر
- st.markdown("#### توصيات لإدارة المخاطر")
-
- st.markdown("1. **تخصيص احتياطي للطوارئ:** يوصى بتخصيص احتياطي للطوارئ بنسبة 10-15% من قيمة المشروع.")
- st.markdown("2. **مراجعة خطة إدارة المخاطر بشكل دوري:** يجب مراجعة خطة إدارة المخاطر بشكل دوري وتحديثها حسب الحاجة.")
- st.markdown("3. **تعيين مسؤول لإدارة المخاطر:** يوصى بتعيين مسؤول لإدارة المخاطر في المشروع.")
- st.markdown("4. **توثيق الدروس المستفادة:** يجب توثيق الدروس المستفادة من إدارة المخاطر في المشاريع السابقة.")
- st.markdown("5. **التواصل المستمر مع أصحاب المصلحة:** يجب التواصل المستمر مع أصحاب المصلحة لتحديد المخاطر المحتملة.")
-
- # إرسال تحليل المخاطر إلى وحدة التسعير
- if st.button("إرسال إلى وحدة التسعير", key="send_risk_to_pricing"):
- st.success("تم إرسال تحليل المخاطر إلى وحدة التسعير بنجاح!")
-
- def _render_ai_models_tab(self):
- """عرض تبويب نماذج الذكاء الاصطناعي"""
-
- st.markdown("### نماذج الذكاء الاصطناعي")
-
- # عرض نماذج الذكاء الاصطناعي
- st.markdown("#### قائمة نماذج الذكاء الاصطناعي")
-
- # تحويل قائمة النماذج إلى DataFrame
- models_df = pd.DataFrame(st.session_state.ai_models)
-
- # عرض النماذج كجدول
- st.dataframe(
- models_df,
- column_config={
- "id": st.column_config.NumberColumn("الرقم"),
- "name": st.column_config.TextColumn("اسم النموذج"),
- "description": st.column_config.TextColumn("الوصف"),
- "type": st.column_config.TextColumn("النوع"),
- "accuracy": st.column_config.ProgressColumn("الدقة (%)", min_value=0, max_value=100),
- "last_updated": st.column_config.DateColumn("تاريخ التحديث")
- },
- use_container_width=True,
- hide_index=True
- )
-
- # عرض تفاصيل النماذج
- st.markdown("#### تفاصيل النماذج")
-
- for model in st.session_state.ai_models:
- with st.expander(f"{model['name']} - دقة {model['accuracy']}%"):
- st.markdown(f"**الوصف:** {model['description']}")
- st.markdown(f"**النوع:** {model['type']}")
- st.markdown(f"**تاريخ التحديث:** {model['last_updated']}")
-
- if model['name'] == "نموذج تحليل المستندات":
- st.markdown("**القدرات:**")
- st.markdown("- استخراج المعلومات الرئيسية من مستندات المناقصات")
- st.markdown("- تحليل الشروط والمواصفات")
- st.markdown("- استخراج جداول الكميات")
- st.markdown("- تحديد الكيانات المهمة مثل الجهة المالكة، موقع المشروع، تواريخ المناقصة")
- st.markdown("- تلخيص المستندات الطويلة")
-
- elif model['name'] == "نموذج تقدير التكاليف":
- st.markdown("**القدرات:**")
- st.markdown("- تقدير تكاليف المشاريع بناءً على بيانات المشاريع السابقة")
- st.markdown("- تحليل العوامل المؤثرة على التكاليف")
- st.markdown("- تقديم توصيات لتحسين التكاليف")
- st.markdown("- مقارنة التكاليف مع المشاريع المماثلة")
- st.markdown("- تحليل التكاليف المباشرة وغير المباشرة")
-
- elif model['name'] == "نموذج تحليل المخاطر":
- st.markdown("**القدرات:**")
- st.markdown("- تحديد المخاطر المحتملة للمشاريع")
- st.markdown("- تقييم احتمالية وتأثير المخاطر")
- st.markdown("- إنشاء مصفوفة المخاطر")
- st.markdown("- تقديم توصيات لإدارة المخاطر")
- st.markdown("- تحليل المخاطر بناءً على بيانات المشاريع السابقة")
-
- elif model['name'] == "نموذج تحليل المنافسين":
- st.markdown("**القدرات:**")
- st.markdown("- تحليل بيانات المنافسين")
- st.markdown("- تحديد نقاط القوة والضعف للمنافسين")
- st.markdown("- تقديم توصيات للتسعير التنافسي")
- st.markdown("- تحليل استراتيجيات المنافسين")
- st.markdown("- تحليل حصص السوق")
-
- elif model['name'] == "نموذج المساعد الذكي":
- st.markdown("**القدرات:**")
- st.markdown("- الإجابة على الاستفسارات المتعلقة بإدارة المناقصات")
- st.markdown("- تقديم توصيات لتحسين إدارة المناقصات")
- st.markdown("- مساعدة المستخدمين في استخدام النظام")
- st.markdown("- تقديم معلومات عن المشاريع والمناقصات")
- st.markdown("- تقديم إحصائيات وتحليلات عن المناقصات")
-
- # عرض أداء النماذج
- st.markdown("#### أداء النماذج")
-
- # إنشاء رسم بياني لأداء النماذج
- performance_df = pd.DataFrame({
- "النموذج": [model['name'] for model in st.session_state.ai_models],
- "الدقة (%)": [model['accuracy'] for model in st.session_state.ai_models]
- })
-
- fig = px.bar(
- performance_df,
- x="النموذج",
- y="الدقة (%)",
- title="أداء نماذج الذكاء الاصطناعي",
- color="الدقة (%)",
- text_auto='.0f'
- )
-
- fig.update_layout(yaxis_range=[0, 100])
-
- st.plotly_chart(fig, use_container_width=True)
-
- # تدريب النماذج
- st.markdown("#### تدريب النماذج")
-
- col1, col2 = st.columns(2)
-
- with col1:
- model_to_train = st.selectbox(
- "اختر النموذج للتدريب",
- [model['name'] for model in st.session_state.ai_models]
- )
-
- with col2:
- training_data = st.file_uploader("قم بتحميل بيانات التدريب (CSV, XLSX)", type=["csv", "xlsx"])
-
- if st.button("تدريب النموذج"):
- # محاكاة تدريب النموذج
- with st.spinner(f"جاري تدريب {model_to_train}..."):
- time.sleep(3) # محاكاة وقت التدريب
- st.success(f"تم تدريب {model_to_train} بنجاح!")
-
- # تحديث دقة النموذج
- for i, model in enumerate(st.session_state.ai_models):
- if model['name'] == model_to_train:
- # زيادة الدقة بنسبة عشوائية بين 1% و 3%
- import random
- accuracy_increase = random.uniform(1, 3)
- new_accuracy = min(model['accuracy'] + accuracy_increase, 99)
- st.session_state.ai_models[i]['accuracy'] = new_accuracy
- st.session_state.ai_models[i]['last_updated'] = time.strftime("%Y-%m-%d")
-
- st.metric(
- "الدقة الجديدة",
- f"{new_accuracy:.1f}%",
- f"+{accuracy_increase:.1f}%"
- )
-
- break
-
- # تقييم النماذج
- st.markdown("#### تقييم النماذج")
-
- col1, col2 = st.columns(2)
-
- with col1:
- model_to_evaluate = st.selectbox(
- "اختر النموذج للتقييم",
- [model['name'] for model in st.session_state.ai_models],
- key="model_to_evaluate"
- )
-
- with col2:
- evaluation_data = st.file_uploader("قم بتحميل بيانات التقييم (CSV, XLSX)", type=["csv", "xlsx"], key="evaluation_data")
-
- if st.button("تقييم النموذج"):
- # محاكاة تقييم النموذج
- with st.spinner(f"جاري تقييم {model_to_evaluate}..."):
- time.sleep(2) # محاكاة وقت التقييم
- st.success(f"تم تقييم {model_to_evaluate} بنجاح!")
-
- # عرض نتائج التقييم
- for model in st.session_state.ai_models:
- if model['name'] == model_to_evaluate:
- accuracy = model['accuracy']
- break
-
- evaluation_metrics = {
- "المقياس": ["الدقة", "الاستدعاء", "F1", "AUC-ROC"],
- "القيمة": [accuracy / 100, (accuracy - 5) / 100, (accuracy - 3) / 100, (accuracy - 2) / 100]
- }
-
- evaluation_df = pd.DataFrame(evaluation_metrics)
- st.dataframe(evaluation_df, use_container_width=True, hide_index=True)
-
- # عرض مصفوفة الارتباك
- st.markdown("##### مصفوفة الارتباك")
-
- # إنشاء مصفوفة ارتباك افتراضية
- confusion_matrix = np.array([
- [85, 10, 5],
- [8, 80, 12],
- [7, 13, 80]
- ])
-
- fig, ax = plt.subplots(figsize=(10, 8))
-
- im = ax.imshow(confusion_matrix, cmap="Blues")
-
- # إضافة النص إلى الخلايا
- for i in range(3):
- for j in range(3):
- text = ax.text(j, i, confusion_matrix[i, j], ha="center", va="center", color="black")
-
- # إضافة العناوين
- ax.set_xticks(np.arange(3))
- ax.set_yticks(np.arange(3))
- ax.set_xticklabels(["الفئة 1", "الفئة 2", "الفئة 3"])
- ax.set_yticklabels(["الفئة 1", "الفئة 2", "الفئة 3"])
-
- # إضافة العناوين الرئيسية
- ax.set_xlabel("الفئة المتوقعة")
- ax.set_ylabel("الفئة الحقيقية")
- ax.set_title("مصفوفة الارتباك")
-
- # إضافة شريط الألوان
- cbar = ax.figure.colorbar(im, ax=ax)
- cbar.ax.set_ylabel("عدد العينات", rotation=-90, va="bottom")
-
- # عرض الرسم البياني
- st.pyplot(fig)
diff --git a/modules/ai_assistant/ai_assistant.py b/modules/ai_assistant/ai_assistant.py
deleted file mode 100644
index 9ac352b74035d2f4360a95e1fb1913eef4b154c7..0000000000000000000000000000000000000000
--- a/modules/ai_assistant/ai_assistant.py
+++ /dev/null
@@ -1,773 +0,0 @@
-#!/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
deleted file mode 100644
index 4f667ece6e9a58e33ef6805b933cd551aeb2d2be..0000000000000000000000000000000000000000
--- a/modules/ai_assistant/ai_assistant_app.py
+++ /dev/null
@@ -1,3175 +0,0 @@
-# -*- 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.py b/modules/ai_assistant/assistant.py
deleted file mode 100644
index 00df7a6113f2597ccfebd77eb38bb0b24e77eaf1..0000000000000000000000000000000000000000
--- a/modules/ai_assistant/assistant.py
+++ /dev/null
@@ -1,444 +0,0 @@
-"""
-وحدة المساعد الذكي لنظام إدارة المناقصات - Hybrid Face
-"""
-
-import os
-import logging
-import threading
-import datetime
-import json
-import re
-from pathlib import Path
-
-# تهيئة السجل
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger('ai_assistant')
-
-class AIAssistant:
- """المساعد الذكي"""
-
- def __init__(self, config=None, db=None):
- """تهيئة المساعد الذكي"""
- self.config = config
- self.db = db
- self.processing_in_progress = False
- self.current_query = None
- self.processing_results = {}
- self.conversation_history = []
-
- # إعدادات المساعد الذكي
- self.ai_model = config.AI_MODEL if config and hasattr(config, 'AI_MODEL') else "gpt-4"
- self.ai_temperature = config.AI_TEMPERATURE if config and hasattr(config, 'AI_TEMPERATURE') else 0.7
- self.ai_max_tokens = config.AI_MAX_TOKENS if config and hasattr(config, 'AI_MAX_TOKENS') else 2000
-
- # إنشاء مجلد المساعد الذكي إذا لم يكن موجوداً
- if config and hasattr(config, 'EXPORTS_PATH'):
- self.exports_path = Path(config.EXPORTS_PATH)
- else:
- self.exports_path = Path('data/exports')
-
- if not self.exports_path.exists():
- self.exports_path.mkdir(parents=True, exist_ok=True)
-
- def process_query(self, query, context=None, callback=None):
- """معالجة استعلام المستخدم"""
- if self.processing_in_progress:
- logger.warning("هناك عملية معالجة جارية بالفعل")
- return False
-
- self.processing_in_progress = True
- self.current_query = query
- self.processing_results = {
- "query": query,
- "context": context,
- "processing_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- "status": "جاري المعالجة",
- "response": "",
- "suggestions": [],
- "references": []
- }
-
- # إضافة الاستعلام إلى سجل المحادثة
- self.conversation_history.append({
- "role": "user",
- "content": query,
- "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- })
-
- # بدء المعالجة في خيط منفصل
- thread = threading.Thread(
- target=self._process_query_thread,
- args=(query, context, callback)
- )
- thread.daemon = True
- thread.start()
-
- return True
-
- def _process_query_thread(self, query, context, callback):
- """خيط معالجة الاستعلام"""
- try:
- # تحليل الاستعلام
- query_type = self._analyze_query(query)
-
- # معالجة الاستعلام بناءً على نوعه
- if query_type == "document_analysis":
- response = self._handle_document_analysis_query(query, context)
- elif query_type == "pricing":
- response = self._handle_pricing_query(query, context)
- elif query_type == "risk_analysis":
- response = self._handle_risk_analysis_query(query, context)
- elif query_type == "project_management":
- response = self._handle_project_management_query(query, context)
- elif query_type == "reporting":
- response = self._handle_reporting_query(query, context)
- else:
- response = self._handle_general_query(query, context)
-
- # توليد اقتراحات
- suggestions = self._generate_suggestions(query_type, query, response)
-
- # تحديث نتائج المعالجة
- self.processing_results["response"] = response
- self.processing_results["query_type"] = query_type
- self.processing_results["suggestions"] = suggestions
- self.processing_results["status"] = "اكتملت المعالجة"
- self.processing_results["processing_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
-
- # إضافة الاستجابة إلى سجل المحادثة
- self.conversation_history.append({
- "role": "assistant",
- "content": response,
- "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- })
-
- logger.info(f"اكتملت معالجة الاستعلام: {query[:50]}...")
-
- except Exception as e:
- logger.error(f"خطأ في معالجة الاستعلام: {str(e)}")
- self.processing_results["status"] = "فشلت المعالجة"
- self.processing_results["error"] = str(e)
-
- # إضافة رسالة الخطأ إلى سجل المحادثة
- self.conversation_history.append({
- "role": "system",
- "content": f"حدث خطأ أثناء معالجة الاستعلام: {str(e)}",
- "timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- })
-
- finally:
- self.processing_in_progress = False
-
- # استدعاء دالة الاستجابة إذا تم توفيرها
- if callback and callable(callback):
- callback(self.processing_results)
-
- def _analyze_query(self, query):
- """تحليل نوع الاستعلام"""
- query = query.lower()
-
- # تحديد نوع الاستعلام بناءً على الكلمات المفتاحية
- if any(keyword in query for keyword in ["تحليل المستند", "تحليل وثيقة", "استخراج بيانات", "قراءة مستند"]):
- return "document_analysis"
- elif any(keyword in query for keyword in ["تسعير", "سعر", "تكلفة", "ميزانية", "تقدير"]):
- return "pricing"
- elif any(keyword in query for keyword in ["مخاطر", "تحليل المخاطر", "تقييم المخاطر"]):
- return "risk_analysis"
- elif any(keyword in query for keyword in ["مشروع", "إدارة المشروع", "جدول زمني", "خطة"]):
- return "project_management"
- elif any(keyword in query for keyword in ["تقرير", "إحصائيات", "تحليل البيانات", "رسم بياني"]):
- return "reporting"
- else:
- return "general"
-
- def _handle_document_analysis_query(self, query, context):
- """معالجة استعلام تحليل المستندات"""
- # محاكاة استجابة المساعد الذكي لاستعلام تحليل المستندات
- response = """
-يمكنني مساعدتك في تحليل المستندات واستخراج المعلومات المهمة منها. لتحليل مستند، يرجى اتباع الخطوات التالية:
-
-1. انتقل إلى وحدة "تحليل المستندات" من القائمة الجانبية.
-2. انقر على زر "تحميل مستند" واختر المستند المراد تحليله.
-3. حدد نوع المستند (مناقصة، عقد، مواصفات فنية، إلخ).
-4. انقر على زر "تحليل" لبدء عملية التحليل.
-
-سيقوم النظام باستخراج المعلومات التالية من المستند:
-- البنود والكميات
-- الكيانات (العميل، الموقع، المقاول، إلخ)
-- التواريخ المهمة
-- المبالغ والتكاليف
-- المخاطر المحتملة
-
-بعد اكتمال التحليل، يمكنك مراجعة النتائج وتعديلها إذا لزم الأمر، ثم استخدامها في وحدات النظام الأخرى مثل التسعير وتحليل المخاطر.
-"""
-
- # إضافة مراجع ذات صلة
- self.processing_results["references"] = [
- {"title": "دليل استخدام وحدة تحليل المستندات", "type": "manual"},
- {"title": "أنواع المستندات المدعومة", "type": "documentation"},
- {"title": "تقنيات استخراج البيانات من المستندات", "type": "article"}
- ]
-
- return response
-
- def _handle_pricing_query(self, query, context):
- """معالجة استعلام التسعير"""
- # محاكاة استجابة المساعد الذكي لاستعلام التسعير
- response = """
-يمكنني مساعدتك في تسعير المشاريع وتقدير التكاليف. لإنشاء تسعير لمشروع، يرجى اتباع الخطوات التالية:
-
-1. انتقل إلى وحدة "التسعير المتكامل" من القائمة الجانبية.
-2. اختر المشروع المراد تسعيره أو أنشئ مشروعاً جديداً.
-3. أدخل بنود المشروع والكميات التقديرية (يمكن استيرادها من نتائج تحليل المستندات).
-4. حدد الموارد المطلوبة (مواد، معدات، عمالة).
-5. اختر استراتيجية التسعير المناسبة:
- - شاملة: تغطية كاملة للتكاليف والمخاطر مع هامش ربح مناسب.
- - تنافسية: تخفيض الهوامش لتقديم سعر تنافسي.
- - متوازنة: توازن بين الربحية والتنافسية.
-6. انقر على زر "حساب التسعير" لإنشاء التسعير.
-
-سيقوم النظام بحساب:
-- التكاليف المباشرة (بنود المشروع)
-- التكاليف غير المباشرة (نفقات عامة، إدارية، ربح)
-- تكاليف المخاطر
-- ضريبة القيمة المضافة
-- السعر النهائي
-
-يمكنك تعديل المعلمات وإعادة حساب التسعير، ثم تصدير النتائج إلى تقرير مفصل.
-"""
-
- # إضافة مراجع ذات صلة
- self.processing_results["references"] = [
- {"title": "دليل استخدام وحدة التسعير المتكامل", "type": "manual"},
- {"title": "استراتيجيات التسعير", "type": "documentation"},
- {"title": "حساب التكاليف غير المباشرة", "type": "article"}
- ]
-
- return response
-
- def _handle_risk_analysis_query(self, query, context):
- """معالجة استعلام تحليل المخاطر"""
- # محاكاة استجابة المساعد الذكي لاستعلام تحليل المخاطر
- response = """
-يمكنني مساعدتك في تحليل وإدارة مخاطر المشروع. لإجراء تحليل للمخاطر، يرجى اتباع الخطوات التالية:
-
-1. انتقل إلى وحدة "تحليل المخاطر" من القائمة الجانبية.
-2. اختر المشروع المراد تحليل مخاطره.
-3. اختر طريقة التحليل:
- - شاملة: تحليل مفصل يغطي جميع جوانب المشروع.
- - أساسية: تحليل سريع للمخاطر الرئيسية.
-4. انقر على زر "تحليل المخاطر" لبدء التحليل.
-
-سيقوم النظام بما يلي:
-- تحديد المخاطر المحتملة بناءً على بيانات المشروع
-- تصنيف المخاطر إلى فئات (فني، مالي، إداري، إلخ)
-- إنشاء مصفوفة المخاطر (الاحتمالية × التأثير)
-- تطوير استراتيجيات التخفيف لكل مخاطرة
-- إنشاء ملخص للمخاطر وتوصيات
-
-يمكنك مراجعة نتائج التحليل وتعديلها، ثم تصدير التقرير النهائي واستخدامه في خطة إدارة المشروع.
-"""
-
- # إضافة مراجع ذات صلة
- self.processing_results["references"] = [
- {"title": "دليل استخدام وحدة تحليل المخاطر", "type": "manual"},
- {"title": "منهجيات تحليل المخاطر", "type": "documentation"},
- {"title": "استراتيجيات التخفيف من المخاطر", "type": "article"}
- ]
-
- return response
-
- def _handle_project_management_query(self, query, context):
- """معالجة استعلام إدارة المشاريع"""
- # محاكاة استجابة المساعد الذكي لاستعلام إدارة المشاريع
- response = """
-يمكنني مساعدتك في إدارة المشاريع وتتبع تقدمها. لإدارة مشروع، يرجى اتباع الخطوات التالية:
-
-1. انتقل إلى وحدة "إدارة المشاريع" من القائمة الجانبية.
-2. أنشئ مشروعاً جديداً أو اختر مشروعاً موجوداً.
-3. أدخل معلومات المشروع الأساسية (الاسم، العميل، الوصف، التواريخ).
-4. أضف بنود المشروع (يمكن استيرادها من نتائج تحليل المستندات).
-5. أنشئ الجدول الزمني للمشروع وحدد المراحل والمهام.
-6. عين الموارد للمهام وحدد التبعيات بينها.
-
-يمكنك استخدام وحدة إدارة المشاريع لـ:
-- تتبع تقدم المشروع ومقارنته بالخطة
-- إدارة الموارد وتوزيعها
-- متابعة المشكلات والمخاطر
-- إدارة التغييرات في نطاق العمل
-- إنشاء تقارير حالة المشروع
-
-كما يمكنك دمج نتائج التسعير وتحليل المخاطر في خطة المشروع لإدارة شاملة.
-"""
-
- # إضافة مراجع ذات صلة
- self.processing_results["references"] = [
- {"title": "دليل استخدام وحدة إدارة المشاريع", "type": "manual"},
- {"title": "أفضل ممارسات إدارة المشاريع", "type": "documentation"},
- {"title": "إنشاء جداول زمنية فعالة", "type": "article"}
- ]
-
- return response
-
- def _handle_reporting_query(self, query, context):
- """معالجة استعلام التقارير"""
- # محاكاة استجابة المساعد الذكي لاستعلام التقارير
- response = """
-يمكنني مساعدتك في إنشاء تقارير وتحليلات للمشاريع والمناقصات. لإنشاء تقرير، يرجى اتباع الخطوات التالية:
-
-1. انتقل إلى وحدة "التقارير والتحليلات" من القائمة الجانبية.
-2. اختر نوع التقرير:
- - تقرير المشروع: معلومات شاملة عن مشروع محدد
- - تقرير التسعير: تفاصيل تسعير مشروع أو مناقصة
- - تقرير المخاطر: تحليل مخاطر المشروع واستراتيجيات التخفيف
- - تقرير الأداء: مقارنة الأداء الفعلي بالمخطط
- - تقرير مالي: تحليل مالي للمشاريع والمناقصات
-3. حدد معلمات التقرير (المشروع، الفترة الزمنية، إلخ).
-4. انقر على زر "إنشاء التقرير".
-
-يمكنك تخصيص التقارير بإضافة أو إزالة أقسام، وتغيير طريقة عرض البيانات (جداول، رسوم بيانية، إلخ).
-
-التقارير المنشأة يمكن:
-- تصديرها بتنسيقات مختلفة (PDF، Excel، Word)
-- مشاركتها مع أعضاء الفريق أو العملاء
-- جدولتها للإنشاء التلقائي بشكل دوري
-- حفظها كقوالب لاستخدامها في المستقبل
-"""
-
- # إضافة مراجع ذات صلة
- self.processing_results["references"] = [
- {"title": "دليل استخدام وحدة التقارير والتحليلات", "type": "manual"},
- {"title": "أنواع التقارير المتاحة", "type": "documentation"},
- {"title": "إنشاء رسوم بيانية فعالة", "type": "article"}
- ]
-
- return response
-
- def _handle_general_query(self, query, context):
- """معالجة استعلام عام"""
- # محاكاة استجابة المساعد الذكي لاستعلام عام
- response = """
-مرحباً بك في المساعد الذكي لنظام إدارة المناقصات Hybrid Face. يمكنني مساعدتك في مجموعة متنوعة من المهام المتعلقة بإدارة المناقصات والمشاريع.
-
-يمكنني مساعدتك في:
-- تحليل مستندات المناقصات واستخراج المعلومات المهمة منها
-- تسعير المشاريع وتقدير التكاليف
-- تحليل وإدارة مخاطر المشاريع
-- إدارة المشاريع وتتبع تقدمها
-- إنشاء تقارير وتحليلات
-
-للحصول على مساعدة محددة، يرجى طرح سؤال يتعلق بإحدى هذه المجالات. على سبيل المثال:
-- "كيف يمكنني تحليل مستند مناقصة؟"
-- "ساعدني في تسعير مشروع جديد"
-- "كيف أقوم بتحليل مخاطر المشروع؟"
-- "أريد إنشاء تقرير عن حالة المشروع"
-
-يمكنك أيضاً استخدام الوحدات المختلفة في النظام مباشرة من القائمة الجانبية.
-"""
-
- # إضافة مراجع ذات صلة
- self.processing_results["references"] = [
- {"title": "دليل المستخدم الشامل", "type": "manual"},
- {"title": "نظرة عامة على النظام", "type": "documentation"},
- {"title": "الأسئلة الشائعة", "type": "faq"}
- ]
-
- return response
-
- def _generate_suggestions(self, query_type, query, response):
- """توليد اقتراحات للمستخدم بناءً على الاستعلام والاستجابة"""
- suggestions = []
-
- if query_type == "document_analysis":
- suggestions = [
- "كيف يمكنني استيراد نتائج تحليل المستندات إلى وحدة التسعير؟",
- "ما هي أنواع المستندات المدعومة للتحليل؟",
- "كيف يمكنني تحسين دقة تحليل المستندات؟"
- ]
- elif query_type == "pricing":
- suggestions = [
- "ما هي استراتيجية التسعير المناسبة لمشروعي؟",
- "كيف يمكنني حساب التكاليف غير المباشرة؟",
- "كيف أضيف تكاليف المخاطر إلى التسعير؟"
- ]
- elif query_type == "risk_analysis":
- suggestions = [
- "ما هي أفضل استراتيجيات التخفيف من المخاطر؟",
- "كيف يمكنني تحديد المخاطر الحرجة في المشروع؟",
- "كيف أدمج تحليل المخاطر في خطة المشروع؟"
- ]
- elif query_type == "project_management":
- suggestions = [
- "كيف أنشئ جدولاً زمنياً فعالاً للمشروع؟",
- "كيف أتتبع تقدم المشروع مقارنة بالخطة؟",
- "كيف أدير التغييرات في نطاق المشروع؟"
- ]
- elif query_type == "reporting":
- suggestions = [
- "ما هي أنواع التقارير المتاحة في النظام؟",
- "كيف يمكنني تخصيص تقرير المشروع؟",
- "كيف أقوم بجدولة إنشاء تقارير دورية؟"
- ]
- else:
- suggestions = [
- "كيف يمكنني البدء باستخدام النظام؟",
- "ما هي الوحدات المتاحة في النظام؟",
- "كيف يمكنني إنشاء مشروع جديد؟"
- ]
-
- return suggestions
-
- def get_processing_status(self):
- """الحصول على حالة المعالجة الحالية"""
- if not self.processing_in_progress:
- if not self.processing_results:
- return {"status": "لا توجد معالجة جارية"}
- else:
- return {"status": self.processing_results.get("status", "غير معروف")}
-
- return {
- "status": "جاري المعالجة",
- "query": self.current_query,
- "start_time": self.processing_results.get("processing_start_time")
- }
-
- def get_processing_results(self):
- """الحصول على نتائج المعالجة"""
- return self.processing_results
-
- def get_conversation_history(self, limit=10):
- """الحصول على سجل المحادثة"""
- if limit and limit > 0:
- return self.conversation_history[-limit:]
- return self.conversation_history
-
- def clear_conversation_history(self):
- """مسح سجل المحادثة"""
- self.conversation_history = []
- return True
-
- def export_conversation_history(self, output_path=None):
- """تصدير سجل المحادثة إلى ملف JSON"""
- if not self.conversation_history:
- logger.warning("لا يوجد سجل محادثة للتصدير")
- return None
-
- if not output_path:
- # إنشاء اسم ملف افتراضي
- timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
- filename = f"conversation_history_{timestamp}.json"
- output_path = os.path.join(self.exports_path, filename)
-
- try:
- with open(output_path, 'w', encoding='utf-8') as f:
- json.dump(self.conversation_history, f, ensure_ascii=False, indent=4)
-
- logger.info(f"تم تصدير سجل المحادثة إلى: {output_path}")
- return output_path
-
- except Exception as e:
- logger.error(f"خطأ في تصدير سجل المحادثة: {str(e)}")
- return None
diff --git a/modules/ai_assistant/assistant_app.py b/modules/ai_assistant/assistant_app.py
deleted file mode 100644
index 8306a78f6923fe730ef0c096c1e95c238e59ebe9..0000000000000000000000000000000000000000
--- a/modules/ai_assistant/assistant_app.py
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/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
deleted file mode 100644
index 070419ec30c2df5c5ba01ff2fa55212770dd1391..0000000000000000000000000000000000000000
--- a/modules/ai_finetuning/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# ملف تهيئة حزمة ضبط نماذج الذكاء الاصطناعي
\ No newline at end of file
diff --git a/modules/ai_finetuning/finetuning_app.py b/modules/ai_finetuning/finetuning_app.py
deleted file mode 100644
index a1dc1a7b8e4982246bd7567707b37e0b762a5b74..0000000000000000000000000000000000000000
--- a/modules/ai_finetuning/finetuning_app.py
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/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
deleted file mode 100644
index 464b6c2455ca7b925a2920a3fab386449e407243..0000000000000000000000000000000000000000
--- a/modules/ai_finetuning/model_finetuning.py
+++ /dev/null
@@ -1,2081 +0,0 @@
-#!/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/document_comparison/document_comparison_app.py b/modules/document_comparison/document_comparison_app.py
deleted file mode 100644
index 9921479e4b89392338e1c3486f5dd5e1f99a6a24..0000000000000000000000000000000000000000
--- a/modules/document_comparison/document_comparison_app.py
+++ /dev/null
@@ -1,1003 +0,0 @@
-"""
-وحدة مقارنة المستندات - نظام تحليل المناقصات
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-import os
-import sys
-from pathlib import Path
-import difflib
-import re
-import datetime
-
-# إضافة مسار المشروع للنظام
-sys.path.append(str(Path(__file__).parent.parent))
-
-# استيراد محسن واجهة المستخدم
-from styling.enhanced_ui import UIEnhancer
-
-class DocumentComparisonApp:
- """تطبيق مقارنة المستندات"""
-
- def __init__(self):
- """تهيئة تطبيق مقارنة المستندات"""
- self.ui = UIEnhancer(page_title="مقارنة المستندات - نظام تحليل المناقصات", page_icon="📄")
- self.ui.apply_theme_colors()
-
- # بيانات المستندات (نموذجية)
- self.documents_data = [
- {
- "id": "DOC001",
- "name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
- "type": "كراسة شروط",
- "version": "1.0",
- "date": "2025-01-15",
- "size": 2.4,
- "pages": 45,
- "related_entity": "T-2025-001",
- "path": "/documents/T-2025-001/specs_v1.pdf"
- },
- {
- "id": "DOC002",
- "name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
- "type": "كراسة شروط",
- "version": "1.1",
- "date": "2025-02-10",
- "size": 2.6,
- "pages": 48,
- "related_entity": "T-2025-001",
- "path": "/documents/T-2025-001/specs_v1.1.pdf"
- },
- {
- "id": "DOC003",
- "name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
- "type": "كراسة شروط",
- "version": "2.0",
- "date": "2025-03-05",
- "size": 2.8,
- "pages": 52,
- "related_entity": "T-2025-001",
- "path": "/documents/T-2025-001/specs_v2.0.pdf"
- },
- {
- "id": "DOC004",
- "name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
- "type": "جدول كميات",
- "version": "1.0",
- "date": "2025-01-15",
- "size": 1.2,
- "pages": 20,
- "related_entity": "T-2025-001",
- "path": "/documents/T-2025-001/boq_v1.0.xlsx"
- },
- {
- "id": "DOC005",
- "name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
- "type": "جدول كميات",
- "version": "1.1",
- "date": "2025-02-20",
- "size": 1.3,
- "pages": 22,
- "related_entity": "T-2025-001",
- "path": "/documents/T-2025-001/boq_v1.1.xlsx"
- },
- {
- "id": "DOC006",
- "name": "المخططات - مناقصة إنشاء مبنى إداري",
- "type": "مخططات",
- "version": "1.0",
- "date": "2025-01-15",
- "size": 15.6,
- "pages": 30,
- "related_entity": "T-2025-001",
- "path": "/documents/T-2025-001/drawings_v1.0.pdf"
- },
- {
- "id": "DOC007",
- "name": "المخططات - مناقصة إنشاء مبنى إداري",
- "type": "مخططات",
- "version": "2.0",
- "date": "2025-03-10",
- "size": 18.2,
- "pages": 35,
- "related_entity": "T-2025-001",
- "path": "/documents/T-2025-001/drawings_v2.0.pdf"
- },
- {
- "id": "DOC008",
- "name": "كراسة الشروط - مناقصة صيانة طرق",
- "type": "كراسة شروط",
- "version": "1.0",
- "date": "2025-02-05",
- "size": 1.8,
- "pages": 32,
- "related_entity": "T-2025-002",
- "path": "/documents/T-2025-002/specs_v1.0.pdf"
- },
- {
- "id": "DOC009",
- "name": "كراسة الشروط - مناقصة صيانة طرق",
- "type": "كراسة شروط",
- "version": "1.1",
- "date": "2025-03-15",
- "size": 1.9,
- "pages": 34,
- "related_entity": "T-2025-002",
- "path": "/documents/T-2025-002/specs_v1.1.pdf"
- },
- {
- "id": "DOC010",
- "name": "جدول الكميات - مناقصة صيانة طرق",
- "type": "جدول كميات",
- "version": "1.0",
- "date": "2025-02-05",
- "size": 0.9,
- "pages": 15,
- "related_entity": "T-2025-002",
- "path": "/documents/T-2025-002/boq_v1.0.xlsx"
- }
- ]
-
- # بيانات نموذجية لمحتوى المستندات (للعرض فقط)
- self.sample_document_content = {
- "DOC001": """
- # كراسة الشروط والمواصفات
- ## مناقصة إنشاء مبنى إداري
-
- ### 1. مقدمة
- تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
-
- ### 2. نطاق العمل
- يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع، ويشمل ذلك:
- - أعمال الهيكل الإنشائي
- - أعمال التشطيبات الداخلية والخارجية
- - أعمال الكهرباء والميكانيكا
- - أعمال تنسيق الموقع
-
- ### 3. المواصفات الفنية
- #### 3.1 أعمال الخرسانة
- - يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 30 نيوتن/مم²
- - يجب استخدام حديد تسليح مطابق للمواصفات السعودية
-
- #### 3.2 أعمال التشطيبات
- - يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
- - يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
-
- ### 4. الشروط العامة
- - مدة التنفيذ: 18 شهراً من تاريخ استلام الموقع
- - غرامة التأخير: 0.1% من قيمة العقد عن كل يوم تأخير
- - ضمان الأعمال: 10 سنوات للهيكل الإنشائي، 5 سنوات للأعمال الميكانيكية والكهربائية
- """,
-
- "DOC002": """
- # كراسة الشروط والمواصفات
- ## مناقصة إنشاء مبنى إداري
-
- ### 1. مقدمة
- تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
-
- ### 2. نطاق العمل
- يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5500 متر مربع، ويشمل ذلك:
- - أعمال الهيكل الإنشائي
- - أعمال التشطيبات الداخلية والخارجية
- - أعمال الكهرباء والميكانيكا
- - أعمال تنسيق الموقع
- - أعمال أنظمة الأمن والسلامة
-
- ### 3. المواصفات الفنية
- #### 3.1 أعمال الخرسانة
- - يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 35 نيوتن/مم²
- - يجب استخدام حديد تسليح مطابق للمواصفات السعودية
-
- #### 3.2 أعمال التشطيبات
- - يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
- - يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
- - يجب استخدام زجاج عاكس للحرارة للواجهات
-
- ### 4. الشروط العامة
- - مدة التنفيذ: 16 شهراً من تاريخ استلام الموقع
- - غرامة التأخير: 0.15% من قيمة العقد عن كل يوم تأخير
- - ضمان الأعمال: 10 سنوات للهيكل الإنشائي، 5 سنوات للأعمال الميكانيكية والكهربائية
- """,
-
- "DOC003": """
- # كراسة الشروط والمواصفات
- ## مناقصة إنشاء مبنى إداري
-
- ### 1. مقدمة
- تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض وفقاً للمواصفات المعتمدة من الهيئة السعودية للمواصفات والمقاييس.
-
- ### 2. نطاق العمل
- يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 6 طوابق بمساحة إجمالية 6000 متر مربع، ويشمل ذلك:
- - أعمال الهيكل الإنشائي
- - أعمال التشطيبات الداخلية والخارجية
- - أعمال الكهرباء والميكانيكا
- - أعمال تنسيق الموقع
- - أعمال أنظمة الأمن والسلامة
- - أعمال أنظمة المباني الذكية
-
- ### 3. المواصفات الفنية
- #### 3.1 أعمال الخرسانة
- - يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 40 نيوتن/مم²
- - يجب استخدام حديد تسليح مطابق للمواصفات السعودية
- - يجب استخدام إضافات للخرسانة لزيادة مقاومتها للعوامل الجوية
-
- #### 3.2 أعمال التشطيبات
- - يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
- - يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
- - يجب استخدام زجاج عاكس للحرارة للواجهات
- - يجب استخدام مواد صديقة للبيئة
-
- ### 4. الشروط العامة
- - مدة التنفيذ: 15 شهراً من تاريخ استلام الموقع
- - غرامة التأخير: 0.2% من قيمة العقد عن كل يوم تأخير
- - ضمان الأعمال: 15 سنوات للهيكل الإنشائي، 7 سنوات للأعمال الميكانيكية والكهربائية
-
- ### 5. متطلبات الاستدامة
- - يجب أن يحقق المبنى متطلبات الاستدامة وفقاً لمعايير LEED
- - يجب توفير أنظمة لترشيد استهلاك الطاقة والمياه
- """
- }
-
- def run(self):
- """تشغيل تطبيق مقارنة المستندات"""
- # إنشاء قائمة العناصر
- menu_items = [
- {"name": "لوحة المعلومات", "icon": "house"},
- {"name": "المناقصات والعقود", "icon": "file-text"},
- {"name": "تحليل المستندات", "icon": "file-earmark-text"},
- {"name": "نظام التسعير", "icon": "calculator"},
- {"name": "حاسبة تكاليف البناء", "icon": "building"},
- {"name": "الموارد والتكاليف", "icon": "people"},
- {"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
- {"name": "إدارة المشاريع", "icon": "kanban"},
- {"name": "الخرائط والمواقع", "icon": "geo-alt"},
- {"name": "الجدول الزمني", "icon": "calendar3"},
- {"name": "الإشعارات", "icon": "bell"},
- {"name": "مقارنة المستندات", "icon": "files"},
- {"name": "المساعد الذكي", "icon": "robot"},
- {"name": "التقارير", "icon": "bar-chart"},
- {"name": "الإعدادات", "icon": "gear"}
- ]
-
- # إنشاء الشريط الجانبي
- selected = self.ui.create_sidebar(menu_items)
-
- # إنشاء ترويسة الصفحة
- self.ui.create_header("مقارنة المستندات", "أدوات متقدمة لمقارنة وتحليل المستندات")
-
- # إنشاء علامات تبويب للوظائف المختلفة
- tabs = st.tabs(["مقارنة الإصدارات", "مقارنة المستندات", "تحليل التغييرات", "سجل التغييرات"])
-
- # علامة تبويب مقارنة الإصدارات
- with tabs[0]:
- self.compare_versions()
-
- # علامة تبويب مقارنة المستندات
- with tabs[1]:
- self.compare_documents()
-
- # علامة تبويب تحليل التغييرات
- with tabs[2]:
- self.analyze_changes()
-
- # علامة تبويب سجل التغييرات
- with tabs[3]:
- self.show_change_history()
-
- def compare_versions(self):
- """مقارنة إصدارات المستندات"""
- st.markdown("### مقارنة إصدارات المستندات")
-
- # اختيار المناقصة
- tender_options = list(set([doc["related_entity"] for doc in self.documents_data]))
- selected_tender = st.selectbox(
- "اختر المناقصة",
- options=tender_options
- )
-
- # فلترة المستندات حسب المناقصة المختارة
- filtered_docs = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender]
-
- # اختيار نوع المستند
- doc_types = list(set([doc["type"] for doc in filtered_docs]))
- selected_type = st.selectbox(
- "اختر نوع المستند",
- options=doc_types
- )
-
- # فلترة المستندات حسب النوع المختار
- type_filtered_docs = [doc for doc in filtered_docs if doc["type"] == selected_type]
-
- # ترتيب المستندات حسب الإصدار
- type_filtered_docs = sorted(type_filtered_docs, key=lambda x: x["version"])
-
- if len(type_filtered_docs) < 2:
- st.warning("يجب توفر إصدارين على الأقل للمقارنة")
- else:
- # اختيار الإصدارات للمقارنة
- col1, col2 = st.columns(2)
-
- with col1:
- version_options = [f"{doc['name']} (الإصدار {doc['version']})" for doc in type_filtered_docs]
- selected_version1_index = st.selectbox(
- "الإصدار الأول",
- options=range(len(version_options)),
- format_func=lambda x: version_options[x]
- )
- selected_doc1 = type_filtered_docs[selected_version1_index]
-
- with col2:
- remaining_indices = [i for i in range(len(type_filtered_docs)) if i != selected_version1_index]
- selected_version2_index = st.selectbox(
- "الإصدار الثاني",
- options=remaining_indices,
- format_func=lambda x: version_options[x]
- )
- selected_doc2 = type_filtered_docs[selected_version2_index]
-
- # زر بدء المقارنة
- if st.button("بدء المقارنة", use_container_width=True):
- # عرض معلومات المستندات المختارة
- st.markdown("### معلومات المستندات المختارة")
-
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown(f"**الإصدار الأول:** {selected_doc1['version']}")
- st.markdown(f"**التاريخ:** {selected_doc1['date']}")
- st.markdown(f"**عدد الصفحات:** {selected_doc1['pages']}")
- st.markdown(f"**الحجم:** {selected_doc1['size']} ميجابايت")
-
- with col2:
- st.markdown(f"**الإصدار الثاني:** {selected_doc2['version']}")
- st.markdown(f"**التاريخ:** {selected_doc2['date']}")
- st.markdown(f"**عدد الصفحات:** {selected_doc2['pages']}")
- st.markdown(f"**الحجم:** {selected_doc2['size']} ميجابايت")
-
- # الحصول على محتوى المستندات (في تطبيق حقيقي، سيتم استرجاع المحتوى من الملفات الفعلية)
- doc1_content = self.sample_document_content.get(selected_doc1["id"], "محتوى المستند غير متوفر")
- doc2_content = self.sample_document_content.get(selected_doc2["id"], "محتوى المستند غير متوفر")
-
- # إجراء المقارنة
- self.display_comparison(doc1_content, doc2_content)
-
- def display_comparison(self, text1, text2):
- """عرض نتائج المقارنة بين نصين"""
- st.markdown("### نتائج المقارنة")
-
- # تقسيم النصوص إلى أسطر
- lines1 = text1.splitlines()
- lines2 = text2.splitlines()
-
- # إجراء المقارنة باستخدام difflib
- d = difflib.Differ()
- diff = list(d.compare(lines1, lines2))
-
- # عرض ملخص التغييرات
- added = len([line for line in diff if line.startswith('+ ')])
- removed = len([line for line in diff if line.startswith('- ')])
- changed = len([line for line in diff if line.startswith('? ')])
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- self.ui.create_metric_card(
- "الإضافات",
- str(added),
- None,
- self.ui.COLORS['success']
- )
-
- with col2:
- self.ui.create_metric_card(
- "الحذف",
- str(removed),
- None,
- self.ui.COLORS['danger']
- )
-
- with col3:
- self.ui.create_metric_card(
- "التغييرات",
- str(changed // 2), # تقسيم على 2 لأن كل تغيير يظهر مرتين
- None,
- self.ui.COLORS['warning']
- )
-
- # عرض التغييرات بالتفصيل
- st.markdown("### التغييرات بالتفصيل")
-
- # إنشاء عرض HTML للتغييرات
- html_diff = []
- for line in diff:
- if line.startswith('+ '):
- html_diff.append(f'
يمكنك إضافة مواقع جديدة أو تحديث المواقع الموجودة.
-
- """, unsafe_allow_html=True)
-
- # تبويبات إدارة المواقع
- management_tabs = st.tabs(["إضافة موقع جديد", "تحرير المواقع الموجودة", "استيراد وتصدير المواقع"])
-
- # تبويب إضافة موقع جديد
- with management_tabs[0]:
- self._render_add_location()
-
- # تبويب تحرير المواقع الموجودة
- with management_tabs[1]:
- self._render_edit_locations()
-
- # تبويب استيراد وتصدير المواقع
- with management_tabs[2]:
- self._render_import_export_locations()
-
- def _render_add_location(self):
- """عرض نموذج إضافة موقع جديد"""
- st.markdown("### إضافة موقع مشروع جديد")
-
- # البيانات الأساسية
- project_name = st.text_input("اسم المشروع", key="new_project_name")
- project_desc = st.text_area("وصف المشروع", key="new_project_desc")
-
- col1, col2 = st.columns(2)
-
- with col1:
- project_city = st.text_input("المدينة", key="new_project_city")
- project_status = st.selectbox(
- "حالة المشروع",
- options=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"],
- key="new_project_status"
- )
-
- with col2:
- project_id = st.text_input("معرف المشروع (اختياري)", key="new_project_id", placeholder="سيتم إنشاؤه تلقائيًا إذا تُرك فارغًا")
-
- # إدخال إحداثيات الموقع
- st.markdown("#### إحداثيات الموقع")
- location_method = st.radio(
- "طريقة تحديد الموقع",
- options=["إدخال يدوي", "اختيار من الخريطة"],
- key="new_location_method"
- )
-
- # تحديد الموقع
- if location_method == "إدخال يدوي":
- loc_col1, loc_col2 = st.columns(2)
-
- with loc_col1:
- latitude = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="new_latitude")
-
- with loc_col2:
- longitude = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="new_longitude")
-
- # عرض الموقع على خريطة صغيرة
- mini_map = folium.Map(location=[latitude, longitude], zoom_start=10)
- folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map)
- folium_static(mini_map, width=700, height=300)
- else:
- st.markdown("#### اختر الموقع من الخريطة")
- st.info("انقر على الخريطة لتحديد الموقع.")
-
- # إنشاء خريطة
- m = folium.Map(location=[24.7136, 46.6753], zoom_start=6)
-
- # إضافة محدد النقر
- m.add_child(folium.ClickForMarker(popup="الموقع المحدد"))
-
- # عرض الخريطة
- map_data = folium_static(m, width=700, height=400)
-
- # استخراج الإحداثيات المحددة (ليس مدعومًا حاليًا في Streamlit)
- st.warning("ملاحظة: خاصية النقر على الخريطة غير مدعومة حاليًا في Streamlit. يرجى استخدام الإدخال اليدوي.")
-
- latitude = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="map_latitude")
- longitude = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="map_longitude")
-
- # زر إضافة الموقع
- if styled_button("إضافة الموقع", key="add_location", type="primary", icon="➕"):
- if not project_name or not project_desc or not project_city:
- st.error("يرجى تعبئة جميع الحقول المطلوبة.")
- else:
- # إنشاء معرف فريد للمشروع إذا لم يتم تحديده
- if not project_id:
- project_id = f"PRJ-{len(st.session_state.project_locations) + 1:04d}"
-
- # إنشاء كائن الموقع
- new_location = {
- "name": project_name,
- "description": project_desc,
- "city": project_city,
- "status": project_status,
- "latitude": latitude,
- "longitude": longitude,
- "project_id": project_id,
- "created_at": pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S"),
- "updated_at": pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")
- }
-
- # إضافة الموقع للقائمة
- st.session_state.project_locations.append(new_location)
-
- # حفظ البيانات
- self._save_locations_data()
-
- st.success(f"تمت إضافة موقع المشروع '{project_name}' بنجاح!")
- st.balloons()
-
- def _render_edit_locations(self):
- """عرض واجهة تحرير المواقع الموجودة"""
- st.markdown("### تحرير أو حذف مواقع المشاريع")
-
- # التحقق من وجود مواقع
- if not st.session_state.project_locations:
- st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع أولاً.")
- return
-
- # اختيار المشروع للتحرير
- selected_project_id = st.selectbox(
- "اختر مشروعًا للتحرير",
- options=[p["project_id"] for p in st.session_state.project_locations],
- format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x),
- key="edit_project_select"
- )
-
- # العثور على المشروع المحدد
- selected_project = next((p for p in st.session_state.project_locations if p["project_id"] == selected_project_id), None)
-
- if selected_project:
- # عرض نموذج التحرير
- st.markdown(f"### تحرير مشروع: {selected_project['name']}")
-
- # البيانات الأساسية
- project_name = st.text_input("اسم المشروع", value=selected_project["name"], key="edit_project_name")
- project_desc = st.text_area("وصف المشروع", value=selected_project["description"], key="edit_project_desc")
-
- col1, col2 = st.columns(2)
-
- with col1:
- project_city = st.text_input("المدينة", value=selected_project["city"], key="edit_project_city")
- project_status = st.selectbox(
- "حالة المشروع",
- options=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"],
- index=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"].index(selected_project["status"]),
- key="edit_project_status"
- )
-
- with col2:
- st.text_input("معرف المشروع", value=selected_project["project_id"], disabled=True, key="edit_project_id")
-
- # إدخال إحداثيات الموقع
- st.markdown("#### إحداثيات الموقع")
-
- # تحديد الموقع
- loc_col1, loc_col2 = st.columns(2)
-
- with loc_col1:
- latitude = st.number_input(
- "خط العرض",
- value=selected_project["latitude"],
- step=0.0001,
- format="%.6f",
- key="edit_latitude"
- )
-
- with loc_col2:
- longitude = st.number_input(
- "خط الطول",
- value=selected_project["longitude"],
- step=0.0001,
- format="%.6f",
- key="edit_longitude"
- )
-
- # عرض الموقع على خريطة صغيرة
- mini_map = folium.Map(location=[latitude, longitude], zoom_start=10)
- folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map)
- folium_static(mini_map, width=700, height=300)
-
- # أزرار الإجراءات
- col1, col2 = st.columns(2)
-
- with col1:
- if styled_button("حفظ التغييرات", key="save_location_changes", type="primary", icon="💾"):
- if not project_name or not project_desc or not project_city:
- st.error("يرجى تعبئة جميع الحقول المطلوبة.")
- else:
- # تحديث بيانات المشروع
- selected_project["name"] = project_name
- selected_project["description"] = project_desc
- selected_project["city"] = project_city
- selected_project["status"] = project_status
- selected_project["latitude"] = latitude
- selected_project["longitude"] = longitude
- selected_project["updated_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")
-
- # حفظ البيانات
- self._save_locations_data()
-
- st.success(f"تم تحديث بيانات المشروع '{project_name}' بنجاح!")
- st.experimental_rerun()
-
- with col2:
- if styled_button("حذف المشروع", key="delete_location", type="danger", icon="🗑️"):
- # تأكيد الحذف
- st.warning(f"هل أنت متأكد من حذف المشروع '{selected_project['name']}'؟")
-
- confirm_col1, confirm_col2 = st.columns(2)
-
- with confirm_col1:
- if styled_button("تأكيد الحذف", key="confirm_delete", type="danger", icon="✓"):
- # إزالة المشروع من القائمة
- st.session_state.project_locations.remove(selected_project)
-
- # حفظ البيانات
- self._save_locations_data()
-
- st.success(f"تم حذف المشروع '{selected_project['name']}' بنجاح!")
- st.experimental_rerun()
-
- with confirm_col2:
- if styled_button("إلغاء", key="cancel_delete", type="secondary", icon="❌"):
- st.experimental_rerun()
-
- def _render_import_export_locations(self):
- """عرض واجهة استيراد وتصدير المواقع"""
- st.markdown("### استيراد وتصدير مواقع المشاريع")
-
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown("#### تصدير المواقع")
-
- export_format = st.selectbox(
- "صيغة التصدير",
- options=["CSV", "JSON", "GeoJSON"],
- key="export_format"
- )
-
- if styled_button("تصدير المواقع", key="export_locations", type="primary", icon="📤"):
- self._export_locations(export_format)
-
- with col2:
- st.markdown("#### استيراد المواقع")
-
- import_format = st.selectbox(
- "صيغة الاستيراد",
- options=["CSV", "JSON", "GeoJSON"],
- key="import_format"
- )
-
- uploaded_file = st.file_uploader(
- "اختر ملف للاستيراد",
- type=["csv", "json", "geojson"],
- key="import_locations_file"
- )
-
- if uploaded_file is not None:
- if styled_button("استيراد المواقع", key="import_locations", type="success", icon="📥"):
- self._import_locations(uploaded_file, import_format)
-
- # عرض إحصائيات البيانات
- st.markdown("### إحصائيات البيانات")
-
- stats_col1, stats_col2, stats_col3 = st.columns(3)
-
- with stats_col1:
- st.metric("عدد المشاريع", len(st.session_state.project_locations))
-
- with stats_col2:
- cities = set(p["city"] for p in st.session_state.project_locations)
- st.metric("عدد المدن", len(cities))
-
- with stats_col3:
- statuses = {}
- for p in st.session_state.project_locations:
- statuses[p["status"]] = statuses.get(p["status"], 0) + 1
-
- status_str = ", ".join([f"{k}: {v}" for k, v in statuses.items()])
- st.metric("توزيع الحالات", status_str if statuses else "لا توجد بيانات")
-
- # خيارات متقدمة
- with st.expander("خيارات متقدمة"):
- if styled_button("حذف جميع المواقع", key="clear_locations", type="danger", icon="🗑️"):
- # تأكيد الحذف
- st.warning("هل أنت متأكد من حذف جميع مواقع المشاريع؟ لا يمكن التراجع عن هذا الإجراء.")
-
- confirm_col1, confirm_col2 = st.columns(2)
-
- with confirm_col1:
- if styled_button("تأكيد الحذف", key="confirm_clear", type="danger", icon="✓"):
- # مسح القائمة
- st.session_state.project_locations = []
-
- # حفظ البيانات
- self._save_locations_data()
-
- st.success("تم حذف جميع مواقع المشاريع بنجاح!")
- st.experimental_rerun()
-
- with confirm_col2:
- if styled_button("إلغاء", key="cancel_clear", type="secondary", icon="❌"):
- st.experimental_rerun()
-
- def _fetch_terrain_data(self, latitude, longitude, radius_km=5):
- """جلب بيانات التضاريس من واجهة برمجة التطبيقات"""
- try:
- # تحديث حالة الجلسة
- import plotly.express as px
-
- # تعيين الإحداثيات وحجم المنطقة
- center_lat, center_lon = latitude, longitude
-
- # تحويل نصف القطر من كم إلى درجات (تقريبي)
- radius_deg = radius_km / 111.0 # تقريب: 1 درجة = 111 كم
-
- # تحديد حدود المنطقة
- min_lat = center_lat - radius_deg
- max_lat = center_lat + radius_deg
- min_lon = center_lon - radius_deg
- max_lon = center_lon + radius_deg
-
- # إنشاء شبكة من النقاط
- resolution = 50 # عدد النقاط في كل اتجاه
- lats = np.linspace(min_lat, max_lat, resolution)
- lons = np.linspace(min_lon, max_lon, resolution)
-
- # إنشاء مصفوفة للإحداثيات
- grid_lats, grid_lons = np.meshgrid(lats, lons)
-
- # تحويل الشبكة إلى قائمة من النقاط
- points = []
- for i in range(grid_lats.shape[0]):
- for j in range(grid_lats.shape[1]):
- points.append((grid_lats[i, j], grid_lons[i, j]))
-
- # تقسيم النقاط إلى مجموعات لتقليل عدد الطلبات
- batch_size = 100
- batches = [points[i:i + batch_size] for i in range(0, len(points), batch_size)]
-
- # إنشاء بيانات التضاريس
- elevation_data = np.zeros((len(lats), len(lons)))
-
- # محاكاة بيانات التضاريس (يمكن استبدالها بواجهة برمجة تطبيقات حقيقية)
- for batch_idx, batch in enumerate(batches):
- # في بيئة الإنتاج، سيتم استبدال هذا بطلب API حقيقي
- # هنا نقوم بمحاكاة بيانات التضاريس لأغراض العرض
- for point_idx, (lat, lon) in enumerate(batch):
- # حساب المؤشر في مصفوفة الارتفاع
- lat_idx = np.abs(lats - lat).argmin()
- lon_idx = np.abs(lons - lon).argmin()
-
- # محاكاة الارتفاع (في بيئة الإنتاج سيكون هذا من واجهة برمجة التطبيقات)
- # هنا نصنع تضاريس اصطناعية باستخدام دالة جيبية
- dist_from_center = np.sqrt(
- (lat - center_lat) ** 2 + (lon - center_lon) ** 2
- )
-
- # إنشاء بعض التلال والوديان الاصطناعية
- elevation = 500 + 200 * np.sin(dist_from_center * 100) + 100 * np.cos(lat * 30) + 150 * np.sin(lon * 40)
-
- # إضافة بعض الضوضاء العشوائية
- elevation += np.random.normal(0, 30)
-
- # تخزين الارتفاع
- elevation_data[lat_idx, lon_idx] = elevation
-
- # حساب إحصائيات الارتفاع
- elevation_stats = {
- "min": float(np.min(elevation_data)),
- "max": float(np.max(elevation_data)),
- "mean": float(np.mean(elevation_data)),
- "range": float(np.max(elevation_data) - np.min(elevation_data))
- }
-
- # إنشاء مقطع ارتفاع من الشمال إلى الجنوب عبر المركز
- center_lon_idx = np.abs(lons - center_lon).argmin()
- ns_profile = []
- for i, lat in enumerate(lats):
- ns_profile.append({
- "distance": (lat - min_lat) * 111.0, # تحويل الدرجات إلى كم
- "elevation": float(elevation_data[i, center_lon_idx])
- })
-
- # إنشاء مقطع ارتفاع من الشرق إلى الغرب عبر المركز
- center_lat_idx = np.abs(lats - center_lat).argmin()
- ew_profile = []
- for i, lon in enumerate(lons):
- ew_profile.append({
- "distance": (lon - min_lon) * 111.0 * np.cos(np.radians(center_lat)), # تحويل الدرجات إلى كم مع تصحيح خط العرض
- "elevation": float(elevation_data[center_lat_idx, i])
- })
-
- # دمج المقاطع
- elevation_profile = ns_profile + ew_profile
-
- # تحضير بيانات التضاريس للعرض ثلاثي الأبعاد
- bounds = [min_lon, min_lat, max_lon, max_lat]
-
- # تحويل مصفوفة الارتفاع إلى تنسيق مناسب لـ PyDeck
- terrain_array = elevation_data.astype(np.float32)
-
- # إنشاء كائن للتضاريس
- terrain_data = [{
- "bounds": bounds,
- "terrain": terrain_array.tolist(),
- "elevation_stats": elevation_stats,
- "elevation_profile": elevation_profile
- }]
-
- return terrain_data
-
- except Exception as e:
- st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}")
- raise e
-
- def _calculate_distance(self, lat1, lon1, lat2, lon2):
- """حساب المسافة بين نقطتين بالكيلومترات باستخدام صيغة هافرساين"""
- import math
-
- # تحويل الإحداثيات إلى راديان
- lat1 = math.radians(lat1)
- lon1 = math.radians(lon1)
- lat2 = math.radians(lat2)
- lon2 = math.radians(lon2)
-
- # صيغة هافرساين
- dlon = lon2 - lon1
- dlat = lat2 - lat1
- a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
- c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
- distance = 6371 * c # نصف قطر الأرض بالكيلومترات
-
- return distance
-
- def _get_color_map(self, scheme):
- """الحصول على خريطة الألوان حسب النظام المختار"""
- if scheme == "terrain":
- return [
- [0, (0, 50, 0)],
- [0.1, (0, 100, 0)],
- [0.25, (0, 150, 0)],
- [0.4, (200, 170, 0)],
- [0.6, (150, 100, 0)],
- [0.8, (100, 50, 0)],
- [1, (200, 200, 200)]
- ]
- elif scheme == "elevation":
- return [
- [0, (0, 0, 100)],
- [0.2, (0, 100, 150)],
- [0.4, (0, 150, 50)],
- [0.6, (150, 150, 0)],
- [0.8, (150, 50, 0)],
- [1, (100, 0, 0)]
- ]
- else: # custom
- return [
- [0, (30, 100, 200)],
- [0.3, (60, 170, 250)],
- [0.5, (200, 220, 150)],
- [0.7, (180, 120, 60)],
- [0.9, (110, 60, 30)],
- [1, (80, 30, 10)]
- ]
-
- def _export_locations(self, format):
- """تصدير مواقع المشاريع إلى ملف"""
- try:
- if not st.session_state.project_locations:
- st.error("لا توجد مواقع مشاريع للتصدير.")
- return
-
- if format == "CSV":
- # تصدير إلى CSV
- df = pd.DataFrame(st.session_state.project_locations)
-
- csv_data = df.to_csv(index=False)
-
- st.download_button(
- label="تنزيل ملف CSV",
- data=csv_data,
- file_name="project_locations.csv",
- mime="text/csv"
- )
-
- elif format == "JSON":
- # تصدير إلى JSON
- json_data = json.dumps(st.session_state.project_locations, ensure_ascii=False, indent=2)
-
- st.download_button(
- label="تنزيل ملف JSON",
- data=json_data,
- file_name="project_locations.json",
- mime="application/json"
- )
-
- elif format == "GeoJSON":
- # تصدير إلى GeoJSON
- features = []
-
- for location in st.session_state.project_locations:
- feature = {
- "type": "Feature",
- "geometry": {
- "type": "Point",
- "coordinates": [location["longitude"], location["latitude"]]
- },
- "properties": {
- "name": location["name"],
- "description": location["description"],
- "city": location["city"],
- "status": location["status"],
- "project_id": location["project_id"],
- "created_at": location.get("created_at", ""),
- "updated_at": location.get("updated_at", "")
- }
- }
-
- features.append(feature)
-
- geojson = {
- "type": "FeatureCollection",
- "features": features
- }
-
- geojson_data = json.dumps(geojson, ensure_ascii=False, indent=2)
-
- st.download_button(
- label="تنزيل ملف GeoJSON",
- data=geojson_data,
- file_name="project_locations.geojson",
- mime="application/geo+json"
- )
-
- st.success(f"تم تصدير {len(st.session_state.project_locations)} موقع بنجاح!")
-
- except Exception as e:
- st.error(f"حدث خطأ أثناء تصدير المواقع: {str(e)}")
-
- def _import_locations(self, uploaded_file, format):
- """استيراد مواقع المشاريع من ملف"""
- try:
- if format == "CSV":
- # استيراد من CSV
- df = pd.read_csv(uploaded_file)
-
- # التحقق من وجود الأعمدة المطلوبة
- required_columns = ["name", "latitude", "longitude"]
- missing_columns = [col for col in required_columns if col not in df.columns]
-
- if missing_columns:
- st.error(f"الملف لا يحتوي على الأعمدة التالية: {', '.join(missing_columns)}")
- return
-
- # تحويل DataFrame إلى قائمة من القواميس
- imported_locations = df.to_dict("records")
-
- elif format == "JSON":
- # استيراد من JSON
- imported_locations = json.loads(uploaded_file.read())
-
- elif format == "GeoJSON":
- # استيراد من GeoJSON
- geojson = json.loads(uploaded_file.read())
-
- # التحقق من صحة التنسيق
- if "type" not in geojson or geojson["type"] != "FeatureCollection" or "features" not in geojson:
- st.error("تنسيق GeoJSON غير صحيح.")
- return
-
- # تحويل المميزات إلى مواقع
- imported_locations = []
-
- for feature in geojson["features"]:
- if feature["type"] == "Feature" and feature["geometry"]["type"] == "Point":
- coords = feature["geometry"]["coordinates"]
- properties = feature["properties"]
-
- location = {
- "name": properties.get("name", ""),
- "description": properties.get("description", ""),
- "city": properties.get("city", ""),
- "status": properties.get("status", "جديد"),
- "longitude": coords[0],
- "latitude": coords[1],
- "project_id": properties.get("project_id", f"PRJ-{len(imported_locations)+1:04d}"),
- "created_at": properties.get("created_at", pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")),
- "updated_at": properties.get("updated_at", pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S"))
- }
-
- imported_locations.append(location)
-
- # التحقق من وجود البيانات المطلوبة في الملف المستورد
- valid_locations = []
- for location in imported_locations:
- # التحقق من وجود الحقول المطلوبة
- if "name" not in location or "latitude" not in location or "longitude" not in location:
- continue
-
- # إضافة القيم الافتراضية إذا لم تكن موجودة
- if "description" not in location:
- location["description"] = ""
-
- if "city" not in location:
- location["city"] = ""
-
- if "status" not in location:
- location["status"] = "جديد"
-
- if "project_id" not in location:
- location["project_id"] = f"PRJ-{len(valid_locations)+1:04d}"
-
- if "created_at" not in location:
- location["created_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")
-
- if "updated_at" not in location:
- location["updated_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")
-
- valid_locations.append(location)
-
- if not valid_locations:
- st.error("لم يتم العثور على مواقع صالحة في الملف.")
- return
-
- # سؤال المستخدم عن كيفية الاستيراد
- import_mode = st.radio(
- "كيفية الاستيراد",
- options=["إضافة إلى المواقع الموجودة", "استبدال المواقع الموجودة"],
- key="import_mode"
- )
-
- if styled_button("تأكيد الاستيراد", key="confirm_import", type="success", icon="✓"):
- if import_mode == "إضافة إلى المواقع الموجودة":
- # إضافة المواقع المستوردة إلى القائمة الحالية
- st.session_state.project_locations.extend(valid_locations)
- else:
- # استبدال المواقع الموجودة بالمواقع المستوردة
- st.session_state.project_locations = valid_locations
-
- # حفظ البيانات
- self._save_locations_data()
-
- st.success(f"تم استيراد {len(valid_locations)} موقع بنجاح!")
- st.experimental_rerun()
-
- except Exception as e:
- st.error(f"حدث خطأ أثناء استيراد المواقع: {str(e)}")
-
- def _save_locations_data(self):
- """حفظ بيانات المواقع"""
- try:
- # التأكد من وجود المجلد
- os.makedirs(self.data_dir, exist_ok=True)
-
- # حفظ البيانات
- locations_file = os.path.join(self.data_dir, "project_locations.json")
-
- with open(locations_file, 'w', encoding='utf-8') as f:
- json.dump(st.session_state.project_locations, f, ensure_ascii=False, indent=2)
- except Exception as e:
- st.error(f"حدث خطأ أثناء حفظ بيانات المواقع: {str(e)}")
-
- def _load_locations_data(self):
- """تحميل بيانات المواقع"""
- try:
- # التحقق من وجود الملف
- locations_file = os.path.join(self.data_dir, "project_locations.json")
-
- if os.path.exists(locations_file):
- with open(locations_file, 'r', encoding='utf-8') as f:
- locations = json.load(f)
-
- # تحديث حالة الجلسة
- st.session_state.project_locations = locations
- except Exception as e:
- st.error(f"حدث خطأ أثناء تحميل بيانات المواقع: {str(e)}")
-
- def _initialize_sample_projects(self):
- """تهيئة بيانات اختبارية للمشاريع"""
- # التحقق من وجود بيانات محفوظة
- locations_file = os.path.join(self.data_dir, "project_locations.json")
-
- if os.path.exists(locations_file):
- # تحميل البيانات المحفوظة
- self._load_locations_data()
- return
-
- # إنشاء بيانات اختبارية إذا لم تكن هناك بيانات محفوظة
- sample_projects = [
- {
- "name": "تطوير شبكة الطرق في منطقة الرياض",
- "description": "مشروع تطوير وتوسعة شبكة الطرق الرئيسية في منطقة الرياض",
- "city": "الرياض",
- "status": "قيد التنفيذ",
- "latitude": 24.7136,
- "longitude": 46.6753,
- "project_id": "PRJ-0001",
- "created_at": "2025-01-15 10:30:00",
- "updated_at": "2025-01-15 10:30:00"
- },
- {
- "name": "إنشاء سد وادي حنيفة",
- "description": "مشروع إنشاء سد لحجز مياه الأمطار في وادي حنيفة",
- "city": "الرياض",
- "status": "جديد",
- "latitude": 24.6748,
- "longitude": 46.5831,
- "project_id": "PRJ-0002",
- "created_at": "2025-02-01 14:45:00",
- "updated_at": "2025-02-01 14:45:00"
- },
- {
- "name": "تطوير ميناء جدة الإسلامي",
- "description": "مشروع تطوير وتوسعة ميناء جدة الإسلامي لزيادة الطاقة الاستيعابية",
- "city": "جدة",
- "status": "قيد التنفيذ",
- "latitude": 21.4858,
- "longitude": 39.1925,
- "project_id": "PRJ-0003",
- "created_at": "2024-11-20 09:15:00",
- "updated_at": "2024-11-20 09:15:00"
- },
- {
- "name": "إنشاء مطار الدمام الجديد",
- "description": "مشروع إنشاء مطار جديد في مدينة الدمام لتلبية الطلب المتزايد",
- "city": "الدمام",
- "status": "متوقف",
- "latitude": 26.4207,
- "longitude": 50.0888,
- "project_id": "PRJ-0004",
- "created_at": "2024-10-05 11:30:00",
- "updated_at": "2024-10-05 11:30:00"
- },
- {
- "name": "توسعة جامعة الملك فهد للبترول والمعادن",
- "description": "مشروع توسعة مباني ومرافق جامعة الملك فهد للبترول والمعادن",
- "city": "الظهران",
- "status": "قيد التنفيذ",
- "latitude": 26.3927,
- "longitude": 50.1150,
- "project_id": "PRJ-0005",
- "created_at": "2025-01-10 08:00:00",
- "updated_at": "2025-01-10 08:00:00"
- },
- {
- "name": "إنشاء محطة تحلية مياه القنفذة",
- "description": "مشروع إنشاء محطة تحلية مياه جديدة في محافظة القنفذة",
- "city": "القنفذة",
- "status": "جديد",
- "latitude": 19.1299,
- "longitude": 41.0825,
- "project_id": "PRJ-0006",
- "created_at": "2025-02-20 15:20:00",
- "updated_at": "2025-02-20 15:20:00"
- },
- {
- "name": "تطوير مجمع حكومي في حائل",
- "description": "مشروع إنشاء وتطوير مجمع للدوائر الحكومية في مدينة حائل",
- "city": "حائل",
- "status": "مكتمل",
- "latitude": 27.5114,
- "longitude": 41.7208,
- "project_id": "PRJ-0007",
- "created_at": "2024-06-15 10:00:00",
- "updated_at": "2024-12-10 14:30:00"
- },
- {
- "name": "إنشاء مستشفى الإحساء العام",
- "description": "مشروع إنشاء مستشفى عام جديد في محافظة الإحساء بسعة 500 سرير",
- "city": "الإحساء",
- "status": "قيد التنفيذ",
- "latitude": 25.3753,
- "longitude": 49.5873,
- "project_id": "PRJ-0008",
- "created_at": "2024-09-01 09:45:00",
- "updated_at": "2024-09-01 09:45:00"
- },
- {
- "name": "تطوير شبكة الصرف الصحي في أبها",
- "description": "مشروع تطوير وتوسعة شبكة الصرف الصحي في مدينة أبها",
- "city": "أبها",
- "status": "جديد",
- "latitude": 18.2164,
- "longitude": 42.5053,
- "project_id": "PRJ-0009",
- "created_at": "2025-02-25 11:15:00",
- "updated_at": "2025-02-25 11:15:00"
- },
- {
- "name": "إنشاء مدينة صناعية في سكاكا",
- "description": "مشروع إنشاء مدينة صناعية جديدة في منطقة سكاكا",
- "city": "سكاكا",
- "status": "متوقف",
- "latitude": 29.9720,
- "longitude": 40.2006,
- "project_id": "PRJ-0010",
- "created_at": "2024-07-20 13:30:00",
- "updated_at": "2024-07-20 13:30:00"
- }
- ]
-
- # تحديث حالة الجلسة
- st.session_state.project_locations = sample_projects
-
- # حفظ البيانات
- self._save_locations_data()
-
-
-# فئة تحويل Folium إلى Streamlit
-class folium_static:
- """فئة لعرض خرائط Folium في Streamlit"""
-
- def __init__(self, fig, width=700, height=500):
- """عرض خريطة Folium في Streamlit"""
- import streamlit.components.v1 as components
-
- # تحويل خريطة Folium إلى HTML
- fig_html = fig._repr_html_()
-
- # إنشاء مكون HTML مخصص
- components.html(fig_html, width=width, height=height)
-
-
-# تشغيل الوحدة بشكل مستقل
-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 لتحليل المناقصات'
- }
- )
-
- # تهيئة وحدة الخريطة التفاعلية
- interactive_map = InteractiveMap()
-
- # عرض واجهة الوحدة
- interactive_map.render()
-
-# تشغيل الوحدة عند استدعاء الملف مباشرة
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/modules/maps/maps_app.py b/modules/maps/maps_app.py
deleted file mode 100644
index cbd8753a34662ccdf1af4454e3de51fb79e8457f..0000000000000000000000000000000000000000
--- a/modules/maps/maps_app.py
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/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.maps.interactive_map import InteractiveMap
-
-
-class MapsApp:
- """وحدة تطبيق الخريطة التفاعلية"""
-
- def __init__(self):
- """تهيئة وحدة تطبيق الخريطة التفاعلية"""
- self.interactive_map = InteractiveMap()
-
- def render(self):
- """عرض واجهة وحدة تطبيق الخريطة التفاعلية"""
- st.markdown("
وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد
", unsafe_allow_html=True)
-
- st.markdown("""
-
- تمكنك هذه الوحدة من عرض وإدارة مواقع المشاريع على خريطة تفاعلية، مع إمكانية عرض التضاريس بشكل ثلاثي الأبعاد.
- يمكنك إضافة وتحرير مواقع المشاريع، وتحليل توزيعها الجغرافي، وعرض المعلومات الطبوغرافية للمواقع.
-
- """, unsafe_allow_html=True)
-
- # عرض وحدة الخريطة التفاعلية
- self.interactive_map.render()
-
-
-# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
-if __name__ == "__main__":
- 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
deleted file mode 100644
index a177748231ffad32b4284b13461159d77057db3d..0000000000000000000000000000000000000000
--- a/modules/notifications/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# ملف تهيئة وحدة الإشعارات الذكية
\ No newline at end of file
diff --git a/modules/notifications/notifications_app.py b/modules/notifications/notifications_app.py
deleted file mode 100644
index 16aae363ddc6a62d64c8d66ecb40d0999c009f57..0000000000000000000000000000000000000000
--- a/modules/notifications/notifications_app.py
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/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.notifications.smart_notifications import SmartNotificationSystem
-
-
-class NotificationsApp:
- """وحدة تطبيق نظام الإشعارات الذكي"""
-
- def __init__(self):
- """تهيئة وحدة تطبيق نظام الإشعارات الذكي"""
- self.smart_notification_system = SmartNotificationSystem()
-
- def render(self):
- """عرض واجهة وحدة تطبيق نظام الإشعارات الذكي"""
- st.markdown("
نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات
", unsafe_allow_html=True)
-
- st.markdown("""
-
- يتيح لك نظام الإشعارات الذكي متابعة تحديثات المشاريع وتلقي تنبيهات مخصصة حسب أدوارك واهتماماتك.
- يمكنك تخصيص إعدادات الإشعارات وجدولة التذكيرات للمواعيد النهائية والمهام.
-
- """, unsafe_allow_html=True)
-
- # عرض نظام الإشعارات الذكي
- self.smart_notification_system.render()
-
-
-# تشغيل التطبيق بشكل مستقل عند استدعاء الملف مباشرة
-if __name__ == "__main__":
- st.set_page_config(
- page_title="نظام الإشعارات الذكي | WAHBi AI",
- page_icon="🔔",
- layout="wide",
- initial_sidebar_state="expanded"
- )
-
- app = NotificationsApp()
- app.render()
\ No newline at end of file
diff --git a/modules/notifications/smart_notifications.py b/modules/notifications/smart_notifications.py
deleted file mode 100644
index cb9cf6615e230c40ff913ae9e7d8e2b226f725d9..0000000000000000000000000000000000000000
--- a/modules/notifications/smart_notifications.py
+++ /dev/null
@@ -1,1237 +0,0 @@
-#!/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 datetime
-import time
-import threading
-import logging
-from typing import List, Dict, Any, Tuple, Optional, Union
-
-# إضافة مسار النظام للوصول للملفات المشتركة
-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 SmartNotificationSystem:
- """فئة نظام الإشعارات الذكي لتحديثات المشروع والتنبيهات"""
-
- def __init__(self):
- """تهيئة نظام الإشعارات الذكي"""
- # تهيئة مجلدات حفظ البيانات
- self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/notifications"))
- os.makedirs(self.data_dir, exist_ok=True)
-
- # تهيئة قائمة الإشعارات
- if 'notifications' not in st.session_state:
- st.session_state.notifications = []
-
- if 'unread_count' not in st.session_state:
- st.session_state.unread_count = 0
-
- if 'notification_channels' not in st.session_state:
- st.session_state.notification_channels = {
- "browser": True,
- "email": False,
- "sms": False,
- "mobile_app": False
- }
-
- if 'notification_preferences' not in st.session_state:
- st.session_state.notification_preferences = {
- "project_updates": True,
- "document_analysis": True,
- "deadline_reminders": True,
- "risk_alerts": True,
- "price_changes": True,
- "team_mentions": True,
- "system_updates": True
- }
-
- # تحميل الإشعارات المحفوظة
- self._load_notifications()
-
- # تسجيل الأحداث
- logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
- handlers=[
- logging.FileHandler(os.path.join(self.data_dir, "notifications.log")),
- logging.StreamHandler()
- ]
- )
- self.logger = logging.getLogger("smart_notifications")
-
- def render(self):
- """عرض واجهة نظام الإشعارات الذكي"""
- render_header("نظام الإشعارات الذكي")
-
- # تبويبات الوحدة
- tabs = st.tabs([
- "جميع الإشعارات",
- "إشعارات غير مقروءة",
- "إعدادات الإشعارات",
- "جدولة الإشعارات",
- "تقارير وإحصائيات"
- ])
-
- # تبويب جميع الإشعارات
- with tabs[0]:
- self._render_all_notifications()
-
- # تبويب الإشعارات غير المقروءة
- with tabs[1]:
- self._render_unread_notifications()
-
- # تبويب إعدادات الإشعارات
- with tabs[2]:
- self._render_notification_settings()
-
- # تبويب جدولة الإشعارات
- with tabs[3]:
- self._render_notification_scheduling()
-
- # تبويب تقارير وإحصائيات
- with tabs[4]:
- self._render_notification_analytics()
-
- # عرض حقوق النشر
- render_credits()
-
- def _render_all_notifications(self):
- """عرض جميع الإشعارات"""
- st.markdown("""
-
-
🔔 جميع الإشعارات
-
عرض كافة الإشعارات والتنبيهات الخاصة بالمشاريع والنظام.
-
- """, unsafe_allow_html=True)
-
- # أزرار التحكم
- col1, col2, col3 = st.columns([1, 1, 1])
-
- with col1:
- if styled_button("تحديث الإشعارات", key="refresh_notifications", type="primary", icon="🔄"):
- self._load_notifications()
- st.success("تم تحديث الإشعارات بنجاح")
-
- with col2:
- if styled_button("تعليم الكل كمقروء", key="mark_all_read", type="secondary", icon="✓"):
- self._mark_all_as_read()
- st.success("تم تعليم جميع الإشعارات كمقروءة")
-
- with col3:
- if styled_button("حذف جميع الإشعارات", key="clear_notifications", type="danger", icon="🗑️"):
- confirmed = st.text_input("اكتب 'تأكيد' لحذف جميع الإشعارات", key="confirm_clear")
- if confirmed == "تأكيد":
- self._clear_all_notifications()
- st.success("تم حذف جميع الإشعارات بنجاح")
-
- # فلترة الإشعارات
- filter_col1, filter_col2 = st.columns(2)
-
- with filter_col1:
- notification_type = st.multiselect(
- "تصفية حسب النوع",
- options=[
- "تحديث مشروع", "وثيقة جديدة", "تذكير موعد نهائي",
- "تنبيه مخاطر", "تغيير سعر", "إشارة فريق العمل", "تحديث النظام"
- ],
- key="filter_notification_type"
- )
-
- with filter_col2:
- date_range = st.date_input(
- "نطاق التاريخ",
- value=(
- datetime.datetime.now() - datetime.timedelta(days=30),
- datetime.datetime.now()
- ),
- key="filter_date_range"
- )
-
- # تصفية الإشعارات
- filtered_notifications = self._filter_notifications(
- notification_type=notification_type,
- date_range=date_range
- )
-
- # عرض الإشعارات المصفاة
- if filtered_notifications:
- for notification in filtered_notifications:
- self._render_notification_card(notification)
- else:
- st.info("لا توجد إشعارات متاحة")
-
- def _render_unread_notifications(self):
- """عرض الإشعارات غير المقروءة"""
- st.markdown("""
-
-
🔔 الإشعارات غير المقروءة
-
عرض الإشعارات والتنبيهات التي لم تتم قراءتها بعد.
-
- """, unsafe_allow_html=True)
-
- # أزرار التحكم
- col1, col2 = st.columns(2)
-
- with col1:
- if styled_button("تحديث الإشعارات", key="refresh_unread", type="primary", icon="🔄"):
- self._load_notifications()
- st.success("تم تحديث الإشعارات بنجاح")
-
- with col2:
- if styled_button("تعليم الكل كمقروء", key="mark_unread_read", type="secondary", icon="✓"):
- self._mark_all_as_read()
- st.success("تم تعليم جميع الإشعارات كمقروءة")
-
- # فلترة الإشعارات غير المقروءة
- unread_notifications = [n for n in st.session_state.notifications if not n.get("read", False)]
-
- # عرض الإشعارات غير المقروءة
- if unread_notifications:
- for notification in unread_notifications:
- self._render_notification_card(notification, show_mark_button=True)
- else:
- st.success("لا توجد إشعارات غير مقروءة")
-
- def _render_notification_settings(self):
- """عرض إعدادات الإشعارات"""
- st.markdown("""
-
-
⚙️ إعدادات الإشعارات
-
تخصيص إعدادات وتفضيلات الإشعارات الخاصة بك.
-
- """, unsafe_allow_html=True)
-
- # قسم قنوات الإشعارات
- st.markdown("### قنوات الإشعارات")
- st.markdown("حدد الطرق التي ترغب في تلقي الإشعارات من خلالها.")
-
- channels_col1, channels_col2 = st.columns(2)
-
- with channels_col1:
- st.session_state.notification_channels["browser"] = st.checkbox(
- "إشعارات المتصفح",
- value=st.session_state.notification_channels.get("browser", True),
- key="channel_browser"
- )
-
- st.session_state.notification_channels["email"] = st.checkbox(
- "البريد الإلكتروني",
- value=st.session_state.notification_channels.get("email", False),
- key="channel_email"
- )
-
- if st.session_state.notification_channels["email"]:
- email = st.text_input(
- "البريد الإلكتروني للإشعارات",
- value=st.session_state.get("notification_email", ""),
- key="notification_email"
- )
- st.session_state.notification_email = email
-
- with channels_col2:
- st.session_state.notification_channels["sms"] = st.checkbox(
- "الرسائل النصية (SMS)",
- value=st.session_state.notification_channels.get("sms", False),
- key="channel_sms"
- )
-
- if st.session_state.notification_channels["sms"]:
- phone = st.text_input(
- "رقم الهاتف للإشعارات",
- value=st.session_state.get("notification_phone", ""),
- key="notification_phone"
- )
- st.session_state.notification_phone = phone
-
- st.session_state.notification_channels["mobile_app"] = st.checkbox(
- "تطبيق الهاتف المحمول",
- value=st.session_state.notification_channels.get("mobile_app", False),
- key="channel_mobile_app"
- )
-
- # قسم تفضيلات الإشعارات
- st.markdown("### أنواع الإشعارات")
- st.markdown("حدد أنواع الإشعارات التي ترغب في تلقيها.")
-
- prefs_col1, prefs_col2 = st.columns(2)
-
- with prefs_col1:
- st.session_state.notification_preferences["project_updates"] = st.checkbox(
- "تحديثات المشاريع",
- value=st.session_state.notification_preferences.get("project_updates", True),
- key="pref_project_updates"
- )
-
- st.session_state.notification_preferences["document_analysis"] = st.checkbox(
- "تحليل المستندات",
- value=st.session_state.notification_preferences.get("document_analysis", True),
- key="pref_document_analysis"
- )
-
- st.session_state.notification_preferences["deadline_reminders"] = st.checkbox(
- "تذكيرات المواعيد النهائية",
- value=st.session_state.notification_preferences.get("deadline_reminders", True),
- key="pref_deadline_reminders"
- )
-
- st.session_state.notification_preferences["risk_alerts"] = st.checkbox(
- "تنبيهات المخاطر",
- value=st.session_state.notification_preferences.get("risk_alerts", True),
- key="pref_risk_alerts"
- )
-
- with prefs_col2:
- st.session_state.notification_preferences["price_changes"] = st.checkbox(
- "تغييرات الأسعار",
- value=st.session_state.notification_preferences.get("price_changes", True),
- key="pref_price_changes"
- )
-
- st.session_state.notification_preferences["team_mentions"] = st.checkbox(
- "إشارات فريق العمل",
- value=st.session_state.notification_preferences.get("team_mentions", True),
- key="pref_team_mentions"
- )
-
- st.session_state.notification_preferences["system_updates"] = st.checkbox(
- "تحديثات النظام",
- value=st.session_state.notification_preferences.get("system_updates", True),
- key="pref_system_updates"
- )
-
- # إعدادات التكرار
- st.markdown("### إعدادات التكرار")
-
- frequency = st.radio(
- "تكرار الإشعارات المتشابهة",
- options=["فوري", "تجميع كل ساعة", "تجميع كل يوم", "مخصص"],
- index=0,
- key="notification_frequency"
- )
-
- if frequency == "مخصص":
- custom_hours = st.number_input(
- "التجميع كل (ساعات)",
- min_value=1,
- max_value=24,
- value=4,
- key="custom_frequency_hours"
- )
- st.session_state.custom_frequency_hours = custom_hours
-
- # إعدادات متقدمة
- with st.expander("إعدادات متقدمة"):
- st.checkbox(
- "عرض الإشعارات عند بدء تشغيل النظام",
- value=True,
- key="show_on_startup"
- )
-
- st.checkbox(
- "الإشعارات الصوتية",
- value=False,
- key="audio_notifications"
- )
-
- st.checkbox(
- "حفظ سجل الإشعارات",
- value=True,
- key="log_notifications"
- )
-
- # استدعاء القيمة من session_state إذا كانت موجودة أو استخدام القيمة الافتراضية
- retention_days = st.slider(
- "الاحتفاظ بالإشعارات (أيام)",
- min_value=7,
- max_value=365,
- value=st.session_state.get("retention_days_value", 90),
- key="retention_days"
- )
- # حفظ القيمة في مفتاح آخر بعد تحديثها عن طريق المستخدم
- if "retention_days_value" not in st.session_state:
- st.session_state.retention_days_value = retention_days
-
- # زر حفظ الإعدادات
- if styled_button("حفظ الإعدادات", key="save_notification_settings", type="primary", icon="💾"):
- self._save_notification_settings()
- st.success("تم حفظ إعدادات الإشعارات بنجاح")
-
- def _render_notification_scheduling(self):
- """عرض واجهة جدولة الإشعارات"""
- st.markdown("""
-
-
🕒 جدولة الإشعارات
-
إنشاء وإدارة الإشعارات المجدولة والتذكيرات الدورية.
", 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)
-
- 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
deleted file mode 100644
index 77df2b857fe88bb48c307b3b14c5217c425efad2..0000000000000000000000000000000000000000
--- a/modules/pricing/services/unbalanced_pricing.py
+++ /dev/null
@@ -1,213 +0,0 @@
-"""
-خدمة التسعير غير المتزن
-"""
-
-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
deleted file mode 100644
index 9db1b7f586f7317f57fcf81089a4f79d5bd0d09b..0000000000000000000000000000000000000000
--- a/modules/pricing/specs_analyzer.py
+++ /dev/null
@@ -1,527 +0,0 @@
-"""
-تطبيق وحدة التسعير المتكاملة
-"""
-
-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_management/project_management_app.py b/modules/project_management/project_management_app.py
deleted file mode 100644
index d731b68e21c3ae64fb6e8b5b2357e7dc46e8b598..0000000000000000000000000000000000000000
--- a/modules/project_management/project_management_app.py
+++ /dev/null
@@ -1,666 +0,0 @@
-"""
-وحدة إدارة المشاريع - نظام تحليل المناقصات
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-from datetime import datetime, timedelta
-import os
-import time
-import io
-import sys
-from pathlib import Path
-
-# إضافة مسار المشروع للنظام
-sys.path.append(str(Path(__file__).parent.parent))
-
-# استيراد محسن واجهة المستخدم
-from styling.enhanced_ui import UIEnhancer
-
-class ProjectsApp:
- """وحدة إدارة المشاريع"""
-
- def __init__(self):
- """تهيئة وحدة إدارة المشاريع"""
- self.ui = UIEnhancer(page_title="إدارة المشاريع - نظام تحليل المناقصات", page_icon="📋")
- self.ui.apply_theme_colors()
-
- # تهيئة البيانات المبدئية
- if 'projects' not in st.session_state:
- st.session_state.projects = self._generate_sample_projects()
-
- 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": "translate"},
- {"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._render_projects_list_tab()
-
- with tabs[1]:
- self._render_add_project_tab()
-
- with tabs[2]:
- self._render_project_details_tab()
-
- with tabs[3]:
- self._render_projects_tracking_tab()
-
- def _render_projects_list_tab(self):
- """عرض تبويب قائمة المشاريع"""
-
- st.markdown("### قائمة المشاريع")
-
- # فلترة المشاريع
- col1, col2, col3 = st.columns(3)
-
- with col1:
- search_term = st.text_input("البحث في المشاريع", key="project_search")
-
- with col2:
- status_filter = st.multiselect(
- "حالة المشروع",
- ["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
- default=["جديد", "قيد التسعير", "تم التقديم"],
- key="project_status_filter"
- )
-
- with col3:
- client_filter = st.multiselect(
- "الجهة المالكة",
- list(set([p['client'] for p in st.session_state.projects])),
- key="project_client_filter"
- )
-
- # تطبيق الفلترة
- filtered_projects = st.session_state.projects
-
- if search_term:
- filtered_projects = [p for p in filtered_projects if search_term.lower() in p['name'].lower() or search_term in p['number']]
-
- if status_filter:
- filtered_projects = [p for p in filtered_projects if p['status'] in status_filter]
-
- if client_filter:
- filtered_projects = [p for p in filtered_projects if p['client'] in client_filter]
-
- # تحويل المشاريع المفلترة إلى DataFrame للعرض
- if filtered_projects:
- projects_df = pd.DataFrame(filtered_projects)
-
- # اختيار وترتيب الأعمدة
- display_columns = [
- 'name', 'number', 'client', 'location', 'status',
- 'submission_date', 'tender_type', 'created_at'
- ]
-
- # تغيير أسماء الأعمدة للعرض
- column_names = {
- 'name': 'اسم المشروع',
- 'number': 'رقم المناقصة',
- 'client': 'الجهة المالكة',
- 'location': 'الموقع',
- 'status': 'الحالة',
- 'submission_date': 'تاريخ التقديم',
- 'tender_type': 'نوع المناقصة',
- 'created_at': 'تاريخ الإنشاء'
- }
-
- display_df = projects_df[display_columns].rename(columns=column_names)
-
- # تنسيق التواريخ
- date_columns = ['تاريخ التقديم', 'تاريخ الإنشاء']
- for col in date_columns:
- if col in display_df.columns:
- display_df[col] = pd.to_datetime(display_df[col]).dt.strftime('%Y-%m-%d')
-
- # عرض الجدول
- st.dataframe(display_df, use_container_width=True, hide_index=True)
-
- # زر تصدير المشاريع
- if st.button("تصدير المشاريع إلى Excel"):
- # محاكاة التصدير
- st.success("تم تصدير المشاريع بنجاح!")
- else:
- st.info("لا توجد مشاريع تطابق معايير البحث.")
-
- def _render_add_project_tab(self):
- """عرض تبويب إضافة مشروع جديد"""
-
- st.markdown("### إضافة مشروع جديد")
-
- # نموذج إدخال بيانات المشروع
- with st.form("new_project_form"):
- col1, col2 = st.columns(2)
-
- with col1:
- project_name = st.text_input("اسم المشروع", key="new_project_name")
- client = st.text_input("الجهة المالكة", key="new_project_client")
- location = st.text_input("الموقع", key="new_project_location")
- tender_type = st.selectbox(
- "نوع المناقصة",
- ["عامة", "خاصة", "أمر مباشر"],
- key="new_project_tender_type"
- )
-
- with col2:
- tender_number = st.text_input("رقم المناقصة", key="new_project_number")
- submission_date = st.date_input("تاريخ التقديم", key="new_project_submission_date")
- pricing_method = st.selectbox(
- "طريقة التسعير",
- ["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
- key="new_project_pricing_method"
- )
- status = st.selectbox(
- "حالة المشروع",
- ["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
- index=0,
- key="new_project_status"
- )
-
- description = st.text_area("وصف المشروع", key="new_project_description")
-
- submitted = st.form_submit_button("إضافة المشروع")
-
- if submitted:
- # التحقق من تعبئة الحقول الإلزامية
- if not project_name or not tender_number or not client:
- st.error("يرجى تعبئة جميع الحقول الإلزامية (اسم المشروع، رقم المناقصة، الجهة المالكة).")
- else:
- # إنشاء مشروع جديد
- new_project = {
- 'id': len(st.session_state.projects) + 1,
- 'name': project_name,
- 'number': tender_number,
- 'client': client,
- 'location': location,
- 'description': description,
- 'status': status,
- 'tender_type': tender_type,
- 'pricing_method': pricing_method,
- 'submission_date': submission_date,
- 'created_at': datetime.now(),
- 'created_by_id': 1 # معرف المستخدم الحالي
- }
-
- # إضافة المشروع إلى قائمة المشاريع
- st.session_state.projects.append(new_project)
-
- # رسالة نجاح
- st.success(f"تم إضافة المشروع [{project_name}] بنجاح!")
-
- # تعيين المشروع الحالي
- st.session_state.current_project = new_project
-
- def _render_project_details_tab(self):
- """عرض تبويب تفاصيل المشروع"""
-
- st.markdown("### تفاصيل المشروع")
-
- # التحقق من وجود مشروع حالي
- if 'current_project' not in st.session_state or st.session_state.current_project is None:
- # إذا لم يكن هناك مشروع محدد، اعرض قائمة باختيار المشروع
- 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
-
- # عرض تفاصيل المشروع
- project = st.session_state.current_project
-
- # عرض معلومات المشروع الأساسية
- col1, col2, col3 = st.columns(3)
-
- with col1:
- st.markdown(f"**اسم المشروع**: {project['name']}")
- st.markdown(f"**رقم المناقصة**: {project['number']}")
- st.markdown(f"**الجهة المالكة**: {project['client']}")
-
- with col2:
- st.markdown(f"**الموقع**: {project['location']}")
- st.markdown(f"**نوع المناقصة**: {project['tender_type']}")
- st.markdown(f"**حالة المشروع**: {project['status']}")
-
- with col3:
- st.markdown(f"**طريقة التسعير**: {project['pricing_method']}")
- st.markdown(f"**تاريخ التقديم**: {project['submission_date'].strftime('%Y-%m-%d') if isinstance(project['submission_date'], datetime) else project['submission_date']}")
- st.markdown(f"**تاريخ الإنشاء**: {project['created_at'].strftime('%Y-%m-%d') if isinstance(project['created_at'], datetime) else project['created_at']}")
-
- # عرض وصف المشروع
- st.markdown("#### وصف المشروع")
- st.text_area("", value=project.get('description', ''), disabled=True, height=100)
-
- # عرض المستندات المرتبطة بالمشروع
- st.markdown("#### مستندات المشروع")
-
- if 'documents' in project and project['documents']:
- docs_df = pd.DataFrame(project['documents'])
- st.dataframe(docs_df, use_container_width=True, hide_index=True)
- else:
- st.info("لا توجد مستندات مرتبطة بهذا المشروع حاليًا.")
-
- # زر إضافة مستندات
- if st.button("إضافة مستندات"):
- st.session_state.upload_documents = True
-
- # واجهة تحميل المستندات
- if 'upload_documents' in st.session_state and st.session_state.upload_documents:
- st.markdown("#### تحميل مستندات جديدة")
-
- uploaded_file = st.file_uploader("اختر ملفًا", type=['pdf', 'docx', 'xlsx', 'png', 'jpg', 'dwg'])
- doc_type = st.selectbox("نوع المستند", ["كراسة شروط", "عقد", "مخططات", "جدول كميات", "مواصفات فنية", "تعديلات وملاحق"])
-
- if uploaded_file and st.button("تحميل المستند"):
- # محاكاة تحميل المستند
- with st.spinner("جاري تحميل المستند..."):
- time.sleep(2)
-
- # إنشاء مستند جديد
- new_document = {
- 'filename': uploaded_file.name,
- 'type': doc_type,
- 'upload_date': datetime.now().strftime('%Y-%m-%d'),
- 'size': f"{uploaded_file.size / 1024:.1f} KB"
- }
-
- # إضافة المستند إلى المشروع
- if 'documents' not in project:
- project['documents'] = []
-
- project['documents'].append(new_document)
-
- st.success(f"تم تحميل المستند [{uploaded_file.name}] بنجاح!")
- st.session_state.upload_documents = False
- st.experimental_rerun()
-
- # عرض البنود والكميات
- st.markdown("#### بنود وكميات المشروع")
-
- if 'items' in project and project['items']:
- items_df = pd.DataFrame(project['items'])
- st.dataframe(items_df, use_container_width=True, hide_index=True)
-
- # زر لتحويل البنود إلى وحدة التسعير
- if st.button("تحويل البنود إلى وحدة التسعير"):
- if 'manual_items' not in st.session_state:
- st.session_state.manual_items = pd.DataFrame()
-
- st.session_state.manual_items = items_df.copy()
- st.success("تم تحويل البنود إلى وحدة التسعير بنجاح!")
- else:
- st.info("لا توجد بنود وكميات لهذا المشروع حاليًا.")
-
- # زر استيراد البنود من وحدة تحليل المستندات
- if st.button("استيراد البنود من تحليل المستندات"):
- st.warning("ميزة استيراد البنود من تحليل المستندات قيد التطوير.")
-
- # أزرار الإجراءات
- col1, col2, col3 = st.columns(3)
-
- with col1:
- if st.button("تعديل المشروع"):
- st.session_state.edit_project = True
- st.experimental_rerun()
-
- with col2:
- if st.button("تصدير بيانات المشروع"):
- st.success("تم تصدير بيانات المشروع بنجاح!")
-
- with col3:
- if st.button("إرسال للاعتماد"):
- st.success("تم إرسال المشروع للاعتماد بنجاح!")
-
- # نموذج تعديل المشروع
- if 'edit_project' in st.session_state and st.session_state.edit_project:
- st.markdown("#### تعديل المشروع")
-
- with st.form("edit_project_form"):
- col1, col2 = st.columns(2)
-
- with col1:
- project_name = st.text_input("اسم المشروع", value=project['name'])
- client = st.text_input("الجهة المالكة", value=project['client'])
- location = st.text_input("الموقع", value=project['location'])
- tender_type = st.selectbox(
- "نوع المناقصة",
- ["عامة", "خاصة", "أمر مباشر"],
- index=["عامة", "خاصة", "أمر مباشر"].index(project['tender_type'])
- )
-
- with col2:
- tender_number = st.text_input("رقم المناقصة", value=project['number'])
- submission_date = st.date_input(
- "تاريخ التقديم",
- value=datetime.strptime(project['submission_date'], "%Y-%m-%d") if isinstance(project['submission_date'], str) else project['submission_date']
- )
- pricing_method = st.selectbox(
- "طريقة التسعير",
- ["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
- index=["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"].index(project['pricing_method'])
- )
- status = st.selectbox(
- "حالة المشروع",
- ["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
- index=["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"].index(project['status'])
- )
-
- description = st.text_area("وصف المشروع", value=project.get('description', ''))
-
- col1, col2 = st.columns(2)
-
- with col1:
- submit = st.form_submit_button("حفظ التعديلات")
-
- with col2:
- cancel = st.form_submit_button("إلغاء")
-
- if submit:
- # تحديث بيانات المشروع
- project['name'] = project_name
- project['number'] = tender_number
- project['client'] = client
- project['location'] = location
- project['description'] = description
- project['status'] = status
- project['tender_type'] = tender_type
- project['pricing_method'] = pricing_method
- project['submission_date'] = submission_date
-
- st.success("تم تحديث بيانات المشروع بنجاح!")
- st.session_state.edit_project = False
- st.experimental_rerun()
-
- elif cancel:
- st.session_state.edit_project = False
- st.experimental_rerun()
-
- def _render_projects_tracking_tab(self):
- """عرض تبويب متابعة المشاريع"""
-
- st.markdown("### متابعة المشاريع")
-
- # عرض إحصائيات المشاريع
- col1, col2, col3, col4 = st.columns(4)
-
- projects = st.session_state.projects
-
- with col1:
- total_projects = len(projects)
- self.ui.create_metric_card("إجمالي المشاريع", str(total_projects), None, self.ui.COLORS['primary'])
-
- with col2:
- active_projects = len([p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]])
- self.ui.create_metric_card("المشاريع النشطة", str(active_projects), None, self.ui.COLORS['success'])
-
- with col3:
- pending_submission = len([p for p in projects if p['status'] in ["جديد", "قيد التسعير"]])
- self.ui.create_metric_card("مشاريع قيد التسعير", str(pending_submission), None, self.ui.COLORS['warning'])
-
- with col4:
- completed_projects = len([p for p in projects if p['status'] in ["منتهي"]])
- self.ui.create_metric_card("المشاريع المنتهية", str(completed_projects), None, self.ui.COLORS['info'])
-
- # عرض رسم بياني لحالة المشاريع
- st.markdown("#### توزيع المشاريع حسب الحالة")
-
- status_counts = {}
- for p in projects:
- status = p['status']
- status_counts[status] = status_counts.get(status, 0) + 1
-
- status_df = pd.DataFrame({
- 'الحالة': list(status_counts.keys()),
- 'عدد المشاريع': list(status_counts.values())
- })
-
- st.bar_chart(status_df.set_index('الحالة'))
-
- # عرض المشاريع قيد المتابعة
- st.markdown("#### المشاريع قيد المتابعة")
-
- # عرض المشاريع النشطة المرتبة حسب تاريخ التقديم
- active_projects_list = [p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]]
-
- if active_projects_list:
- # تحويل التواريخ إلى كائنات تاريخ إذا كانت نصوصًا
- for p in active_projects_list:
- if isinstance(p['submission_date'], str):
- p['submission_date'] = datetime.strptime(p['submission_date'], "%Y-%m-%d")
-
- # ترتيب المشاريع حسب تاريخ التقديم
- active_projects_list.sort(key=lambda x: x['submission_date'])
-
- # تحويل إلى DataFrame
- active_df = pd.DataFrame(active_projects_list)
-
- # اختيار وترتيب الأعمدة
- display_columns = [
- 'name', 'number', 'client', 'status',
- 'submission_date', 'tender_type'
- ]
-
- # تغيير أسماء الأعمدة
- column_names = {
- 'name': 'اسم المشروع',
- 'number': 'رقم المناقصة',
- 'client': 'الجهة المالكة',
- 'status': 'الحالة',
- 'submission_date': 'تاريخ التقديم',
- 'tender_type': 'نوع المناقصة'
- }
-
- # تنسيق البيانات
- display_df = active_df[display_columns].rename(columns=column_names)
- display_df['تاريخ التقديم'] = pd.to_datetime(display_df['تاريخ التقديم']).dt.strftime('%Y-%m-%d')
-
- # عرض الجدول
- st.dataframe(display_df, use_container_width=True, hide_index=True)
- else:
- st.info("لا توجد مشاريع نشطة حاليًا.")
-
- # عرض المشاريع المقبلة
- st.markdown("#### المواعيد المقبلة")
-
- upcoming_events = []
- today = datetime.now().date()
-
- for p in projects:
- submission_date = p['submission_date']
- if isinstance(submission_date, str):
- submission_date = datetime.strptime(submission_date, "%Y-%m-%d").date()
- elif isinstance(submission_date, datetime):
- submission_date = submission_date.date()
-
- # المشاريع التي موعد تقديمها خلال الأسبوعين القادمين
- if today <= submission_date <= today + timedelta(days=14) and p['status'] in ["قيد التسعير"]:
- days_left = (submission_date - today).days
- upcoming_events.append({
- 'المشروع': p['name'],
- 'الحدث': 'موعد تقديم المناقصة',
- 'التاريخ': submission_date.strftime('%Y-%m-%d'),
- 'الأيام المتبقية': days_left
- })
-
- if upcoming_events:
- events_df = pd.DataFrame(upcoming_events)
- st.dataframe(events_df, use_container_width=True, hide_index=True)
- else:
- st.info("لا توجد مواعيد قريبة.")
-
- def _generate_sample_projects(self):
- """توليد بيانات افتراضية للمشاريع"""
-
- projects = [
- {
- 'id': 1,
- 'name': "إنشاء مبنى مستشفى الولادة والأطفال بمنطقة الشرقية",
- 'number': "SHPD-2025-001",
- 'client': "وزارة الصحة",
- 'location': "الدمام، المنطقة الشرقية",
- 'description': "يشمل المشروع إنشاء وتجهيز مبنى مستشفى الولادة والأطفال بسعة 300 سرير، ويتكون المبنى من 4 طوابق بمساحة إجمالية 15,000 متر مربع.",
- 'status': "قيد التسعير",
- 'tender_type': "عامة",
- 'pricing_method': "قياسي",
- 'submission_date': (datetime.now() + timedelta(days=5)),
- 'created_at': datetime.now() - timedelta(days=10),
- 'created_by_id': 1,
- 'documents': [
- {
- 'filename': "كراسة الشروط والمواصفات.pdf",
- 'type': "كراسة شروط",
- 'upload_date': (datetime.now() - timedelta(days=9)).strftime('%Y-%m-%d'),
- 'size': "5.2 MB"
- },
- {
- 'filename': "المخططات الهندسية.dwg",
- 'type': "مخططات",
- 'upload_date': (datetime.now() - timedelta(days=8)).strftime('%Y-%m-%d'),
- 'size': "25.7 MB"
- },
- {
- 'filename': "جدول الكميات.xlsx",
- 'type': "جدول كميات",
- 'upload_date': (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'),
- 'size': "1.8 MB"
- }
- ],
- 'items': [
- {
- 'رقم البند': "A1",
- 'وصف البند': "أعمال الحفر والردم",
- 'الوحدة': "م3",
- 'الكمية': 12500
- },
- {
- 'رقم البند': "A2",
- 'وصف البند': "أعمال الخرسانة المسلحة للأساسات",
- 'الوحدة': "م3",
- 'الكمية': 3500
- },
- {
- 'رقم البند': "A3",
- 'وصف البند': "أعمال حديد التسليح",
- 'الوحدة': "طن",
- 'الكمية': 450
- }
- ]
- },
- {
- 'id': 2,
- 'name': "صيانة وتطوير طريق الملك عبدالله",
- 'number': "MOT-2025-042",
- 'client': "وزارة النقل",
- 'location': "الرياض، المنطقة الوسطى",
- 'description': "صيانة وتطوير طريق الملك عبدالله بطول 25 كم، ويشمل المشروع إعادة الرصف وتحسين الإنارة وتركيب اللوحات الإرشادية.",
- 'status': "تم التقديم",
- 'tender_type': "عامة",
- 'pricing_method': "غير متزن",
- 'submission_date': (datetime.now() - timedelta(days=15)),
- 'created_at': datetime.now() - timedelta(days=45),
- 'created_by_id': 1
- },
- {
- 'id': 3,
- 'name': "إنشاء محطة معالجة مياه الصرف الصحي",
- 'number': "SWPC-2025-007",
- 'client': "شركة المياه الوطنية",
- 'location': "جدة، المنطقة الغربية",
- 'description': "إنشاء محطة معالجة مياه الصرف الصحي بطاقة استيعابية 50,000 م3/يوم، مع جميع الأعمال المدنية والكهروميكانيكية.",
- 'status': "تمت الترسية",
- 'tender_type': "عامة",
- 'pricing_method': "قياسي",
- 'submission_date': (datetime.now() - timedelta(days=90)),
- 'created_at': datetime.now() - timedelta(days=120),
- 'created_by_id': 1
- },
- {
- 'id': 4,
- 'name': "إنشاء منتزه الملك سلمان",
- 'number': "RAM-2025-015",
- 'client': "أمانة منطقة الرياض",
- 'location': "الرياض، المنطقة الوسطى",
- 'description': "إنشاء منتزه الملك سلمان على مساحة 500,000 متر مربع، ويشمل المشروع أعمال التشجير والتنسيق والمسطحات المائية والمباني الخدمية.",
- 'status': "قيد التنفيذ",
- 'tender_type': "عامة",
- 'pricing_method': "قياسي",
- 'submission_date': (datetime.now() - timedelta(days=180)),
- 'created_at': datetime.now() - timedelta(days=210),
- 'created_by_id': 1
- },
- {
- 'id': 5,
- 'name': "إنشاء مبنى مختبرات كلية العلوم",
- 'number': "KSU-2025-032",
- 'client': "جامعة الملك سعود",
- 'location': "الرياض، المنطقة الوسطى",
- 'description': "إنشاء مبنى المختبرات الجديد لكلية العلوم بمساحة 8,000 متر مربع، ويتكون من 3 طوابق ويشمل تجهيز المعامل والمختبرات العلمية.",
- 'status': "جديد",
- 'tender_type': "خاصة",
- 'pricing_method': "تنافسي",
- 'submission_date': (datetime.now() + timedelta(days=10)),
- 'created_at': datetime.now() - timedelta(days=5),
- 'created_by_id': 1
- },
- {
- 'id': 6,
- 'name': "توريد وتركيب أنظمة الطاقة الشمسية",
- 'number': "SEC-2025-098",
- 'client': "الشركة السعودية للكهرباء",
- 'location': "تبوك، المنطقة الشمالية",
- 'description': "توريد وتركيب أنظمة الطاقة الشمسية بقدرة 5 ميجاوات، مع جميع الأعمال المدنية والكهربائية.",
- 'status': "جديد",
- 'tender_type': "عامة",
- 'pricing_method': "قياسي",
- 'submission_date': (datetime.now() + timedelta(days=20)),
- 'created_at': datetime.now() - timedelta(days=2),
- 'created_by_id': 1
- }
- ]
-
- return projects
-
-# تشغيل التطبيق
-if __name__ == "__main__":
- projects_app = ProjectsApp()
- projects_app.run()
diff --git a/modules/project_tracker/__init__.py b/modules/project_tracker/__init__.py
deleted file mode 100644
index 3d9b980b1d848af2732c586ff297deb2fdf8995e..0000000000000000000000000000000000000000
--- a/modules/project_tracker/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-# -*- 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
deleted file mode 100644
index 8a304fdc87c1ca5a30ff7a9dc736adaa764da7ed..0000000000000000000000000000000000000000
--- a/modules/project_tracker/status_tracker.py
+++ /dev/null
@@ -1,1740 +0,0 @@
-#!/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
deleted file mode 100644
index 3fa249317afc63249666dbecd7802b8e6f902e94..0000000000000000000000000000000000000000
--- a/modules/project_tracker/tracker_app.py
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/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
deleted file mode 100644
index 4a6c17a73333f06321271be498c9a2e2a175542f..0000000000000000000000000000000000000000
--- a/modules/projects/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# ملف تهيئة حزمة إدارة المشاريع
\ No newline at end of file
diff --git a/modules/projects/projects_app.py b/modules/projects/projects_app.py
deleted file mode 100644
index a2996048f89249e786f202765bc753d054e48c3e..0000000000000000000000000000000000000000
--- a/modules/projects/projects_app.py
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/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
deleted file mode 100644
index 812d74bb3eac0b29d54ef2ff3b3210c749983096..0000000000000000000000000000000000000000
--- a/modules/projects/projects_management.py
+++ /dev/null
@@ -1,942 +0,0 @@
-#!/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
deleted file mode 100644
index 55485214f6d4779aeec220e21326e8f27eccee53..0000000000000000000000000000000000000000
--- a/modules/reports/reports_app.py
+++ /dev/null
@@ -1,88 +0,0 @@
-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_materials_tab()
-
- with tabs[2]:
- self._render_labor_tab()
-
- with tabs[3]:
- self._render_equipment_tab()
-
- with tabs[4]:
- self._render_subcontractors_tab()
-
- with tabs[5]:
- self._render_price_analysis_tab()
-
- def _render_dashboard_tab(self):
- """عرض تبويب لوحة المعلومات"""
-
- st.markdown("### لوحة معلومات إدارة الموارد")
-
- # عرض مؤشرات الأداء الرئيسية
- col1, col2, col3, col4 = st.columns(4)
-
- with col1:
- total_materials = len(st.session_state.materials)
- st.metric("عدد المواد", total_materials)
-
- with col2:
- total_labor = len(st.session_state.labor)
- st.metric("عدد موارد العمالة", total_labor)
-
- with col3:
- total_equipment = len(st.session_state.equipment)
- st.metric("عدد المعدات", total_equipment)
-
- with col4:
- total_subcontractors = len(st.session_state.subcontractors)
- st.metric("عدد المقاولين من الباطن", total_subcontractors)
-
- # رسم بياني لتوزيع المحتوى المحلي
- st.markdown("### المحتوى المحلي للموارد")
-
- # إعداد البيانات
- local_content_data = []
-
- # إضافة بيانات المواد
- for material in st.session_state.materials:
- local_content_data.append({
- 'النوع': 'المواد',
- 'اسم المورد': material['name'],
- 'نسبة المحتوى المحلي': material['local_content']
- })
-
- # إضافة بيانات العمالة
- for labor in st.session_state.labor:
- local_content_data.append({
- 'النوع': 'العمالة',
- 'اسم المورد': labor['name'],
- 'نسبة المحتوى المحلي': labor['local_content']
- })
-
- # إضافة بيانات المعدات
- for equipment in st.session_state.equipment:
- local_content_data.append({
- 'النوع': 'المعدات',
- 'اسم المورد': equipment['name'],
- 'نسبة المحتوى المحلي': equipment['local_content']
- })
-
- # إضافة بيانات المقاولين من الباطن
- for subcontractor in st.session_state.subcontractors:
- local_content_data.append({
- 'النوع': 'المقاولين من الباطن',
- 'اسم المورد': subcontractor['name'],
- 'نسبة المحتوى المحلي': subcontractor['local_content']
- })
-
- # تحويل البيانات إلى DataFrame
- local_content_df = pd.DataFrame(local_content_data)
-
- # حساب متوسط المحتوى المحلي لكل نوع
- avg_local_content = local_content_df.groupby('النوع')['نسبة المحتوى المحلي'].mean().reset_index()
-
- # رسم المخطط الشريطي
- fig = px.bar(
- avg_local_content,
- x='النوع',
- y='نسبة المحتوى المحلي',
- title='متوسط نسبة المحتوى المحلي حسب نوع المورد',
- color='النوع',
- text_auto='.1f'
- )
-
- fig.update_traces(texttemplate='%{text}%', textposition='outside')
-
- 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.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("### تنبيهات الموارد")
-
- # محاكاة تنبيهات الموارد
- 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": "متوسطة"
- }
- ]
-
- # عرض التنبيهات
- 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_materials_tab(self):
- """عرض تبويب المواد"""
-
- st.markdown("### إدارة المواد")
-
- # عرض أدوات البحث والتصفية
- col1, col2 = st.columns(2)
-
- with col1:
- search_query = st.text_input("بحث في المواد", placeholder="ابحث باسم المادة أو الفئة أو المورد...")
-
- with col2:
- category_filter = st.multiselect(
- "تصفية حسب الفئة",
- options=list(set(material['category'] for material in st.session_state.materials)),
- default=[],
- key="material_category_filter_tab"
- )
-
- # تطبيق البحث والتصفية
- filtered_materials = st.session_state.materials
-
- 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())
- ]
-
- if category_filter:
- filtered_materials = [material for material in filtered_materials if material['category'] in category_filter]
-
- # زر إضافة مادة جديدة
- if st.button("إضافة مادة جديدة"):
- st.session_state.show_material_form = 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()
-
- # عرض قائمة المواد
- 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("### إدارة العمالة")
-
- # عرض أدوات البحث والتصفية
- col1, col2 = st.columns(2)
-
- with col1:
- search_query = st.text_input("بحث في العمالة", placeholder="ابحث باسم العامل أو الفئة أو المورد...")
-
- with col2:
- category_filter = st.multiselect(
- "تصفية حسب الفئة",
- options=list(set(labor['category'] for labor in st.session_state.labor)),
- default=[],
- key="labor_category_filter_tab"
- )
-
- # تطبيق البحث والتصفية
- filtered_labor = st.session_state.labor
-
- 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())
- ]
-
- if category_filter:
- filtered_labor = [labor for labor in filtered_labor if labor['category'] in category_filter]
-
- # زر إضافة عامل جديد
- if st.button("إضافة عامل جديد"):
- st.session_state.show_labor_form = 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()
-
- # عرض قائمة العمالة
- 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("### إدارة المعدات")
-
- # عرض أدوات البحث والتصفية
- col1, col2 = st.columns(2)
-
- with col1:
- search_query = st.text_input("بحث في المعدات", placeholder="ابحث باسم المعدة أو الفئة أو المورد...")
-
- with col2:
- category_filter = st.multiselect(
- "تصفية حسب الفئة",
- options=list(set(equipment['category'] for equipment in st.session_state.equipment)),
- default=[],
- key="equipment_category_filter_tab"
- )
-
- # تطبيق البحث والتصفية
- filtered_equipment = st.session_state.equipment
-
- 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())
- ]
-
- if category_filter:
- filtered_equipment = [equipment for equipment in filtered_equipment if equipment['category'] in category_filter]
-
- # زر إضافة معدة جديدة
- if st.button("إضافة معدة جديدة"):
- st.session_state.show_equipment_form = True
-
- # نموذج إضافة معدة جديدة
- 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()
-
- # عرض قائمة المعدات
- 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:
- st.metric("إجمالي عدد المعدات", len(filtered_equipment))
-
- with col2:
- 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}%")
-
- # عرض مخطط توزيع المعدات حسب الفئة
- category_counts = equipment_df.groupby('category').size().reset_index(name='count')
-
- 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_subcontractors_tab(self):
- """عرض تبويب المقاولين من الباطن"""
-
- st.markdown("### إدارة المقاولين من الباطن")
-
- # عرض أدوات البحث والتصفية
- col1, col2, col3 = st.columns(3)
-
- with col1:
- search_query = st.text_input("بحث في المقاولين", placeholder="ابحث باسم المقاول أو التخصص...")
-
- with col2:
- category_filter = st.multiselect(
- "تصفية حسب الفئة",
- options=list(set(subcontractor['category'] for subcontractor in st.session_state.subcontractors)),
- default=[],
- key="subcontractor_category_filter_tab"
- )
-
- with col3:
- city_filter = st.multiselect(
- "تصفية حسب المدينة",
- options=list(set(subcontractor['city'] for subcontractor in st.session_state.subcontractors)),
- default=[],
- key="subcontractor_city_filter_tab"
- )
-
- # تطبيق البحث والتصفية
- filtered_subcontractors = st.session_state.subcontractors
-
- 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())
- ]
-
- if category_filter:
- filtered_subcontractors = [subcontractor for subcontractor in filtered_subcontractors if subcontractor['category'] in category_filter]
-
- if city_filter:
- filtered_subcontractors = [subcontractor for subcontractor in filtered_subcontractors if subcontractor['city'] in city_filter]
-
- # زر إضافة مقاول جديد
- if st.button("إضافة مقاول جديد"):
- st.session_state.show_subcontractor_form = True
-
- # نموذج إضافة مقاول جديد
- 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()
-
- # عرض قائمة المقاولين
- 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:
- st.metric("إجمالي عدد المقاولين", len(filtered_subcontractors))
-
- with col2:
- avg_rating = sum(subcontractor['rating'] for subcontractor in filtered_subcontractors) / len(filtered_subcontractors)
- st.metric("متوسط التقييم", f"{avg_rating:.1f}/5.0")
-
- with col3:
- avg_local_content = sum(subcontractor['local_content'] for subcontractor in filtered_subcontractors) / len(filtered_subcontractors)
- st.metric("متوسط نسبة المحتوى المحلي", f"{avg_local_content:.1f}%")
-
- # عرض مخطط توزيع المقاولين حسب الفئة
- category_counts = subcontractors_df.groupby('category').size().reset_index(name='count')
-
- fig = px.pie(
- category_counts,
- names='category',
- values='count',
- title='توزيع المقاولين حسب الفئة',
- hole=0.4
- )
-
- 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):
- """عرض تبويب تحليل الأسعار"""
-
- st.markdown("### تحليل الأسعار")
-
- # اختيار نوع التحليل
- analysis_type = st.radio(
- "نوع التحليل",
- ["تحليل أسعار المواد", "مقارنة الأسعار", "توقع الأسعار المستقبلية"],
- horizontal=True
- )
-
- 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"
- )
-
- if not selected_materials:
- st.warning("الرجاء اختيار مادة واحدة على الأقل للتحليل.")
- return
-
- # إعداد البيانات للتحليل
- 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]
-
- # التحقق من وجود بيانات سعرية في 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)
-
- # حساب التغيرات في الأسعار
- materials_price_changes = []
-
- 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(
- changes_df,
- x='المادة',
- y='نسبة التغير (%)',
- title='نسبة التغير في الأسعار',
- color='المادة',
- text_auto='.1f'
- )
-
- fig.update_traces(texttemplate='%{text}%', textposition='outside')
-
- st.plotly_chart(fig, use_container_width=True)
-
- def _render_price_comparison(self):
- """عرض مقارنة الأسعار"""
-
- st.markdown("#### مقارنة الأسعار")
-
- # اختيار نوع المورد للمقارنة
- resource_type = st.selectbox(
- "نوع المورد",
- ["المواد", "العمالة", "المعدات"]
- )
-
- if resource_type == "المواد":
- resources = st.session_state.materials
- elif resource_type == "العمالة":
- resources = st.session_state.labor
- else:
- resources = st.session_state.equipment
-
- # اختيار الفئة للمقارنة
- categories = list(set([resource['category'] for resource in resources]))
- selected_category = st.selectbox(
- "الفئة",
- options=["الكل"] + categories
- )
-
- # فلترة الموارد حسب الفئة
- 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']
- })
-
- # تحويل البيانات إلى DataFrame
- comparison_df = pd.DataFrame(comparison_data)
-
- # عرض المخطط الشريطي للأسعار
- fig = px.bar(
- comparison_df,
- x='الاسم',
- y='السعر',
- title=f'مقارنة أسعار {resource_type}',
- color='الفئة' if selected_category == "الكل" else 'المورد',
- text_auto='.2s',
- labels={'السعر': 'السعر (ريال)'}
- )
-
- fig.update_traces(texttemplate='%{text} ريال', textposition='outside')
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض العلاقة بين السعر ونسبة المحتوى المحلي
- fig = px.scatter(
- comparison_df,
- x='نسبة المحتوى المحلي',
- y='السعر',
- color='الفئة' if selected_category == "الكل" else None,
- title='العلاقة بين السعر ونسبة المحتوى المحلي',
- labels={'نسبة المحتوى المحلي': 'نسبة المحتوى المحلي (%)', 'السعر': 'السعر (ريال)'},
- size=[50] * len(comparison_df),
- text='الاسم'
- )
-
- fig.update_traces(textposition='top center')
-
- st.plotly_chart(fig, use_container_width=True)
-
- # عرض جدول المقارنة
- st.markdown("#### جدول مقارنة الأسعار")
-
- # تنسيق البيانات للعرض
- display_df = comparison_df.copy()
- display_df['السعر'] = display_df['السعر'].apply(lambda x: f"{x:,.2f} ريال")
- display_df['نسبة المحتوى المحلي'] = display_df['نسبة المحتوى المحلي'].apply(lambda x: f"{x}%")
-
- st.dataframe(display_df, use_container_width=True, hide_index=True)
-
- def _render_price_forecast(self):
- """عرض توقع الأسعار المستقبلية"""
-
- st.markdown("#### توقع الأسعار المستقبلية")
-
- # اختيار المادة للتوقع
- material_options = [material['name'] for material in st.session_state.materials]
- selected_material = st.selectbox(
- "اختر المادة للتوقع",
- options=material_options
- )
-
- # اختيار فترة التوقع
- forecast_period = st.slider(
- "فترة التوقع (أشهر)",
- min_value=1,
- max_value=12,
- value=6
- )
-
- 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
-
- if not price_history_data:
- st.warning("لا توجد بيانات تاريخية كافية للمادة المحددة للقيام بالتوقع.")
- return
-
- # تحويل البيانات إلى DataFrame
- price_history_df = pd.DataFrame(price_history_data).sort_values('date')
-
- # إجراء التوقع
- # في الواقع، ستستخدم نماذج تعلم آلي مثل ARIMA أو Prophet
- # هنا سنستخدم توقعًا بسيطًا للأغراض التوضيحية
-
- # حساب متوسط التغير الشهري
- 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'])
-
- if monthly_changes:
- avg_monthly_change = sum(monthly_changes) / len(monthly_changes)
- else:
- avg_monthly_change = 0
-
- # إنشاء بيانات التوقع
- last_date = price_history_df['date'].max()
- last_price = price_history_df.loc[price_history_df['date'] == last_date, 'price'].values[0]
-
- 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)]
-
- # إضافة بعض التقلبات العشوائية للتوقع
- forecast_prices = [price + random.uniform(-price*0.05, price*0.05) for price in forecast_prices]
-
- forecast_df = pd.DataFrame({
- 'date': forecast_dates,
- 'price': forecast_prices,
- 'type': ['توقع'] * forecast_period
- })
-
- # دمج البيانات التاريخية والتوقع
- 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'}
- )
-
- # إضافة فترة الثقة حول التوقع
- 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
- )
-
- 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("#### جدول توقع الأسعار")
-
- 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("#### ملخص التوقع")
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- st.metric(
- "السعر الحالي",
- f"{last_price:,.2f} ريال"
- )
-
- with col2:
- 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}%"
- )
-
- with col3:
- avg_forecasted_price = sum(forecast_prices) / len(forecast_prices)
-
- st.metric(
- "متوسط السعر المتوقع",
- f"{avg_forecasted_price:,.2f} ريال"
- )
-
- # عرض ملاحظات وتوصيات
- if price_change_percent > 10:
- st.warning("""
- ### توقع ارتفاع كبير في الأسعار
- - ينصح بشراء المواد مبكراً وتخزينها إذا أمكن
- - التفاوض على عقود توريد طويلة الأجل بأسعار ثابتة
- - البحث عن موردين بديلين أو مواد بديلة
- """)
- elif price_change_percent < -10:
- st.success("""
- ### توقع انخفاض كبير في الأسعار
- - ينصح بتأجيل شراء المواد إذا أمكن
- - شراء كميات أقل والاحتفاظ بمخزون منخفض
- - التفاوض على عقود مرنة مع الموردين
- """)
- else:
- st.info("""
- ### توقع استقرار نسبي في الأسعار
- - يمكن الشراء حسب الاحتياج دون الحاجة لتخزين كميات كبيرة
- - متابعة أسعار السوق بشكل دوري للتأكد من دقة التوقعات
- """)
\ No newline at end of file
diff --git a/modules/risk_analysis/__pycache__/risk_analyzer.cpython-310.pyc b/modules/risk_analysis/__pycache__/risk_analyzer.cpython-310.pyc
deleted file mode 100644
index 7b1a75f3dbce97445a57f3b0ae6cfd9c4a3765e7..0000000000000000000000000000000000000000
Binary files a/modules/risk_analysis/__pycache__/risk_analyzer.cpython-310.pyc and /dev/null differ
diff --git a/modules/risk_analysis/risk_analysis_app.py b/modules/risk_analysis/risk_analysis_app.py
deleted file mode 100644
index 95b2a921305467e214946d3ad8852ac662c8175c..0000000000000000000000000000000000000000
--- a/modules/risk_analysis/risk_analysis_app.py
+++ /dev/null
@@ -1,751 +0,0 @@
-"""
-تطبيق وحدة تحليل المخاطر
-"""
-
-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_analysis/risk_analyzer.py b/modules/risk_analysis/risk_analyzer.py
deleted file mode 100644
index e373f833c742ca681e81f787725669297b34e05a..0000000000000000000000000000000000000000
--- a/modules/risk_analysis/risk_analyzer.py
+++ /dev/null
@@ -1,1154 +0,0 @@
-"""
-وحدة تحليل المخاطر لنظام إدارة المناقصات - Hybrid Face
-"""
-
-import os
-import logging
-import threading
-import datetime
-import json
-import math
-import streamlit as st
-from pathlib import Path
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-import sys
-
-# إضافة مسار المشروع للنظام
-sys.path.append(str(Path(__file__).parent.parent))
-
-# استيراد محسن واجهة المستخدم
-from styling.enhanced_ui import UIEnhancer
-
-# تهيئة السجل
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger('risk_analysis')
-
-class RiskAnalyzer:
- """محلل المخاطر"""
-
- def __init__(self, config=None, db=None):
- """تهيئة محلل المخاطر"""
- self.config = config
- self.db = db
- self.analysis_in_progress = False
- self.current_project = None
- self.analysis_results = {}
-
- # إنشاء مجلد التحليل إذا لم يكن موجوداً
- if config and hasattr(config, 'EXPORTS_PATH'):
- self.exports_path = Path(config.EXPORTS_PATH)
- else:
- self.exports_path = Path('data/exports')
-
- if not self.exports_path.exists():
- self.exports_path.mkdir(parents=True, exist_ok=True)
-
- def analyze_risks(self, project_id, method="comprehensive", callback=None):
- """تحليل مخاطر المشروع"""
- if self.analysis_in_progress:
- logger.warning("هناك عملية تحليل مخاطر جارية بالفعل")
- return False
-
- self.analysis_in_progress = True
- self.current_project = project_id
- self.analysis_results = {
- "project_id": project_id,
- "method": method,
- "analysis_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- "status": "جاري التحليل",
- "identified_risks": [],
- "risk_categories": {},
- "risk_matrix": {},
- "mitigation_strategies": [],
- "summary": {}
- }
-
- # بدء التحليل في خيط منفصل
- thread = threading.Thread(
- target=self._analyze_risks_thread,
- args=(project_id, method, callback)
- )
- thread.daemon = True
- thread.start()
-
- return True
-
- def _analyze_risks_thread(self, project_id, method, callback):
- """خيط تحليل المخاطر"""
- try:
- # محاكاة جلب بيانات المشروع من قاعدة البيانات
- project_data = self._get_project_data(project_id)
-
- if not project_data:
- logger.error(f"لم يتم العثور على بيانات المشروع: {project_id}")
- self.analysis_results["status"] = "فشل التحليل"
- self.analysis_results["error"] = "لم يتم العثور على بيانات المشروع"
- return
-
- # تحديد المخاطر
- self._identify_risks(project_data, method)
-
- # تصنيف المخاطر
- self._categorize_risks()
-
- # إنشاء مصفوفة المخاطر
- self._create_risk_matrix()
-
- # تطوير استراتيجيات التخفيف
- self._develop_mitigation_strategies(method)
-
- # إنشاء ملخص التحليل
- self._create_analysis_summary(method)
-
- # تحديث حالة التحليل
- self.analysis_results["status"] = "اكتمل التحليل"
- self.analysis_results["analysis_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
-
- logger.info(f"اكتمل تحليل مخاطر المشروع: {project_id}")
-
- except Exception as e:
- logger.error(f"خطأ في تحليل مخاطر المشروع: {str(e)}")
- self.analysis_results["status"] = "فشل التحليل"
- self.analysis_results["error"] = str(e)
-
- finally:
- self.analysis_in_progress = False
-
- # استدعاء دالة الاستجابة إذا تم توفيرها
- if callback and callable(callback):
- callback(self.analysis_results)
-
- def _get_project_data(self, project_id):
- """الحصول على بيانات المشروع"""
- # في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات
- # هنا نقوم بمحاكاة البيانات للتوضيح
-
- return {
- "id": project_id,
- "name": "مشروع الطرق السريعة",
- "client": "وزارة النقل",
- "description": "إنشاء طرق سريعة بطول 50 كم في المنطقة الشرقية",
- "start_date": "2025-05-01",
- "end_date": "2025-11-30",
- "status": "تخطيط",
- "budget": 50000000,
- "location": "المنطقة الشرقية",
- "project_type": "بنية تحتية",
- "complexity": "متوسط",
- "existing_risks": [
- {"id": 1, "name": "تأخر توريد المواد", "probability": "متوسط", "impact": "عالي", "category": "توريد"},
- {"id": 2, "name": "تغير أسعار المواد", "probability": "عالي", "impact": "عالي", "category": "مالي"},
- {"id": 3, "name": "ظروف جوية غير مواتية", "probability": "منخفض", "impact": "متوسط", "category": "بيئي"},
- {"id": 4, "name": "نقص العمالة", "probability": "متوسط", "impact": "متوسط", "category": "موارد بشرية"}
- ]
- }
-
- def _identify_risks(self, project_data, method):
- """تحديد المخاطر"""
- # دمج المخاطر الموجودة
- identified_risks = []
- for risk in project_data["existing_risks"]:
- identified_risks.append({
- "id": risk["id"],
- "name": risk["name"],
- "description": f"مخاطر {risk['name']} في المشروع",
- "category": risk["category"],
- "probability": risk["probability"],
- "impact": risk["impact"],
- "risk_score": self._calculate_risk_score(risk["probability"], risk["impact"]),
- "source": "existing"
- })
-
- # إضافة مخاطر إضافية بناءً على نوع المشروع وموقعه وتعقيده
- additional_risks = self._generate_additional_risks(project_data, method)
- identified_risks.extend(additional_risks)
-
- # تخزين المخاطر المحددة
- self.analysis_results["identified_risks"] = identified_risks
-
- def _generate_additional_risks(self, project_data, method):
- """توليد مخاطر إضافية بناءً على بيانات المشروع"""
- additional_risks = []
-
- # مخاطر مرتبطة بنوع المشروع
- if project_data["project_type"] == "بنية تحتية":
- additional_risks.extend([
- {
- "id": 101,
- "name": "مشاكل جيوتقنية",
- "description": "مشاكل غير متوقعة في التربة أو الظروف الجيولوجية",
- "category": "فني",
- "probability": "متوسط",
- "impact": "عالي",
- "risk_score": self._calculate_risk_score("متوسط", "عالي"),
- "source": "generated"
- },
- {
- "id": 102,
- "name": "تعارض مع مرافق قائمة",
- "description": "تعارض أعمال الحفر مع خطوط المرافق القائمة (كهرباء، مياه، اتصالات)",
- "category": "فني",
- "probability": "متوسط",
- "impact": "متوسط",
- "risk_score": self._calculate_risk_score("متوسط", "متوسط"),
- "source": "generated"
- }
- ])
-
- # مخاطر مرتبطة بالموقع
- if project_data["location"] == "المنطقة الشرقية":
- additional_risks.extend([
- {
- "id": 201,
- "name": "ارتفاع درجات الحرارة",
- "description": "تأثير ارتفاع درجات الحرارة على إنتاجية العمل وجودة المواد",
- "category": "بيئي",
- "probability": "عالي",
- "impact": "متوسط",
- "risk_score": self._calculate_risk_score("عالي", "متوسط"),
- "source": "generated"
- },
- {
- "id": 202,
- "name": "رطوبة عالية",
- "description": "تأثير الرطوبة العالية على جودة المواد وتقنيات البناء",
- "category": "بيئي",
- "probability": "عالي",
- "impact": "منخفض",
- "risk_score": self._calculate_risk_score("عالي", "منخفض"),
- "source": "generated"
- }
- ])
-
- # مخاطر مرتبطة بتعقيد المشروع
- if project_data["complexity"] in ["متوسط", "عالي"]:
- additional_risks.extend([
- {
- "id": 301,
- "name": "تغييرات في نطاق العمل",
- "description": "طلبات تغيير من العميل أو تعديلات في متطلبات المشروع",
- "category": "إداري",
- "probability": "عالي",
- "impact": "عالي",
- "risk_score": self._calculate_risk_score("عالي", "عالي"),
- "source": "generated"
- },
- {
- "id": 302,
- "name": "تأخر الموافقات",
- "description": "تأخر الحصول على الموافقات والتصاريح اللازمة",
- "category": "تنظيمي",
- "probability": "متوسط",
- "impact": "عالي",
- "risk_score": self._calculate_risk_score("متوسط", "عالي"),
- "source": "generated"
- }
- ])
-
- # إضافة مخاطر إضافية إذا كانت طريقة التحليل شاملة
- if method == "comprehensive":
- additional_risks.extend([
- {
- "id": 401,
- "name": "مخاطر سياسية",
- "description": "تغييرات في السياسات الحكومية أو اللوائح التنظيمية",
- "category": "خارجي",
- "probability": "منخفض",
- "impact": "عالي",
- "risk_score": self._calculate_risk_score("منخفض", "عالي"),
- "source": "generated"
- },
- {
- "id": 402,
- "name": "مخاطر اقتصادية",
- "description": "تقلبات في أسعار العملات أو التضخم",
- "category": "مالي",
- "probability": "متوسط",
- "impact": "متوسط",
- "risk_score": self._calculate_risk_score("متوسط", "متوسط"),
- "source": "generated"
- },
- {
- "id": 403,
- "name": "مخاطر تقنية",
- "description": "مشاكل في التقنيات الجديدة أو المعدات",
- "category": "فني",
- "probability": "متوسط",
- "impact": "متوسط",
- "risk_score": self._calculate_risk_score("متوسط", "متوسط"),
- "source": "generated"
- }
- ])
-
- return additional_risks
-
- def _calculate_risk_score(self, probability, impact):
- """حساب درجة المخاطرة"""
- # تحويل القيم النصية إلى قيم رقمية
- probability_values = {"منخفض": 1, "متوسط": 2, "عالي": 3}
- impact_values = {"منخفض": 1, "متوسط": 2, "عالي": 3}
-
- # حساب درجة المخاطرة
- p_value = probability_values.get(probability, 1)
- i_value = impact_values.get(impact, 1)
-
- return p_value * i_value
-
- def _categorize_risks(self):
- """تصنيف المخاطر"""
- categories = {}
-
- for risk in self.analysis_results["identified_risks"]:
- category = risk["category"]
- if category not in categories:
- categories[category] = []
-
- categories[category].append(risk)
-
- # حساب إحصائيات لكل فئة
- for category, risks in categories.items():
- total_score = sum(risk["risk_score"] for risk in risks)
- avg_score = total_score / len(risks) if risks else 0
- max_score = max(risk["risk_score"] for risk in risks) if risks else 0
-
- categories[category] = {
- "risks": risks,
- "count": len(risks),
- "total_score": total_score,
- "avg_score": avg_score,
- "max_score": max_score
- }
-
- self.analysis_results["risk_categories"] = categories
-
- def _create_risk_matrix(self):
- """إنشاء مصفوفة المخاطر"""
- matrix = {
- "high_impact": {"high_prob": [], "medium_prob": [], "low_prob": []},
- "medium_impact": {"high_prob": [], "medium_prob": [], "low_prob": []},
- "low_impact": {"high_prob": [], "medium_prob": [], "low_prob": []}
- }
-
- for risk in self.analysis_results["identified_risks"]:
- impact = risk["impact"].lower()
- probability = risk["probability"].lower()
-
- impact_key = f"{impact}_impact"
- if impact == "عالي":
- impact_key = "high_impact"
- elif impact == "متوسط":
- impact_key = "medium_impact"
- else:
- impact_key = "low_impact"
-
- prob_key = f"{probability}_prob"
- if probability == "عالي":
- prob_key = "high_prob"
- elif probability == "متوسط":
- prob_key = "medium_prob"
- else:
- prob_key = "low_prob"
-
- if impact_key in matrix and prob_key in matrix[impact_key]:
- matrix[impact_key][prob_key].append(risk)
-
- self.analysis_results["risk_matrix"] = matrix
-
- def _develop_mitigation_strategies(self, method):
- """تطوير استراتيجيات التخفيف"""
- strategies = []
-
- # استراتيجيات للمخاطر ذات الأولوية العالية
- high_priority_risks = []
-
- # المخاطر ذات التأثير العالي واحتمالية عالية
- high_priority_risks.extend(self.analysis_results["risk_matrix"]["high_impact"]["high_prob"])
-
- # المخاطر ذات التأثير العالي واحتمالية متوسطة
- high_priority_risks.extend(self.analysis_results["risk_matrix"]["high_impact"]["medium_prob"])
-
- # المخاطر ذات التأثير المتوسط واحتمالية عالية
- high_priority_risks.extend(self.analysis_results["risk_matrix"]["medium_impact"]["high_prob"])
-
- for risk in high_priority_risks:
- strategy = self._generate_mitigation_strategy(risk)
- strategies.append(strategy)
-
- # إذا كانت طريقة التحليل شاملة، أضف استراتيجيات للمخاطر ذات الأولوية المتوسطة
- if method == "comprehensive":
- medium_priority_risks = []
-
- # المخاطر ذات التأثير العالي واحتمالية منخفضة
- medium_priority_risks.extend(self.analysis_results["risk_matrix"]["high_impact"]["low_prob"])
-
- # المخاطر ذات التأثير المتوسط واحتمالية متوسطة
- medium_priority_risks.extend(self.analysis_results["risk_matrix"]["medium_impact"]["medium_prob"])
-
- # المخاطر ذات التأثير المنخفض واحتمالية عالية
- medium_priority_risks.extend(self.analysis_results["risk_matrix"]["low_impact"]["high_prob"])
-
- for risk in medium_priority_risks:
- strategy = self._generate_mitigation_strategy(risk)
- strategies.append(strategy)
-
- self.analysis_results["mitigation_strategies"] = strategies
-
- def _generate_mitigation_strategy(self, risk):
- """توليد استراتيجية تخفيف للمخاطر"""
- strategy_templates = {
- "توريد": [
- "إنشاء قائمة بموردين بديلين",
- "التعاقد المسبق مع الموردين",
- "تخزين المواد الحرجة مسبقًا",
- "وضع خطة للتوريد المرحلي"
- ],
- "مالي": [
- "تضمين بند تعديل الأسعار في العقود",
- "تخصيص ميزانية احتياطية",
- "التأمين ضد المخاطر المالية",
- "تحديث دراسة الجدوى بانتظام"
- ],
- "بيئي": [
- "وضع خطة للطوارئ البيئية",
- "جدولة الأنشطة الحرجة في المواسم المناسبة",
- "توفير معدات حماية إضافية",
- "تطبيق تقنيات مقاومة للظروف البيئية"
- ],
- "موارد بشرية": [
- "التعاقد مع شركات توظيف إضافية",
- "تدريب فرق العمل على مهام متعددة",
- "وضع خطة للحوافز والمكافآت",
- "تطوير برنامج للاحتفاظ بالموظفين"
- ],
- "فني": [
- "إجراء اختبارات إضافية قبل التنفيذ",
- "الاستعانة بخبراء متخصصين",
- "تطبيق منهجية مراجعة التصميم",
- "إعداد خطط بديلة للحلول التقنية"
- ],
- "إداري": [
- "تطبيق إجراءات إدارة التغيير",
- "عقد اجتماعات دورية مع أصحاب المصلحة",
- "توثيق متطلبات المشروع بشكل تفصيلي",
- "تحديد نطاق العمل بوضوح في العقود"
- ],
- "تنظيمي": [
- "التواصل المبكر مع الجهات التنظيمية",
- "تعيين مستشار قانوني متخصص",
- "متابعة التحديثات التنظيمية بانتظام",
- "تخصيص وقت إضافي للحصول على الموافقات"
- ],
- "خارجي": [
- "متابعة التطورات السياسية والاقتصادية",
- "وضع خطط بديلة للسيناريوهات المختلفة",
- "التأمين ضد المخاطر الخارجية",
- "تنويع مصادر التوريد والتمويل"
- ]
- }
-
- # اختيار استراتيجيات مناسبة بناءً على فئة المخاطر
- category = risk["category"]
- templates = strategy_templates.get(category, strategy_templates["إداري"])
-
- # اختيار استراتيجية عشوائية من القائمة
- import random
- strategy_text = random.choice(templates)
-
- return {
- "risk_id": risk["id"],
- "risk_name": risk["name"],
- "strategy": strategy_text,
- "priority": "عالية" if risk["risk_score"] >= 6 else "متوسطة" if risk["risk_score"] >= 3 else "منخفضة",
- "responsible": "مدير المشروع",
- "timeline": "قبل بدء المشروع"
- }
-
- def _create_analysis_summary(self, method):
- """إنشاء ملخص التحليل"""
- total_risks = len(self.analysis_results["identified_risks"])
- high_risks = len([r for r in self.analysis_results["identified_risks"] if r["risk_score"] >= 6])
- medium_risks = len([r for r in self.analysis_results["identified_risks"] if 3 <= r["risk_score"] < 6])
- low_risks = len([r for r in self.analysis_results["identified_risks"] if r["risk_score"] < 3])
-
- # حساب توزيع المخاطر حسب الفئة
- category_distribution = {}
- for risk in self.analysis_results["identified_risks"]:
- category = risk["category"]
- if category not in category_distribution:
- category_distribution[category] = 0
- category_distribution[category] += 1
-
- # حساب متوسط درجة المخاطرة
- avg_risk_score = sum(risk["risk_score"] for risk in self.analysis_results["identified_risks"]) / total_risks if total_risks > 0 else 0
-
- # إنشاء الملخص
- summary = {
- "total_risks": total_risks,
- "high_risks": high_risks,
- "medium_risks": medium_risks,
- "low_risks": low_risks,
- "category_distribution": category_distribution,
- "avg_risk_score": avg_risk_score,
- "analysis_method": method,
- "recommendations": self._generate_recommendations(high_risks, medium_risks, low_risks, category_distribution)
- }
-
- self.analysis_results["summary"] = summary
-
- def _generate_recommendations(self, high_risks, medium_risks, low_risks, category_distribution):
- """توليد توصيات بناءً على نتائج التحليل"""
- recommendations = []
-
- # توصيات بناءً على عدد المخاطر العالية
- if high_risks > 3:
- recommendations.append("يجب إجراء مراجعة شاملة لخطة المشروع نظرًا لوجود عدد كبير من المخاطر عالية الخطورة.")
-
- if high_risks > 0:
- recommendations.append("تطوير خطط استجابة تفصيلية لجميع المخاطر عالية الخطورة.")
-
- # توصيات بناءً على توزيع المخاطر حسب الفئة
- max_category = max(category_distribution.items(), key=lambda x: x[1], default=(None, 0))
- if max_category[0]:
- recommendations.append(f"التركيز على إدارة مخاطر فئة '{max_category[0]}' حيث تمثل النسبة الأكبر من المخاطر المحددة.")
-
- # توصيات عامة
- recommendations.append("إجراء مراجعات دورية لسجل المخاطر وتحديثه بانتظام.")
- recommendations.append("تعيين مسؤولين محددين لمتابعة استراتيجيات التخفيف من المخاطر.")
-
- return recommendations
-
- def get_analysis_results(self):
- """الحصول على نتائج التحليل"""
- return self.analysis_results
-
- def export_analysis_results(self, format="json"):
- """تصدير نتائج التحليل"""
- if not self.analysis_results:
- logger.warning("لا توجد نتائج تحليل للتصدير")
- return None
-
- timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
- project_id = self.analysis_results.get("project_id", "unknown")
-
- if format == "json":
- filename = f"risk_analysis_{project_id}_{timestamp}.json"
- filepath = self.exports_path / filename
-
- with open(filepath, 'w', encoding='utf-8') as f:
- json.dump(self.analysis_results, f, ensure_ascii=False, indent=4)
-
- logger.info(f"تم تصدير نتائج التحليل إلى: {filepath}")
- return filepath
-
- else:
- logger.error(f"تنسيق التصدير غير مدعوم: {format}")
- return None
-
-
-class RiskAnalysisApp:
- """تطبيق تحليل المخاطر"""
-
- def __init__(self):
- """تهيئة تطبيق تحليل المخاطر"""
- self.ui = UIEnhancer(page_title="تحليل المخاطر - نظام تحليل المناقصات", page_icon="⚠️")
- self.ui.apply_theme_colors()
- self.risk_analyzer = RiskAnalyzer()
-
- # تهيئة بيانات المشاريع
- if 'projects' not in st.session_state:
- st.session_state.projects = self._generate_sample_projects()
-
- # تهيئة نتائج التحليل
- if 'risk_analysis_results' not in st.session_state:
- st.session_state.risk_analysis_results = {}
-
- 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": "translate"},
- {"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._render_dashboard_tab()
-
- with tabs[1]:
- self._render_analysis_tab()
-
- with tabs[2]:
- self._render_risk_register_tab()
-
- with tabs[3]:
- self._render_risk_matrix_tab()
-
- with tabs[4]:
- self._render_mitigation_strategies_tab()
-
- def _render_dashboard_tab(self):
- """عرض تبويب لوحة المعلومات"""
-
- st.markdown("### لوحة معلومات تحليل المخاطر")
-
- # عرض إحصائيات المخاطر
- col1, col2, col3, col4 = st.columns(4)
-
- # الحصول على إحصائيات المخاطر
- total_risks = 0
- high_risks = 0
- medium_risks = 0
- low_risks = 0
-
- for project_id, results in st.session_state.risk_analysis_results.items():
- if "identified_risks" in results:
- project_risks = results["identified_risks"]
- total_risks += len(project_risks)
- high_risks += len([r for r in project_risks if r["risk_score"] >= 6])
- medium_risks += len([r for r in project_risks if 3 <= r["risk_score"] < 6])
- low_risks += len([r for r in project_risks if r["risk_score"] < 3])
-
- with col1:
- self.ui.create_metric_card("إجمالي المخاطر", str(total_risks), None, self.ui.COLORS['primary'])
-
- with col2:
- self.ui.create_metric_card("مخاطر عالية", str(high_risks), None, self.ui.COLORS['danger'])
-
- with col3:
- self.ui.create_metric_card("مخاطر متوسطة", str(medium_risks), None, self.ui.COLORS['warning'])
-
- with col4:
- self.ui.create_metric_card("مخاطر منخفضة", str(low_risks), None, self.ui.COLORS['success'])
-
- # عرض توزيع المخاطر حسب الفئة
- st.markdown("#### توزيع المخاطر حسب الفئة")
-
- # جمع بيانات توزيع المخاطر
- category_distribution = {}
-
- for project_id, results in st.session_state.risk_analysis_results.items():
- if "identified_risks" in results:
- for risk in results["identified_risks"]:
- category = risk["category"]
- if category not in category_distribution:
- category_distribution[category] = 0
- category_distribution[category] += 1
-
- if category_distribution:
- # تحويل البيانات إلى DataFrame
- category_df = pd.DataFrame({
- 'الفئة': list(category_distribution.keys()),
- 'عدد المخاطر': list(category_distribution.values())
- })
-
- # عرض الرسم البياني
- st.bar_chart(category_df.set_index('الفئة'))
- else:
- st.info("لا توجد بيانات كافية لعرض توزيع المخاطر.")
-
- # عرض المشاريع ذات المخاطر العالية
- st.markdown("#### المشاريع ذات المخاطر العالية")
-
- high_risk_projects = []
-
- for project_id, results in st.session_state.risk_analysis_results.items():
- if "identified_risks" in results:
- project_high_risks = len([r for r in results["identified_risks"] if r["risk_score"] >= 6])
- if project_high_risks > 0:
- # البحث عن بيانات المشروع
- project = next((p for p in st.session_state.projects if p["id"] == int(project_id)), None)
- if project:
- high_risk_projects.append({
- 'اسم المشروع': project["name"],
- 'رقم المناقصة': project["number"],
- 'الجهة المالكة': project["client"],
- 'عدد المخاطر العالية': project_high_risks
- })
-
- if high_risk_projects:
- high_risk_df = pd.DataFrame(high_risk_projects)
- st.dataframe(high_risk_df, use_container_width=True, hide_index=True)
- else:
- st.info("لا توجد مشاريع ذات مخاطر عالية حاليًا.")
-
- def _render_analysis_tab(self):
- """عرض تبويب تحليل المخاطر"""
-
- st.markdown("### تحليل مخاطر المشروع")
-
- # اختيار المشروع
- project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects]
- selected_project = st.selectbox("اختر المشروع", project_options)
-
- if selected_project:
- # استخراج معرف المشروع من الاختيار
- project_index = project_options.index(selected_project)
- project = st.session_state.projects[project_index]
- project_id = project["id"]
-
- # عرض معلومات المشروع
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown(f"**اسم المشروع**: {project['name']}")
- st.markdown(f"**رقم المناقصة**: {project['number']}")
- st.markdown(f"**الجهة المالكة**: {project['client']}")
-
- with col2:
- st.markdown(f"**الموقع**: {project['location']}")
- st.markdown(f"**نوع المشروع**: {project['project_type']}")
- st.markdown(f"**مستوى التعقيد**: {project['complexity']}")
-
- # اختيار طريقة التحليل
- analysis_method = st.radio(
- "طريقة التحليل",
- ["أساسي", "شامل"],
- format_func=lambda x: "تحليل أساسي" if x == "أساسي" else "تحليل شامل"
- )
-
- # زر بدء التحليل
- if st.button("بدء تحليل المخاطر"):
- with st.spinner("جاري تحليل مخاطر المشروع..."):
- # محاكاة وقت المعالجة
- import time
- time.sleep(2)
-
- # إجراء تحليل المخاطر
- self.risk_analyzer.analyze_risks(project_id, method="comprehensive" if analysis_method == "شامل" else "basic")
-
- # الحصول على نتائج التحليل
- results = self.risk_analyzer.get_analysis_results()
-
- # تخزين النتائج في حالة الجلسة
- st.session_state.risk_analysis_results[str(project_id)] = results
-
- st.success("تم الانتهاء من تحليل المخاطر بنجاح!")
- st.experimental_rerun()
-
- # عرض نتائج التحليل إذا كانت متوفرة
- if str(project_id) in st.session_state.risk_analysis_results:
- results = st.session_state.risk_analysis_results[str(project_id)]
-
- st.markdown("#### ملخص نتائج التحليل")
-
- if "summary" in results:
- summary = results["summary"]
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- self.ui.create_metric_card("إجمالي المخاطر", str(summary["total_risks"]), None, self.ui.COLORS['primary'])
-
- with col2:
- self.ui.create_metric_card("مخاطر عالية", str(summary["high_risks"]), None, self.ui.COLORS['danger'])
-
- with col3:
- self.ui.create_metric_card("مخاطر متوسطة", str(summary["medium_risks"]), None, self.ui.COLORS['warning'])
-
- # عرض توزيع المخاطر حسب الفئة
- st.markdown("#### توزيع المخاطر حسب الفئة")
-
- if "category_distribution" in summary:
- category_df = pd.DataFrame({
- 'الفئة': list(summary["category_distribution"].keys()),
- 'عدد المخاطر': list(summary["category_distribution"].values())
- })
-
- st.bar_chart(category_df.set_index('الفئة'))
-
- # عرض التوصيات
- st.markdown("#### التوصيات")
-
- if "recommendations" in summary:
- for i, recommendation in enumerate(summary["recommendations"]):
- st.markdown(f"{i+1}. {recommendation}")
-
- # زر تصدير النتائج
- if st.button("تصدير نتائج التحليل"):
- with st.spinner("جاري تصدير النتائج..."):
- # محاكاة وقت المعالجة
- time.sleep(1)
-
- # تصدير النتائج
- export_path = self.risk_analyzer.export_analysis_results()
-
- if export_path:
- st.success(f"تم تصدير نتائج التحليل بنجاح!")
- else:
- st.error("حدث خطأ أثناء تصدير النتائج.")
-
- def _render_risk_register_tab(self):
- """عرض تبويب سجل المخاطر"""
-
- st.markdown("### سجل المخاطر")
-
- # اختيار المشروع
- project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects]
- project_options.insert(0, "جميع المشاريع")
- selected_project_option = st.selectbox("اختر المشروع", project_options, key="risk_register_project")
-
- # جمع المخاطر المحددة
- all_risks = []
-
- if selected_project_option == "جميع المشاريع":
- # جمع المخاطر من جميع المشاريع
- for project_id, results in st.session_state.risk_analysis_results.items():
- if "identified_risks" in results:
- project = next((p for p in st.session_state.projects if p["id"] == int(project_id)), None)
- project_name = project["name"] if project else f"مشروع {project_id}"
-
- for risk in results["identified_risks"]:
- risk_copy = risk.copy()
- risk_copy["project_name"] = project_name
- all_risks.append(risk_copy)
- else:
- # استخراج معرف المشروع من الاختيار
- project_index = project_options.index(selected_project_option) - 1 # -1 لأننا أضفنا "جميع المشاريع" في البداية
- project = st.session_state.projects[project_index]
- project_id = project["id"]
-
- # جمع المخاطر من المشروع المحدد
- if str(project_id) in st.session_state.risk_analysis_results:
- results = st.session_state.risk_analysis_results[str(project_id)]
- if "identified_risks" in results:
- for risk in results["identified_risks"]:
- risk_copy = risk.copy()
- risk_copy["project_name"] = project["name"]
- all_risks.append(risk_copy)
-
- # فلترة المخاطر
- col1, col2, col3 = st.columns(3)
-
- with col1:
- category_filter = st.multiselect(
- "فئة المخاطر",
- list(set(risk["category"] for risk in all_risks)) if all_risks else [],
- key="risk_register_category"
- )
-
- with col2:
- probability_filter = st.multiselect(
- "الاحتمالية",
- ["عالي", "متوسط", "منخفض"],
- key="risk_register_probability"
- )
-
- with col3:
- impact_filter = st.multiselect(
- "التأثير",
- ["عالي", "متوسط", "منخفض"],
- key="risk_register_impact"
- )
-
- # تطبيق الفلترة
- filtered_risks = all_risks
-
- if category_filter:
- filtered_risks = [risk for risk in filtered_risks if risk["category"] in category_filter]
-
- if probability_filter:
- filtered_risks = [risk for risk in filtered_risks if risk["probability"] in probability_filter]
-
- if impact_filter:
- filtered_risks = [risk for risk in filtered_risks if risk["impact"] in impact_filter]
-
- # عرض سجل المخاطر
- if filtered_risks:
- # تحويل المخاطر إلى DataFrame
- risk_data = []
- for risk in filtered_risks:
- risk_data.append({
- 'المشروع': risk.get("project_name", ""),
- 'اسم المخاطرة': risk["name"],
- 'الوصف': risk.get("description", ""),
- 'الفئة': risk["category"],
- 'الاحتمالية': risk["probability"],
- 'التأثير': risk["impact"],
- 'درجة المخاطرة': risk["risk_score"]
- })
-
- risk_df = pd.DataFrame(risk_data)
-
- # ترتيب المخاطر حسب درجة المخاطرة (تنازليًا)
- risk_df = risk_df.sort_values(by='درجة المخاطرة', ascending=False)
-
- # عرض الجدول
- st.dataframe(risk_df, use_container_width=True, hide_index=True)
-
- # زر تصدير سجل المخاطر
- if st.button("تصدير سجل المخاطر"):
- with st.spinner("جاري تصدير سجل المخاطر..."):
- # محاكاة وقت المعالجة
- time.sleep(1)
- st.success("تم تصدير سجل المخاطر بنجاح!")
- else:
- st.info("لا توجد مخاطر تطابق معايير البحث أو لم يتم إجراء تحليل للمخاطر بعد.")
-
- def _render_risk_matrix_tab(self):
- """عرض تبويب مصفوفة المخاطر"""
-
- st.markdown("### مصفوفة المخاطر")
-
- # اختيار المشروع
- project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects]
- selected_project = st.selectbox("اختر المشروع", project_options, key="risk_matrix_project")
-
- if selected_project:
- # استخراج معرف المشروع من الاختيار
- project_index = project_options.index(selected_project)
- project = st.session_state.projects[project_index]
- project_id = project["id"]
-
- # التحقق من وجود نتائج تحليل للمشروع
- if str(project_id) in st.session_state.risk_analysis_results:
- results = st.session_state.risk_analysis_results[str(project_id)]
-
- if "risk_matrix" in results:
- matrix = results["risk_matrix"]
-
- # إنشاء مصفوفة المخاطر
- st.markdown("#### مصفوفة احتمالية وتأثير المخاطر")
-
- # إنشاء بيانات المصفوفة
- matrix_data = [
- [len(matrix["high_impact"]["high_prob"]), len(matrix["high_impact"]["medium_prob"]), len(matrix["high_impact"]["low_prob"])],
- [len(matrix["medium_impact"]["high_prob"]), len(matrix["medium_impact"]["medium_prob"]), len(matrix["medium_impact"]["low_prob"])],
- [len(matrix["low_impact"]["high_prob"]), len(matrix["low_impact"]["medium_prob"]), len(matrix["low_impact"]["low_prob"])]
- ]
-
- # تحويل البيانات إلى DataFrame
- matrix_df = pd.DataFrame(
- matrix_data,
- columns=["احتمالية عالية", "احتمالية متوسطة", "احتمالية منخفضة"],
- index=["تأثير عالي", "تأثير متوسط", "تأثير منخفض"]
- )
-
- # عرض المصفوفة كجدول
- st.dataframe(matrix_df)
-
- # عرض تفاصيل المخاطر في كل خلية
- st.markdown("#### تفاصيل المخاطر في المصفوفة")
-
- # إنشاء تبويبات للخلايا المختلفة
- matrix_tabs = st.tabs([
- "تأثير عالي / احتمالية عالية",
- "تأثير عالي / احتمالية متوسطة",
- "تأثير متوسط / احتمالية عالية",
- "تأثير متوسط / احتمالية متوسطة",
- "أخرى"
- ])
-
- # عرض المخاطر في كل تبويب
- with matrix_tabs[0]:
- self._display_cell_risks(matrix["high_impact"]["high_prob"], "تأثير عالي / احتمالية عالية")
-
- with matrix_tabs[1]:
- self._display_cell_risks(matrix["high_impact"]["medium_prob"], "تأثير عالي / احتمالية متوسطة")
-
- with matrix_tabs[2]:
- self._display_cell_risks(matrix["medium_impact"]["high_prob"], "تأثير متوسط / احتمالية عالية")
-
- with matrix_tabs[3]:
- self._display_cell_risks(matrix["medium_impact"]["medium_prob"], "تأثير متوسط / احتمالية متوسطة")
-
- with matrix_tabs[4]:
- # جمع المخاطر الأخرى
- other_risks = []
- other_risks.extend(matrix["high_impact"]["low_prob"])
- other_risks.extend(matrix["medium_impact"]["low_prob"])
- other_risks.extend(matrix["low_impact"]["high_prob"])
- other_risks.extend(matrix["low_impact"]["medium_prob"])
- other_risks.extend(matrix["low_impact"]["low_prob"])
-
- self._display_cell_risks(other_risks, "مخاطر أخرى")
- else:
- st.warning("لم يتم العثور على مصفوفة المخاطر للمشروع المحدد.")
- else:
- st.warning("لم يتم إجراء تحليل للمخاطر لهذا المشروع بعد.")
-
- def _display_cell_risks(self, risks, cell_title):
- """عرض المخاطر في خلية من مصفوفة المخاطر"""
-
- if risks:
- st.markdown(f"##### {cell_title} ({len(risks)} مخاطر)")
-
- # تحويل المخاطر إلى DataFrame
- risk_data = []
- for risk in risks:
- risk_data.append({
- 'اسم المخاطرة': risk["name"],
- 'الوصف': risk.get("description", ""),
- 'الفئة': risk["category"],
- 'درجة المخاطرة': risk["risk_score"]
- })
-
- risk_df = pd.DataFrame(risk_data)
-
- # عرض الجدول
- st.dataframe(risk_df, use_container_width=True, hide_index=True)
- else:
- st.info(f"لا توجد مخاطر في خلية {cell_title}.")
-
- def _render_mitigation_strategies_tab(self):
- """عرض تبويب استراتيجيات التخفيف"""
-
- st.markdown("### استراتيجيات التخفيف من المخاطر")
-
- # اختيار المشروع
- project_options = [f"{p['name']} ({p['number']})" for p in st.session_state.projects]
- selected_project = st.selectbox("اختر المشروع", project_options, key="mitigation_project")
-
- if selected_project:
- # استخراج معرف المشروع من الاختيار
- project_index = project_options.index(selected_project)
- project = st.session_state.projects[project_index]
- project_id = project["id"]
-
- # التحقق من وجود نتائج تحليل للمشروع
- if str(project_id) in st.session_state.risk_analysis_results:
- results = st.session_state.risk_analysis_results[str(project_id)]
-
- if "mitigation_strategies" in results and results["mitigation_strategies"]:
- strategies = results["mitigation_strategies"]
-
- # فلترة الاستراتيجيات
- priority_filter = st.multiselect(
- "الأولوية",
- ["عالية", "متوسطة", "منخفضة"],
- default=["عالية"],
- key="mitigation_priority"
- )
-
- # تطبيق الفلترة
- filtered_strategies = strategies
- if priority_filter:
- filtered_strategies = [s for s in filtered_strategies if s["priority"] in priority_filter]
-
- # عرض استراتيجيات التخفيف
- if filtered_strategies:
- # تحويل الاستراتيجيات إلى DataFrame
- strategy_data = []
- for strategy in filtered_strategies:
- strategy_data.append({
- 'المخاطرة': strategy["risk_name"],
- 'استراتيجية التخفيف': strategy["strategy"],
- 'الأولوية': strategy["priority"],
- 'المسؤول': strategy["responsible"],
- 'الإطار الزمني': strategy["timeline"]
- })
-
- strategy_df = pd.DataFrame(strategy_data)
-
- # ترتيب الاستراتيجيات حسب الأولوية
- priority_order = {"عالية": 0, "متوسطة": 1, "منخفضة": 2}
- strategy_df["priority_order"] = strategy_df["الأولوية"].map(priority_order)
- strategy_df = strategy_df.sort_values(by="priority_order")
- strategy_df = strategy_df.drop(columns=["priority_order"])
-
- # عرض الجدول
- st.dataframe(strategy_df, use_container_width=True, hide_index=True)
-
- # زر تصدير استراتيجيات التخفيف
- if st.button("تصدير استراتيجيات التخفيف"):
- with st.spinner("جاري تصدير استراتيجيات التخفيف..."):
- # محاكاة وقت المعالجة
- time.sleep(1)
- st.success("تم تصدير استراتيجيات التخفيف بنجاح!")
- else:
- st.info("لا توجد استراتيجيات تخفيف تطابق معايير الفلترة.")
- else:
- st.warning("لم يتم العثور على استراتيجيات تخفيف للمشروع المحدد.")
- else:
- st.warning("لم يتم إجراء تحليل للمخاطر لهذا المشروع بعد.")
-
- def _generate_sample_projects(self):
- """توليد بيانات افتراضية للمشاريع"""
-
- return [
- {
- 'id': 1,
- 'name': "إنشاء مبنى مستشفى الولادة والأطفال بمنطقة الشرقية",
- 'number': "SHPD-2025-001",
- 'client': "وزارة الصحة",
- 'location': "الدمام، المنطقة الشرقية",
- 'description': "يشمل المشروع إنشاء وتجهيز مبنى مستشفى الولادة والأطفال بسعة 300 سرير، ويتكون المبنى من 4 طوابق بمساحة إجمالية 15,000 متر مربع.",
- 'status': "قيد التسعير",
- 'tender_type': "عامة",
- 'pricing_method': "قياسي",
- 'submission_date': (datetime.datetime.now() + datetime.timedelta(days=5)),
- 'created_at': datetime.datetime.now() - datetime.timedelta(days=10),
- 'created_by_id': 1,
- 'project_type': "مباني",
- 'complexity': "عالي"
- },
- {
- 'id': 2,
- 'name': "صيانة وتطوير طريق الملك عبدالله",
- 'number': "MOT-2025-042",
- 'client': "وزارة النقل",
- 'location': "الرياض، المنطقة الوسطى",
- 'description': "صيانة وتطوير طريق الملك عبدالله بطول 25 كم، ويشمل المشروع إعادة الرصف وتحسين الإنارة وتركيب اللوحات الإرشادية.",
- 'status': "تم التقديم",
- 'tender_type': "عامة",
- 'pricing_method': "غير متزن",
- 'submission_date': (datetime.datetime.now() - datetime.timedelta(days=15)),
- 'created_at': datetime.datetime.now() - datetime.timedelta(days=45),
- 'created_by_id': 1,
- 'project_type': "بنية تحتية",
- 'complexity': "متوسط"
- },
- {
- 'id': 3,
- 'name': "إنشاء محطة معالجة مياه الصرف الصحي",
- 'number': "SWPC-2025-007",
- 'client': "شركة المياه الوطنية",
- 'location': "جدة، المنطقة الغربية",
- 'description': "إنشاء محطة معالجة مياه الصرف الصحي بطاقة استيعابية 50,000 م3/يوم، مع جميع الأعمال المدنية والكهروميكانيكية.",
- 'status': "تمت الترسية",
- 'tender_type': "عامة",
- 'pricing_method': "قياسي",
- 'submission_date': (datetime.datetime.now() - datetime.timedelta(days=90)),
- 'created_at': datetime.datetime.now() - datetime.timedelta(days=120),
- 'created_by_id': 1,
- 'project_type': "بنية تحتية",
- 'complexity': "عالي"
- }
- ]
-
-# تشغيل التطبيق
-if __name__ == "__main__":
- risk_app = RiskAnalysisApp()
- risk_app.run()
diff --git a/modules/risk_assessment/__init__.py b/modules/risk_assessment/__init__.py
deleted file mode 100644
index 7a0af0cba3e41d42a6aff7f8b31f4a81b3ecdfdb..0000000000000000000000000000000000000000
--- a/modules/risk_assessment/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-# -*- 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
deleted file mode 100644
index acf6398e185ff827b156e9ee43629c65c5a77b49..0000000000000000000000000000000000000000
--- a/modules/risk_assessment/contract_risk_analyzer.py
+++ /dev/null
@@ -1,2055 +0,0 @@
-#!/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
deleted file mode 100644
index 1d812640eab14ddbb993fa89fa183073c02524dd..0000000000000000000000000000000000000000
--- a/modules/risk_assessment/risk_assessment_app.py
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/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
deleted file mode 100644
index cafaf3fcf4d1c4108c9272c7841e1cc9194b3cec..0000000000000000000000000000000000000000
--- a/modules/services/item_extractor.py
+++ /dev/null
@@ -1,124 +0,0 @@
-"""
-خدمة استخراج البنود من المستندات
-"""
-
-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
deleted file mode 100644
index 19b3c9d1427c49cfffd7da8237854efc74f8d128..0000000000000000000000000000000000000000
--- a/modules/services/quantity_extractor.py
+++ /dev/null
@@ -1,182 +0,0 @@
-"""
-خدمة استخراج الكميات من المستندات
-"""
-
-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
deleted file mode 100644
index 55eb9bfc21ca5cad78c518053bda36399bda70e1..0000000000000000000000000000000000000000
--- a/modules/services/risk_analyzer.py
+++ /dev/null
@@ -1,219 +0,0 @@
-"""
-خدمة تحليل المخاطر في المستندات
-"""
-
-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
deleted file mode 100644
index a1a128a36b9da7207e8c5196c5c7685dc2c8741f..0000000000000000000000000000000000000000
--- a/modules/services/specs_analyzer.py
+++ /dev/null
@@ -1,364 +0,0 @@
-"""
-خدمة تحليل المواصفات من المستندات
-"""
-
-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
deleted file mode 100644
index e042bda0c9777a5319c3160c4b21d4864e8ff5ba..0000000000000000000000000000000000000000
--- a/modules/services/text_extractor.py
+++ /dev/null
@@ -1,90 +0,0 @@
-"""
-خدمة استخراج النصوص من المستندات
-"""
-
-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/translation/translation_app.py b/modules/translation/translation_app.py
deleted file mode 100644
index 7eac1aa02f855b7a6a59041834ce72a461ad6314..0000000000000000000000000000000000000000
--- a/modules/translation/translation_app.py
+++ /dev/null
@@ -1,936 +0,0 @@
-"""
-وحدة الترجمة - نظام تحليل المناقصات
-"""
-
-import streamlit as st
-import pandas as pd
-import numpy as np
-import os
-import sys
-from pathlib import Path
-import re
-import datetime
-
-# إضافة مسار المشروع للنظام
-sys.path.append(str(Path(__file__).parent.parent))
-
-# استيراد محسن واجهة المستخدم
-from styling.enhanced_ui import UIEnhancer
-
-class TranslationApp:
- """تطبيق الترجمة"""
-
- def __init__(self):
- """تهيئة تطبيق الترجمة"""
- self.ui = UIEnhancer(page_title="الترجمة - نظام تحليل المناقصات", page_icon="🌐")
- self.ui.apply_theme_colors()
-
- # قائمة اللغات المدعومة
- self.supported_languages = {
- "ar": "العربية",
- "en": "الإنجليزية",
- "fr": "الفرنسية",
- "de": "الألمانية",
- "es": "الإسبانية",
- "it": "الإيطالية",
- "zh": "الصينية",
- "ja": "اليابانية",
- "ru": "الروسية",
- "tr": "التركية"
- }
-
- # بيانات نموذجية للمصطلحات الفنية
- self.technical_terms = [
- {"ar": "كراسة الشروط", "en": "Terms and Conditions Document", "category": "مستندات"},
- {"ar": "جدول الكميات", "en": "Bill of Quantities (BOQ)", "category": "مستندات"},
- {"ar": "المواصفات الفنية", "en": "Technical Specifications", "category": "مستندات"},
- {"ar": "ضمان ابتدائي", "en": "Bid Bond", "category": "ضمانات"},
- {"ar": "ضمان حسن التنفيذ", "en": "Performance Bond", "category": "ضمانات"},
- {"ar": "ضمان دفعة مقدمة", "en": "Advance Payment Guarantee", "category": "ضمانات"},
- {"ar": "ضمان صيانة", "en": "Maintenance Bond", "category": "ضمانات"},
- {"ar": "مناقصة عامة", "en": "Public Tender", "category": "أنواع المناقصات"},
- {"ar": "مناقصة محدودة", "en": "Limited Tender", "category": "أنواع المناقصات"},
- {"ar": "منافسة", "en": "Competition", "category": "أنواع المناقصات"},
- {"ar": "أمر شراء", "en": "Purchase Order", "category": "عقود"},
- {"ar": "عقد إطاري", "en": "Framework Agreement", "category": "عقود"},
- {"ar": "عقد زمني", "en": "Time-based Contract", "category": "عقود"},
- {"ar": "عقد تسليم مفتاح", "en": "Turnkey Contract", "category": "عقود"},
- {"ar": "مقاول من الباطن", "en": "Subcontractor", "category": "أطراف"},
- {"ar": "استشاري", "en": "Consultant", "category": "أطراف"},
- {"ar": "مالك المشروع", "en": "Project Owner", "category": "أطراف"},
- {"ar": "مدير المشروع", "en": "Project Manager", "category": "أطراف"},
- {"ar": "مهندس الموقع", "en": "Site Engineer", "category": "أطراف"},
- {"ar": "مراقب الجودة", "en": "Quality Control", "category": "أطراف"},
- {"ar": "أعمال مدنية", "en": "Civil Works", "category": "أعمال"},
- {"ar": "أعمال كهربائية", "en": "Electrical Works", "category": "أعمال"},
- {"ar": "أعمال ميكانيكية", "en": "Mechanical Works", "category": "أعمال"},
- {"ar": "أعمال معمارية", "en": "Architectural Works", "category": "أعمال"},
- {"ar": "أعمال تشطيبات", "en": "Finishing Works", "category": "أعمال"},
- {"ar": "غرامة تأخير", "en": "Delay Penalty", "category": "شروط"},
- {"ar": "مدة التنفيذ", "en": "Execution Period", "category": "شروط"},
- {"ar": "فترة الضمان", "en": "Warranty Period", "category": "شروط"},
- {"ar": "شروط الدفع", "en": "Payment Terms", "category": "شروط"},
- {"ar": "تسوية النزاعات", "en": "Dispute Resolution", "category": "شروط"}
- ]
-
- # بيانات نموذجية للمستندات المترجمة
- self.translated_documents = [
- {
- "id": "TD001",
- "name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
- "source_language": "ar",
- "target_language": "en",
- "original_file": "specs_v2.0_ar.pdf",
- "translated_file": "specs_v2.0_en.pdf",
- "translation_date": "2025-03-15",
- "translated_by": "أحمد محمد",
- "status": "مكتمل",
- "pages": 52,
- "related_entity": "T-2025-001"
- },
- {
- "id": "TD002",
- "name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
- "source_language": "ar",
- "target_language": "en",
- "original_file": "boq_v1.1_ar.xlsx",
- "translated_file": "boq_v1.1_en.xlsx",
- "translation_date": "2025-02-25",
- "translated_by": "سارة عبدالله",
- "status": "مكتمل",
- "pages": 22,
- "related_entity": "T-2025-001"
- },
- {
- "id": "TD003",
- "name": "المخططات - مناقصة إنشاء مبنى إداري",
- "source_language": "ar",
- "target_language": "en",
- "original_file": "drawings_v2.0_ar.pdf",
- "translated_file": "drawings_v2.0_en.pdf",
- "translation_date": "2025-03-20",
- "translated_by": "محمد علي",
- "status": "مكتمل",
- "pages": 35,
- "related_entity": "T-2025-001"
- },
- {
- "id": "TD004",
- "name": "كراسة الشروط - مناقصة صيانة طرق",
- "source_language": "ar",
- "target_language": "en",
- "original_file": "specs_v1.1_ar.pdf",
- "translated_file": "specs_v1.1_en.pdf",
- "translation_date": "2025-03-25",
- "translated_by": "فاطمة أحمد",
- "status": "مكتمل",
- "pages": 34,
- "related_entity": "T-2025-002"
- },
- {
- "id": "TD005",
- "name": "جدول الكميات - مناقصة صيانة طرق",
- "source_language": "ar",
- "target_language": "en",
- "original_file": "boq_v1.0_ar.xlsx",
- "translated_file": "boq_v1.0_en.xlsx",
- "translation_date": "2025-03-10",
- "translated_by": "خالد عمر",
- "status": "مكتمل",
- "pages": 15,
- "related_entity": "T-2025-002"
- },
- {
- "id": "TD006",
- "name": "كراسة الشروط - مناقصة توريد معدات",
- "source_language": "en",
- "target_language": "ar",
- "original_file": "specs_v1.0_en.pdf",
- "translated_file": "specs_v1.0_ar.pdf",
- "translation_date": "2025-02-15",
- "translated_by": "أحمد محمد",
- "status": "مكتمل",
- "pages": 28,
- "related_entity": "T-2025-003"
- },
- {
- "id": "TD007",
- "name": "عقد توريد - مناقصة توريد معدات",
- "source_language": "en",
- "target_language": "ar",
- "original_file": "contract_v1.0_en.pdf",
- "translated_file": "contract_v1.0_ar.pdf",
- "translation_date": "2025-03-05",
- "translated_by": "سارة عبدالله",
- "status": "مكتمل",
- "pages": 20,
- "related_entity": "T-2025-003"
- },
- {
- "id": "TD008",
- "name": "كراسة الشروط - مناقصة تجهيز مختبرات",
- "source_language": "ar",
- "target_language": "en",
- "original_file": "specs_v1.0_ar.pdf",
- "translated_file": "specs_v1.0_en.pdf",
- "translation_date": "2025-03-28",
- "translated_by": "محمد علي",
- "status": "قيد التنفيذ",
- "pages": 30,
- "related_entity": "T-2025-004"
- }
- ]
-
- # بيانات نموذجية للنصوص المترجمة
- self.sample_translations = {
- "text1": {
- "ar": """
- # كراسة الشروط والمواصفات
- ## مناقصة إنشاء مبنى إداري
-
- ### 1. مقدمة
- تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
-
- ### 2. نطاق العمل
- يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 6 طوابق بمساحة إجمالية 6000 متر مربع، ويشمل ذلك:
- - أعمال الهيكل الإنشائي
- - أعمال التشطيبات الداخلية والخارجية
- - أعمال الكهرباء والميكانيكا
- - أعمال تنسيق الموقع
- - أعمال أنظمة الأمن والسلامة
- - أعمال أنظمة المباني الذكية
- """,
-
- "en": """
- # Terms and Conditions Document
- ## Administrative Building Construction Tender
-
- ### 1. Introduction
- Peninsula Contracting Company invites specialized companies to submit their offers for the implementation of an administrative building construction project in Riyadh.
-
- ### 2. Scope of Work
- The scope of work includes the design and implementation of a 6-floor administrative building with a total area of 6000 square meters, including:
- - Structural works
- - Interior and exterior finishing works
- - Electrical and mechanical works
- - Site coordination works
- - Security and safety systems works
- - Smart building systems works
- """
- },
-
- "text2": {
- "ar": """
- ### 3. المواصفات الفنية
- #### 3.1 أعمال الخرسانة
- - يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 40 نيوتن/مم²
- - يجب استخدام حديد تسليح مطابق للمواصفات السعودية
- - يجب استخدام إضافات للخرسانة لزيادة مقاومتها للعوامل الجوية
-
- #### 3.2 أعمال التشطيبات
- - يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
- - يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
- - يجب استخدام زجاج عاكس للحرارة للواجهات
- - يجب استخدام مواد صديقة للبيئة
- """,
-
- "en": """
- ### 3. Technical Specifications
- #### 3.1 Concrete Works
- - Reinforced concrete must have a strength of not less than 40 Newton/mm²
- - Reinforcement steel must comply with Saudi specifications
- - Concrete additives must be used to increase its resistance to weather conditions
-
- #### 3.2 Finishing Works
- - High-quality materials must be used for interior finishes
- - Exterior facades must be weather-resistant
- - Heat-reflective glass must be used for facades
- - Environmentally friendly materials must be used
- """
- }
- }
-
- 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": "translate"},
- {"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.translate_text()
-
- # علامة تبويب ترجمة المستندات
- with tabs[1]:
- self.translate_documents()
-
- # علامة تبويب قاموس المصطلحات
- with tabs[2]:
- self.technical_terms_dictionary()
-
- # علامة تبويب المستندات المترجمة
- with tabs[3]:
- self.show_translated_documents()
-
- def translate_text(self):
- """ترجمة النصوص"""
- 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=0 # العربية كلغة افتراضية
- )
-
- with col2:
- # استبعاد لغة المصدر من خيارات لغة الهدف
- target_languages = {k: v for k, v in self.supported_languages.items() if k != source_language}
- target_language = st.selectbox(
- "لغة الهدف",
- options=list(target_languages.keys()),
- format_func=lambda x: self.supported_languages[x],
- index=0 # أول لغة متاحة
- )
-
- # خيارات الترجمة
- st.markdown("#### خيارات الترجمة")
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- translation_engine = st.radio(
- "محرك الترجمة",
- options=["OpenAI", "Google Translate", "Microsoft Translator", "محلي"]
- )
-
- with col2:
- use_technical_terms = st.checkbox("استخدام قاموس المصطلحات الفنية", value=True)
-
- with col3:
- preserve_formatting = st.checkbox("الحفاظ على التنسيق", value=True)
-
- # إدخال النص المراد ترجمته
- st.markdown("#### النص المراد ترجمته")
-
- # إضافة أمثلة نصية
- examples = st.expander("أمثلة نصية")
- with examples:
- if st.button("مثال 1: مقدمة كراسة الشروط"):
- source_text = self.sample_translations["text1"][source_language] if source_language in self.sample_translations["text1"] else self.sample_translations["text1"]["ar"]
- elif st.button("مثال 2: المواصفات الفنية"):
- source_text = self.sample_translations["text2"][source_language] if source_language in self.sample_translations["text2"] else self.sample_translations["text2"]["ar"]
- else:
- source_text = ""
-
- if "source_text" not in locals():
- source_text = ""
-
- source_text = st.text_area(
- "أدخل النص المراد ترجمته",
- value=source_text,
- height=200
- )
-
- # زر الترجمة
- if st.button("ترجمة النص", use_container_width=True):
- if not source_text:
- st.error("يرجى إدخال النص المراد ترجمته")
- else:
- # في تطبيق حقيقي، سيتم استدعاء واجهة برمجة التطبيقات للترجمة
- # هنا نستخدم النصوص النموذجية المحددة مسبقاً للعرض
-
- with st.spinner("جاري الترجمة..."):
- # محاكاة تأخير الترجمة
- import time
- time.sleep(1)
-
- # التحقق من وجود ترجمة نموذجية
- if source_language == "ar" and target_language == "en" and source_text.strip() in [self.sample_translations["text1"]["ar"].strip(), self.sample_translations["text2"]["ar"].strip()]:
- if source_text.strip() == self.sample_translations["text1"]["ar"].strip():
- translated_text = self.sample_translations["text1"]["en"]
- else:
- translated_text = self.sample_translations["text2"]["en"]
- elif source_language == "en" and target_language == "ar" and source_text.strip() in [self.sample_translations["text1"]["en"].strip(), self.sample_translations["text2"]["en"].strip()]:
- if source_text.strip() == self.sample_translations["text1"]["en"].strip():
- translated_text = self.sample_translations["text1"]["ar"]
- else:
- translated_text = self.sample_translations["text2"]["ar"]
- else:
- # ترجمة نموذجية للعرض فقط
- translated_text = f"[هذا نص مترجم نموذجي من {self.supported_languages[source_language]} إلى {self.supported_languages[target_language]}]\n\n{source_text}"
-
- # عرض النص المترجم
- st.markdown("#### النص المترجم")
- st.text_area(
- "النص المترجم",
- value=translated_text,
- height=200
- )
-
- # أزرار إضافية
- col1, col2, col3 = st.columns(3)
-
- with col1:
- if st.button("نسخ النص المترجم", use_container_width=True):
- st.success("تم نسخ النص المترجم إلى الحافظة")
-
- with col2:
- if st.button("حفظ الترجمة", use_container_width=True):
- st.success("تم حفظ الترجمة بنجاح")
-
- with col3:
- if st.button("تصدير كملف", use_container_width=True):
- st.success("تم تصدير الترجمة كملف بنجاح")
-
- # عرض إحصائيات الترجمة
- st.markdown("#### إحصائيات الترجمة")
-
- col1, col2, col3, col4 = st.columns(4)
-
- with col1:
- self.ui.create_metric_card(
- "عدد الكلمات",
- str(len(source_text.split())),
- None,
- self.ui.COLORS['primary']
- )
-
- with col2:
- self.ui.create_metric_card(
- "عدد الأحرف",
- str(len(source_text)),
- None,
- self.ui.COLORS['secondary']
- )
-
- with col3:
- self.ui.create_metric_card(
- "وقت الترجمة",
- "1.2 ثانية",
- None,
- self.ui.COLORS['success']
- )
-
- with col4:
- self.ui.create_metric_card(
- "المصطلحات الفنية",
- "5",
- None,
- self.ui.COLORS['accent']
- )
-
- def translate_documents(self):
- """ترجمة المستندات"""
- 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=0, # العربية كلغة افتراضية
- key="doc_source_lang"
- )
-
- with col2:
- # استبعاد لغة المصدر من خيارات لغة الهدف
- target_languages = {k: v for k, v in self.supported_languages.items() if k != source_language}
- target_language = st.selectbox(
- "لغة الهدف",
- options=list(target_languages.keys()),
- format_func=lambda x: self.supported_languages[x],
- index=0, # أول لغة متاحة
- key="doc_target_lang"
- )
-
- # تحميل المستند
- st.markdown("#### تحميل المستند")
-
- uploaded_file = st.file_uploader("اختر المستند المراد ترجمته", type=["pdf", "docx", "xlsx", "txt"])
-
- if uploaded_file is not None:
- st.success(f"تم تحميل الملف: {uploaded_file.name}")
-
- # عرض معلومات الملف
- file_details = {
- "اسم الملف": uploaded_file.name,
- "نوع الملف": uploaded_file.type,
- "حجم الملف": f"{uploaded_file.size / 1024:.1f} كيلوبايت"
- }
-
- st.json(file_details)
-
- # خيارات الترجمة
- st.markdown("#### خيارات الترجمة")
-
- col1, col2 = st.columns(2)
-
- with col1:
- translation_engine = st.radio(
- "محرك الترجمة",
- options=["OpenAI", "Google Translate", "Microsoft Translator", "محلي"],
- key="doc_engine"
- )
-
- use_technical_terms = st.checkbox("استخدام قاموس المصطلحات الفنية", value=True, key="doc_terms")
-
- with col2:
- preserve_formatting = st.checkbox("الحفاظ على التنسيق", value=True, key="doc_format")
-
- translate_images = st.checkbox("ترجمة النصوص في الصور", value=False)
-
- maintain_layout = st.checkbox("الحفاظ على تخطيط المستند", value=True)
-
- # معلومات إضافية
- st.markdown("#### معلومات إضافية")
-
- col1, col2 = st.columns(2)
-
- with col1:
- document_name = st.text_input("اسم المستند")
-
- with col2:
- related_entity = st.text_input("الكيان المرتبط (مثل: رقم المناقصة أو المشروع)")
-
- # زر بدء الترجمة
- if st.button("بدء ترجمة المستند", use_container_width=True):
- if uploaded_file is None:
- st.error("يرجى تحميل المستند المراد ترجمته")
- else:
- # في تطبيق حقيقي، سيتم إرسال المستند إلى خدمة الترجمة
- # هنا نعرض محاكاة لعملية الترجمة
-
- progress_bar = st.progress(0)
- status_text = st.empty()
-
- # محاكاة تقدم الترجمة
- import time
- for i in range(101):
- progress_bar.progress(i)
-
- if i < 10:
- status_text.text("جاري تحليل المستند...")
- elif i < 30:
- status_text.text("جاري استخراج النصوص...")
- elif i < 70:
- status_text.text("جاري ترجمة المحتوى...")
- elif i < 90:
- status_text.text("جاري إعادة بناء المستند...")
- else:
- status_text.text("جاري إنهاء الترجمة...")
-
- time.sleep(0.05)
-
- # عرض نتيجة الترجمة
- st.success("تمت ترجمة المستند بنجاح!")
-
- # إنشاء اسم الملف المترجم
- file_name_parts = uploaded_file.name.split('.')
- translated_file_name = f"{'.'.join(file_name_parts[:-1])}_{target_language}.{file_name_parts[-1]}"
-
- # عرض معلومات الملف المترجم
- st.markdown("#### معلومات الملف المترجم")
-
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown(f"**اسم الملف:** {translated_file_name}")
- st.markdown(f"**لغة المصدر:** {self.supported_languages[source_language]}")
- st.markdown(f"**لغة الهدف:** {self.supported_languages[target_language]}")
-
- with col2:
- st.markdown(f"**محرك الترجمة:** {translation_engine}")
- st.markdown(f"**تاريخ الترجمة:** {datetime.datetime.now().strftime('%Y-%m-%d')}")
- st.markdown(f"**حالة الترجمة:** مكتمل")
-
- # أزرار إضافية
- col1, col2, col3 = st.columns(3)
-
- with col1:
- if st.button("تنزيل الملف المترجم", use_container_width=True):
- st.success("تم بدء تنزيل الملف المترجم")
-
- with col2:
- if st.button("حفظ في المستندات المترجمة", use_container_width=True):
- st.success("تم حفظ الملف في المستندات المترجمة")
-
- with col3:
- if st.button("مشاركة الملف", use_container_width=True):
- st.success("تم نسخ رابط مشاركة الملف")
-
- # عرض إحصائيات الترجمة
- st.markdown("#### إحصائيات الترجمة")
-
- col1, col2, col3, col4 = st.columns(4)
-
- with col1:
- self.ui.create_metric_card(
- "عدد الصفحات",
- "12",
- None,
- self.ui.COLORS['primary']
- )
-
- with col2:
- self.ui.create_metric_card(
- "عدد الكلمات",
- "2,450",
- None,
- self.ui.COLORS['secondary']
- )
-
- with col3:
- self.ui.create_metric_card(
- "وقت الترجمة",
- "45 ثانية",
- None,
- self.ui.COLORS['success']
- )
-
- with col4:
- self.ui.create_metric_card(
- "المصطلحات الفنية",
- "28",
- None,
- self.ui.COLORS['accent']
- )
-
- def technical_terms_dictionary(self):
- """قاموس المصطلحات الفنية"""
- st.markdown("### قاموس المصطلحات الفنية")
-
- # إضافة مصطلح جديد
- with st.expander("إضافة مصطلح جديد"):
- with st.form("add_term_form"):
- col1, col2, col3 = st.columns(3)
-
- with col1:
- term_ar = st.text_input("المصطلح بالعربية")
-
- with col2:
- term_en = st.text_input("المصطلح بالإنجليزية")
-
- with col3:
- term_category = st.selectbox(
- "الفئة",
- options=["مستندات", "ضمانات", "أنواع المناقصات", "عقود", "أطراف", "أعمال", "شروط", "أخرى"]
- )
-
- # زر إضافة المصطلح
- submit_button = st.form_submit_button("إضافة المصطلح")
-
- if submit_button:
- if not term_ar or not term_en:
- st.error("يرجى ملء جميع الحقول المطلوبة")
- else:
- # في تطبيق حقيقي، سيتم إضافة المصطلح إلى قاعدة البيانات
- st.success("تمت إضافة المصطلح بنجاح")
-
- # البحث في المصطلحات
- st.markdown("#### البحث في المصطلحات")
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- search_term = st.text_input("البحث عن مصطلح")
-
- with col2:
- search_language = st.radio(
- "لغة البحث",
- options=["الكل", "العربية", "الإنجليزية"],
- horizontal=True
- )
-
- with col3:
- category_filter = st.selectbox(
- "تصفية حسب الفئة",
- options=["الكل", "مستندات", "ضمانات", "أنواع المناقصات", "عقود", "أطراف", "أعمال", "شروط", "أخرى"]
- )
-
- # تطبيق الفلاتر
- filtered_terms = self.technical_terms
-
- if search_term:
- if search_language == "العربية":
- filtered_terms = [term for term in filtered_terms if search_term.lower() in term["ar"].lower()]
- elif search_language == "الإنجليزية":
- filtered_terms = [term for term in filtered_terms if search_term.lower() in term["en"].lower()]
- else:
- filtered_terms = [term for term in filtered_terms if search_term.lower() in term["ar"].lower() or search_term.lower() in term["en"].lower()]
-
- if category_filter != "الكل":
- filtered_terms = [term for term in filtered_terms if term["category"] == category_filter]
-
- # عرض المصطلحات
- st.markdown("#### المصطلحات الفنية")
-
- if not filtered_terms:
- st.info("لا توجد مصطلحات تطابق معايير البحث")
- else:
- # تحويل البيانات إلى DataFrame
- terms_df = pd.DataFrame(filtered_terms)
-
- # إعادة تسمية الأعمدة
- terms_df = terms_df.rename(columns={
- "ar": "المصطلح بالعربية",
- "en": "المصطلح بالإنجليزية",
- "category": "الفئة"
- })
-
- # عرض الجدول
- st.dataframe(
- terms_df,
- use_container_width=True,
- hide_index=True
- )
-
- # أزرار إضافية
- col1, col2 = st.columns([1, 5])
-
- with col1:
- if st.button("تصدير القاموس", use_container_width=True):
- st.success("تم تصدير القاموس بنجاح")
-
- # عرض إحصائيات القاموس
- st.markdown("#### إحصائيات القاموس")
-
- # حساب عدد المصطلحات في كل فئة
- category_counts = {}
- for term in self.technical_terms:
- if term["category"] not in category_counts:
- category_counts[term["category"]] = 0
- category_counts[term["category"]] += 1
-
- # عرض الإحصائيات
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown("##### عدد المصطلحات حسب الفئة")
-
- # تحويل البيانات إلى DataFrame
- category_df = pd.DataFrame({
- "الفئة": list(category_counts.keys()),
- "العدد": list(category_counts.values())
- })
-
- # عرض الرسم البياني
- st.bar_chart(category_df.set_index("الفئة"))
-
- with col2:
- st.markdown("##### إحصائيات عامة")
-
- total_terms = len(self.technical_terms)
- categories_count = len(category_counts)
-
- st.markdown(f"**إجمالي المصطلحات:** {total_terms}")
- st.markdown(f"**عدد الفئات:** {categories_count}")
- st.markdown(f"**متوسط المصطلحات لكل فئة:** {total_terms / categories_count:.1f}")
- st.markdown(f"**آخر تحديث للقاموس:** {datetime.datetime.now().strftime('%Y-%m-%d')}")
-
- def show_translated_documents(self):
- """عرض المستندات المترجمة"""
- st.markdown("### المستندات المترجمة")
-
- # إنشاء فلاتر للمستندات
- col1, col2, col3 = st.columns(3)
-
- with col1:
- entity_filter = st.selectbox(
- "تصفية حسب الكيان",
- options=["الكل"] + list(set([doc["related_entity"] for doc in self.translated_documents]))
- )
-
- with col2:
- language_pair_filter = st.selectbox(
- "تصفية حسب زوج اللغات",
- options=["الكل"] + list(set([f"{doc['source_language']} -> {doc['target_language']}" for doc in self.translated_documents]))
- )
-
- with col3:
- status_filter = st.selectbox(
- "تصفية حسب الحالة",
- options=["الكل", "مكتمل", "قيد التنفيذ"]
- )
-
- # تطبيق الفلاتر
- filtered_docs = self.translated_documents
-
- if entity_filter != "الكل":
- filtered_docs = [doc for doc in filtered_docs if doc["related_entity"] == entity_filter]
-
- if language_pair_filter != "الكل":
- source_lang, target_lang = language_pair_filter.split(" -> ")
- filtered_docs = [doc for doc in filtered_docs if doc["source_language"] == source_lang and doc["target_language"] == target_lang]
-
- if status_filter != "الكل":
- filtered_docs = [doc for doc in filtered_docs if doc["status"] == status_filter]
-
- # عرض المستندات المترجمة
- if not filtered_docs:
- st.info("لا توجد مستندات مترجمة تطابق معايير التصفية")
- else:
- # تحويل البيانات إلى DataFrame
- docs_df = pd.DataFrame(filtered_docs)
-
- # تحويل رموز اللغات إلى أسماء اللغات
- docs_df["source_language"] = docs_df["source_language"].map(self.supported_languages)
- docs_df["target_language"] = docs_df["target_language"].map(self.supported_languages)
-
- # إعادة ترتيب الأعمدة وتغيير أسمائها
- display_df = docs_df[[
- "id", "name", "source_language", "target_language", "translation_date", "status", "pages", "related_entity"
- ]].rename(columns={
- "id": "الرقم",
- "name": "اسم المستند",
- "source_language": "لغة المصدر",
- "target_language": "لغة الهدف",
- "translation_date": "تاريخ الترجمة",
- "status": "الحالة",
- "pages": "عدد الصفحات",
- "related_entity": "الكيان المرتبط"
- })
-
- # عرض الجدول
- st.dataframe(
- display_df,
- use_container_width=True,
- hide_index=True
- )
-
- # عرض تفاصيل المستند المحدد
- st.markdown("#### تفاصيل المستند المترجم")
-
- selected_doc_id = st.selectbox(
- "اختر مستنداً لعرض التفاصيل",
- options=[doc["id"] for doc in filtered_docs],
- format_func=lambda x: next((f"{doc['id']} - {doc['name']}" for doc in filtered_docs if doc["id"] == x), "")
- )
-
- # العثور على المستند المحدد
- selected_doc = next((doc for doc in filtered_docs if doc["id"] == selected_doc_id), None)
-
- if selected_doc:
- col1, col2 = st.columns(2)
-
- with col1:
- st.markdown(f"**اسم المستند:** {selected_doc['name']}")
- st.markdown(f"**لغة المصدر:** {self.supported_languages[selected_doc['source_language']]}")
- st.markdown(f"**لغة الهدف:** {self.supported_languages[selected_doc['target_language']]}")
- st.markdown(f"**تاريخ الترجمة:** {selected_doc['translation_date']}")
-
- with col2:
- st.markdown(f"**الملف الأصلي:** {selected_doc['original_file']}")
- st.markdown(f"**الملف المترجم:** {selected_doc['translated_file']}")
- st.markdown(f"**المترجم:** {selected_doc['translated_by']}")
- st.markdown(f"**الحالة:** {selected_doc['status']}")
-
- # أزرار الإجراءات
- col1, col2, col3 = st.columns(3)
-
- with col1:
- if st.button("تنزيل الملف الأصلي", use_container_width=True):
- st.success("تم بدء تنزيل الملف الأصلي")
-
- with col2:
- if st.button("تنزيل الملف المترجم", use_container_width=True):
- st.success("تم بدء تنزيل الملف المترجم")
-
- with col3:
- if st.button("مشاركة الملف المترجم", use_container_width=True):
- st.success("تم نسخ رابط مشاركة الملف المترجم")
-
- # عرض إحصائيات الترجمة
- st.markdown("#### إحصائيات الترجمة")
-
- col1, col2, col3 = st.columns(3)
-
- with col1:
- # إحصائيات حسب زوج اللغات
- language_pairs = {}
- for doc in self.translated_documents:
- pair = f"{self.supported_languages[doc['source_language']]} -> {self.supported_languages[doc['target_language']]}"
- if pair not in language_pairs:
- language_pairs[pair] = 0
- language_pairs[pair] += 1
-
- st.markdown("##### المستندات حسب زوج اللغات")
-
- # تحويل البيانات إلى DataFrame
- language_df = pd.DataFrame({
- "زوج اللغات": list(language_pairs.keys()),
- "العدد": list(language_pairs.values())
- })
-
- # عرض الرسم البياني
- st.bar_chart(language_df.set_index("زوج اللغات"))
-
- with col2:
- # إحصائيات حسب الكيان المرتبط
- entity_counts = {}
- for doc in self.translated_documents:
- if doc["related_entity"] not in entity_counts:
- entity_counts[doc["related_entity"]] = 0
- entity_counts[doc["related_entity"]] += 1
-
- st.markdown("##### المستندات حسب الكيان المرتبط")
-
- # تحويل البيانات إلى DataFrame
- entity_df = pd.DataFrame({
- "الكيان المرتبط": list(entity_counts.keys()),
- "العدد": list(entity_counts.values())
- })
-
- # عرض الرسم البياني
- st.bar_chart(entity_df.set_index("الكيان المرتبط"))
-
- with col3:
- # إحصائيات عامة
- total_docs = len(self.translated_documents)
- completed_docs = len([doc for doc in self.translated_documents if doc["status"] == "مكتمل"])
- in_progress_docs = len([doc for doc in self.translated_documents if doc["status"] == "قيد التنفيذ"])
- total_pages = sum([doc["pages"] for doc in self.translated_documents])
-
- st.markdown("##### إحصائيات عامة")
- st.markdown(f"**إجمالي المستندات المترجمة:** {total_docs}")
- st.markdown(f"**المستندات المكتملة:** {completed_docs}")
- st.markdown(f"**المستندات قيد التنفيذ:** {in_progress_docs}")
- st.markdown(f"**إجمالي الصفحات المترجمة:** {total_pages}")
- st.markdown(f"**متوسط الصفحات لكل مستند:** {total_pages / total_docs:.1f}")
-
-# تشغيل التطبيق
-if __name__ == "__main__":
- translation_app = TranslationApp()
- translation_app.run()
diff --git a/modules/voice_narration/__init__.py b/modules/voice_narration/__init__.py
deleted file mode 100644
index 1267f2ed042ede80e3b2f414c207a29153d69bd5..0000000000000000000000000000000000000000
--- a/modules/voice_narration/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# ملف تهيئة وحدة الترجمة الصوتية متعددة اللغات
\ No newline at end of file
diff --git a/modules/voice_narration/voice_narration_app.py b/modules/voice_narration/voice_narration_app.py
deleted file mode 100644
index 65374e3adee4067da02770c7e3c59d1387ec7ce2..0000000000000000000000000000000000000000
--- a/modules/voice_narration/voice_narration_app.py
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/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)
-
- # اختيار نوع المحتوى
- 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("""
-
- يتيح لك نظام الترجمة الصوتية متعددة اللغات تحويل النصوص والمستندات إلى ملفات صوتية بلغات متعددة،
- مما يساعد في توصيل المعلومات بشكل أفضل للأشخاص من خلفيات لغوية مختلفة.
-
- """, 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