"""
تطبيق وحدة الموارد
"""
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("""
### توقع استقرار نسبي في الأسعار
- يمكن الشراء حسب الاحتياج دون الحاجة لتخزين كميات كبيرة
- متابعة أسعار السوق بشكل دوري للتأكد من دقة التوقعات
""")