Wahbi-AI / modules /resources /resources_app.py
EGYADMIN's picture
Upload 114 files
25d2b3e verified
raw
history blame
66.3 kB
"""
تطبيق وحدة الموارد
"""
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': '[email protected]',
'local_content': 85,
'last_updated': '2024-01-15'
},
{
'id': 2,
'name': 'شركة التكييف والتبريد',
'category': 'أعمال كهروميكانيكية',
'specialization': 'تركيب أنظمة التكييف والتبريد',
'rating': 4.5,
'city': 'جدة',
'contact_person': 'أحمد الغامدي',
'phone': '0566666666',
'email': '[email protected]',
'local_content': 75,
'last_updated': '2024-01-20'
},
{
'id': 3,
'name': 'مؤسسة الكهرباء الحديثة',
'category': 'أعمال كهروميكانيكية',
'specialization': 'تنفيذ الأعمال الكهربائية',
'rating': 4.6,
'city': 'الرياض',
'contact_person': 'فهد السويلم',
'phone': '0577777777',
'email': '[email protected]',
'local_content': 90,
'last_updated': '2024-02-05'
},
{
'id': 4,
'name': 'شركة المقاولات المتخصصة',
'category': 'أعمال تشطيبات',
'specialization': 'تنفيذ أعمال التشطيبات الداخلية',
'rating': 4.7,
'city': 'الدمام',
'contact_person': 'خالد الدوسري',
'phone': '0588888888',
'email': '[email protected]',
'local_content': 80,
'last_updated': '2024-01-25'
},
{
'id': 5,
'name': 'مؤسسة الصيانة والتشغيل',
'category': 'أعمال صيانة',
'specialization': 'صيانة وتشغيل المباني',
'rating': 4.4,
'city': 'الرياض',
'contact_person': 'عبدالله العنزي',
'phone': '0599999999',
'email': '[email protected]',
'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("<h1 class='module-title'>وحدة إدارة الموارد</h1>", 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("""
### توقع استقرار نسبي في الأسعار
- يمكن الشراء حسب الاحتياج دون الحاجة لتخزين كميات كبيرة
- متابعة أسعار السوق بشكل دوري للتأكد من دقة التوقعات
""")