|
""" |
|
تطبيق وحدة الموارد |
|
""" |
|
|
|
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'] |
|
}) |
|
|
|
|
|
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'] |
|
}) |
|
|
|
|
|
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: |
|
|
|
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: |
|
|
|
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: |
|
|
|
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: |
|
|
|
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] |
|
|
|
|
|
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: |
|
|
|
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 |
|
|
|
|
|
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'] |
|
}) |
|
|
|
|
|
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 |
|
|
|
|
|
price_history_df = pd.DataFrame(price_history_data).sort_values('date') |
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
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(""" |
|
### توقع استقرار نسبي في الأسعار |
|
- يمكن الشراء حسب الاحتياج دون الحاجة لتخزين كميات كبيرة |
|
- متابعة أسعار السوق بشكل دوري للتأكد من دقة التوقعات |
|
""") |