""" تطبيق وحدة الموارد """ 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, timedelta import time import io import os import tempfile import random class ResourcesApp: """وحدة إدارة الموارد""" def __init__(self): """تهيئة وحدة الموارد""" # تهيئة البيانات في حالة الجلسة إذا لم تكن موجودة if 'materials' not in st.session_state: st.session_state.materials = [ { 'id': 1, 'name': 'خرسانة جاهزة', 'category': 'مواد إنشائية', 'unit': 'م3', 'price': 250, 'supplier': 'شركة الخرسانة الوطنية', 'local_content': 95, 'last_updated': '2024-01-15' }, { 'id': 2, 'name': 'حديد تسليح', 'category': 'مواد إنشائية', 'unit': 'طن', 'price': 4500, 'supplier': 'مصنع الحديد السعودي', 'local_content': 45, 'last_updated': '2024-02-10' }, { 'id': 3, 'name': 'بلوك خرساني', 'category': 'مواد إنشائية', 'unit': 'م3', 'price': 350, 'supplier': 'مصنع البلوك الحديث', 'local_content': 95, 'last_updated': '2024-01-20' }, { 'id': 4, 'name': 'رمل', 'category': 'مواد إنشائية', 'unit': 'م3', 'price': 60, 'supplier': 'مؤسسة توريدات البناء', 'local_content': 100, 'last_updated': '2024-01-15' }, { 'id': 5, 'name': 'بلاط سيراميك', 'category': 'مواد تشطيب', 'unit': 'م2', 'price': 120, 'supplier': 'شركة السيراميك الوطنية', 'local_content': 80, 'last_updated': '2024-02-05' } ] if 'labor' not in st.session_state: st.session_state.labor = [ { 'id': 1, 'name': 'مهندس مدني', 'category': 'هندسة', 'unit': 'شهر', 'price': 15000, 'supplier': 'داخلي', 'local_content': 90, 'last_updated': '2024-01-10' }, { 'id': 2, 'name': 'مهندس معماري', 'category': 'هندسة', 'unit': 'شهر', 'price': 14000, 'supplier': 'داخلي', 'local_content': 85, 'last_updated': '2024-01-10' }, { 'id': 3, 'name': 'مساح', 'category': 'هندسة', 'unit': 'شهر', 'price': 8000, 'supplier': 'داخلي', 'local_content': 100, 'last_updated': '2024-01-10' }, { 'id': 4, 'name': 'فني كهرباء', 'category': 'فني', 'unit': 'شهر', 'price': 7000, 'supplier': 'داخلي', 'local_content': 95, 'last_updated': '2024-01-10' }, { 'id': 5, 'name': 'عامل بناء', 'category': 'عمالة', 'unit': 'يوم', 'price': 200, 'supplier': 'شركة توريد عمالة', 'local_content': 60, 'last_updated': '2024-01-20' } ] if 'equipment' not in st.session_state: st.session_state.equipment = [ { 'id': 1, 'name': 'حفارة كبيرة', 'category': 'معدات ثقيلة', 'unit': 'يوم', 'price': 2500, 'supplier': 'شركة المعدات الثقيلة', 'local_content': 70, 'last_updated': '2024-01-15' }, { 'id': 2, 'name': 'خلاطة خرسانة', 'category': 'معدات إنشائية', 'unit': 'يوم', 'price': 1800, 'supplier': 'مؤسسة معدات البناء', 'local_content': 65, 'last_updated': '2024-01-20' }, { 'id': 3, 'name': 'رافعة برجية', 'category': 'معدات ثقيلة', 'unit': 'شهر', 'price': 45000, 'supplier': 'شركة المعدات الثقيلة', 'local_content': 50, 'last_updated': '2024-02-05' }, { 'id': 4, 'name': 'مولد كهربائي', 'category': 'معدات مساندة', 'unit': 'شهر', 'price': 12000, 'supplier': 'شركة المعدات الكهربائية', 'local_content': 75, 'last_updated': '2024-01-25' }, { 'id': 5, 'name': 'سقالات معدنية', 'category': 'معدات مساندة', 'unit': 'م2/شهر', 'price': 50, 'supplier': 'مؤسسة معدات البناء', 'local_content': 90, 'last_updated': '2024-01-15' } ] if 'subcontractors' not in st.session_state: st.session_state.subcontractors = [ { 'id': 1, 'name': 'مؤسسة الإنشاءات المتكاملة', 'category': 'أعمال إنشائية', 'specialization': 'تنفيذ الهيكل الخرساني', 'rating': 4.8, 'city': 'الرياض', 'contact_person': 'محمد العتيبي', 'phone': '0555555555', 'email': 'info@constructionfirm.sa', 'local_content': 85, 'last_updated': '2024-01-15' }, { 'id': 2, 'name': 'شركة التكييف والتبريد', 'category': 'أعمال كهروميكانيكية', 'specialization': 'تركيب أنظمة التكييف والتبريد', 'rating': 4.5, 'city': 'جدة', 'contact_person': 'أحمد الغامدي', 'phone': '0566666666', 'email': 'info@acfirm.sa', 'local_content': 75, 'last_updated': '2024-01-20' }, { 'id': 3, 'name': 'مؤسسة الكهرباء الحديثة', 'category': 'أعمال كهروميكانيكية', 'specialization': 'تنفيذ الأعمال الكهربائية', 'rating': 4.6, 'city': 'الرياض', 'contact_person': 'فهد السويلم', 'phone': '0577777777', 'email': 'info@electricfirm.sa', 'local_content': 90, 'last_updated': '2024-02-05' }, { 'id': 4, 'name': 'شركة المقاولات المتخصصة', 'category': 'أعمال تشطيبات', 'specialization': 'تنفيذ أعمال التشطيبات الداخلية', 'rating': 4.7, 'city': 'الدمام', 'contact_person': 'خالد الدوسري', 'phone': '0588888888', 'email': 'info@specializedcontractor.sa', 'local_content': 80, 'last_updated': '2024-01-25' }, { 'id': 5, 'name': 'مؤسسة الصيانة والتشغيل', 'category': 'أعمال صيانة', 'specialization': 'صيانة وتشغيل المباني', 'rating': 4.4, 'city': 'الرياض', 'contact_person': 'عبدالله العنزي', 'phone': '0599999999', 'email': 'info@maintenancefirm.sa', 'local_content': 95, 'last_updated': '2024-02-10' } ] if 'price_history' not in st.session_state: st.session_state.price_history = [ # تاريخ أسعار الخرسانة الجاهزة *[{'material_id': 1, 'date': (datetime.now() - timedelta(days=i*30)).strftime('%Y-%m-%d'), 'price': 250 - (i * 5) if i < 3 else 250 - 15 + (i - 2) * 10} for i in range(12)], # تاريخ أسعار حديد التسليح *[{'material_id': 2, 'date': (datetime.now() - timedelta(days=i*30)).strftime('%Y-%m-%d'), 'price': 4500 - (i * 100) if i < 4 else 4500 - 400 + (i - 3) * 150} for i in range(12)], # تاريخ أسعار البلوك الخرساني *[{'material_id': 3, 'date': (datetime.now() - timedelta(days=i*30)).strftime('%Y-%m-%d'), 'price': 350 - (i * 10) if i < 6 else 350 - 60 + (i - 5) * 15} for i in range(12)] ] 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(""" ### توقع استقرار نسبي في الأسعار - يمكن الشراء حسب الاحتياج دون الحاجة لتخزين كميات كبيرة - متابعة أسعار السوق بشكل دوري للتأكد من دقة التوقعات """)