Spaces:
Paused
Paused
Create web/pages/procurement.py
Browse files- web/pages/procurement.py +536 -0
web/pages/procurement.py
ADDED
@@ -0,0 +1,536 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import numpy as np
|
4 |
+
import plotly.express as px
|
5 |
+
import plotly.graph_objects as go
|
6 |
+
from datetime import datetime, timedelta
|
7 |
+
|
8 |
+
def show_procurement():
|
9 |
+
"""
|
10 |
+
عرض صفحة إدارة المشتريات والعقود
|
11 |
+
"""
|
12 |
+
st.subheader("إدارة المشتريات والعقود")
|
13 |
+
|
14 |
+
# الخيارات الفرعية
|
15 |
+
tabs = st.tabs(["العقود النشطة", "أوامر الشراء", "المناقصات الداخلية", "تقييم الموردين"])
|
16 |
+
|
17 |
+
# تبويب العقود النشطة
|
18 |
+
with tabs[0]:
|
19 |
+
show_active_contracts()
|
20 |
+
|
21 |
+
# تبويب أوامر الشراء
|
22 |
+
with tabs[1]:
|
23 |
+
show_purchase_orders()
|
24 |
+
|
25 |
+
# تبويب المناقصات الداخلية
|
26 |
+
with tabs[2]:
|
27 |
+
show_internal_tenders()
|
28 |
+
|
29 |
+
# تبويب تقييم الموردين
|
30 |
+
with tabs[3]:
|
31 |
+
show_vendor_evaluation()
|
32 |
+
|
33 |
+
def show_active_contracts():
|
34 |
+
"""
|
35 |
+
عرض بيانات العقود النشطة
|
36 |
+
"""
|
37 |
+
st.markdown("## العقود النشطة")
|
38 |
+
|
39 |
+
# إنشاء بيانات توضيحية للعقود
|
40 |
+
current_date = datetime.now().date()
|
41 |
+
|
42 |
+
contracts_data = {
|
43 |
+
"رقم العقد": ["C-2025-1001", "C-2025-1042", "C-2024-0987", "C-2024-0912", "C-2025-1123",
|
44 |
+
"C-2024-0875", "C-2025-1088", "C-2025-1156", "C-2024-0932", "C-2025-1201"],
|
45 |
+
"المورد": ["شركة الصناعات السعودية", "مؤسسة الخليج للمقاولات", "شركة الرياض للإنشاءات",
|
46 |
+
"الشركة العربية للمعدات", "مصنع المنتجات الإسمنتية", "شركة تقنيات البناء",
|
47 |
+
"مؤسسة المدار للتوريدات", "شركة البنية التحتية المتكاملة", "مصنع الصلب السعودي",
|
48 |
+
"شركة الأنابيب الوطنية"],
|
49 |
+
"نوع العقد": ["توريد مواد", "مقاولات", "خدمات هندسية", "تأجير معدات", "توريد مواد",
|
50 |
+
"خدمات فنية", "توريد مواد", "مقاولات", "توريد مواد", "توريد مواد"],
|
51 |
+
"تاريخ البدء": [
|
52 |
+
current_date - timedelta(days=120),
|
53 |
+
current_date - timedelta(days=90),
|
54 |
+
current_date - timedelta(days=210),
|
55 |
+
current_date - timedelta(days=180),
|
56 |
+
current_date - timedelta(days=60),
|
57 |
+
current_date - timedelta(days=240),
|
58 |
+
current_date - timedelta(days=45),
|
59 |
+
current_date - timedelta(days=30),
|
60 |
+
current_date - timedelta(days=150),
|
61 |
+
current_date - timedelta(days=15)
|
62 |
+
],
|
63 |
+
"تاريخ الانتهاء": [
|
64 |
+
current_date + timedelta(days=245),
|
65 |
+
current_date + timedelta(days=270),
|
66 |
+
current_date + timedelta(days=155),
|
67 |
+
current_date + timedelta(days=185),
|
68 |
+
current_date + timedelta(days=305),
|
69 |
+
current_date + timedelta(days=125),
|
70 |
+
current_date + timedelta(days=320),
|
71 |
+
current_date + timedelta(days=335),
|
72 |
+
current_date + timedelta(days=215),
|
73 |
+
current_date + timedelta(days=350)
|
74 |
+
],
|
75 |
+
"القيمة (مليون ريال)": [12.5, 28.7, 8.3, 6.2, 9.1, 5.4, 7.8, 15.6, 11.2, 10.9],
|
76 |
+
"نسبة الإنجاز (%)": [45, 30, 75, 65, 20, 80, 15, 10, 60, 5]
|
77 |
+
}
|
78 |
+
|
79 |
+
# إنشاء DataFrame
|
80 |
+
contracts_df = pd.DataFrame(contracts_data)
|
81 |
+
|
82 |
+
# إضافة المدة المتبقية
|
83 |
+
contracts_df["المدة المتبقية (يوم)"] = (contracts_df["تاريخ الانتهاء"] - current_date).dt.days
|
84 |
+
|
85 |
+
# تصنيف الحالة
|
86 |
+
conditions = [
|
87 |
+
(contracts_df["المدة المتبقية (يوم)"] < 30),
|
88 |
+
(contracts_df["المدة المتبقية (يوم)"] < 90),
|
89 |
+
(contracts_df["المدة المتبقية (يوم)"] >= 90)
|
90 |
+
]
|
91 |
+
values = ["على وشك الانتهاء", "متوسطة", "طويلة الأجل"]
|
92 |
+
colors = ["#D32F2F", "#FFC107", "#4CAF50"]
|
93 |
+
|
94 |
+
contracts_df["حالة العقد"] = np.select(conditions, values)
|
95 |
+
|
96 |
+
# عرض فلاتر البحث
|
97 |
+
col1, col2, col3 = st.columns(3)
|
98 |
+
|
99 |
+
with col1:
|
100 |
+
contract_type_filter = st.selectbox(
|
101 |
+
"نوع العقد",
|
102 |
+
["الكل"] + sorted(contracts_df["نوع العقد"].unique().tolist())
|
103 |
+
)
|
104 |
+
|
105 |
+
with col2:
|
106 |
+
status_filter = st.selectbox(
|
107 |
+
"حالة العقد",
|
108 |
+
["الكل"] + sorted(contracts_df["حالة العقد"].unique().tolist())
|
109 |
+
)
|
110 |
+
|
111 |
+
with col3:
|
112 |
+
min_value = st.number_input("الحد الأدنى للقيمة (مليون ريال)", 0.0, 50.0, 0.0)
|
113 |
+
|
114 |
+
# تطبيق الفلاتر
|
115 |
+
filtered_df = contracts_df.copy()
|
116 |
+
|
117 |
+
if contract_type_filter != "الكل":
|
118 |
+
filtered_df = filtered_df[filtered_df["نوع العقد"] == contract_type_filter]
|
119 |
+
|
120 |
+
if status_filter != "الكل":
|
121 |
+
filtered_df = filtered_df[filtered_df["حالة العقد"] == status_filter]
|
122 |
+
|
123 |
+
if min_value > 0:
|
124 |
+
filtered_df = filtered_df[filtered_df["القيمة (مليون ريال)"] >= min_value]
|
125 |
+
|
126 |
+
# عرض العقود المصفاة
|
127 |
+
st.dataframe(filtered_df, use_container_width=True)
|
128 |
+
|
129 |
+
# تحليلات العقود
|
130 |
+
st.markdown("### تحليلات العقود")
|
131 |
+
|
132 |
+
col1, col2 = st.columns(2)
|
133 |
+
|
134 |
+
with col1:
|
135 |
+
# توزيع العقود حسب النوع
|
136 |
+
type_distribution = contracts_df.groupby("نوع العقد")["القيمة (مليون ريال)"].sum().reset_index()
|
137 |
+
|
138 |
+
fig1 = px.pie(
|
139 |
+
type_distribution,
|
140 |
+
values="القيمة (مليون ريال)",
|
141 |
+
names="نوع العقد",
|
142 |
+
title="توزيع قيمة العقود حسب النوع",
|
143 |
+
color_discrete_sequence=px.colors.qualitative.Bold
|
144 |
+
)
|
145 |
+
|
146 |
+
fig1.update_traces(textposition="inside", textinfo="percent+label")
|
147 |
+
|
148 |
+
st.plotly_chart(fig1, use_container_width=True)
|
149 |
+
|
150 |
+
with col2:
|
151 |
+
# توزيع العقود حسب الحالة
|
152 |
+
status_distribution = contracts_df.groupby("حالة العقد").agg({
|
153 |
+
"رقم العقد": "count",
|
154 |
+
"القيمة (مليون ريال)": "sum"
|
155 |
+
}).reset_index()
|
156 |
+
|
157 |
+
status_distribution.columns = ["الحالة", "عدد العقود", "إجمالي القيمة (مليون ريال)"]
|
158 |
+
|
159 |
+
# ترتيب الحالات
|
160 |
+
status_order = {"على وشك الانتهاء": 1, "متوسطة": 2, "طويلة الأجل": 3}
|
161 |
+
status_distribution["الترتيب"] = status_distribution["الحالة"].map(status_order)
|
162 |
+
status_distribution = status_distribution.sort_values("الترتيب")
|
163 |
+
|
164 |
+
# اختيار الألوان حسب الحالة
|
165 |
+
status_colors = {"على وشك الانتهاء": "#D32F2F", "متوسطة": "#FFC107", "طويلة الأجل": "#4CAF50"}
|
166 |
+
|
167 |
+
fig2 = px.bar(
|
168 |
+
status_distribution,
|
169 |
+
x="الحالة",
|
170 |
+
y="إجمالي القيمة (مليون ريال)",
|
171 |
+
color="الحالة",
|
172 |
+
text="عدد العقود",
|
173 |
+
title="توزيع العقود حسب المدة المتبقية",
|
174 |
+
color_discrete_map=status_colors
|
175 |
+
)
|
176 |
+
|
177 |
+
fig2.update_traces(texttemplate="%{text} عقد", textposition="outside")
|
178 |
+
|
179 |
+
st.plotly_chart(fig2, use_container_width=True)
|
180 |
+
|
181 |
+
# العقود القريبة من الانتهاء
|
182 |
+
st.markdown("### العقود على وشك الانتهاء")
|
183 |
+
|
184 |
+
expiring_contracts = contracts_df[contracts_df["المدة المتبقية (يوم)"] < 30].sort_values("المدة المتبقية (يوم)")
|
185 |
+
|
186 |
+
if not expiring_contracts.empty:
|
187 |
+
for _, contract in expiring_contracts.iterrows():
|
188 |
+
st.markdown(f"""
|
189 |
+
**{contract['رقم العقد']} - {contract['المورد']}**
|
190 |
+
**نوع العقد:** {contract['نوع العقد']}
|
191 |
+
**المدة المتبقية:** {contract['المدة المتبقية (يوم)']} يوم
|
192 |
+
**نسبة الإنجاز:** {contract['نسبة الإنجاز (%)']}%
|
193 |
+
**القيمة:** {contract['القيمة (مليون ريال)']} مليون ريال
|
194 |
+
""")
|
195 |
+
|
196 |
+
# شريط التقدم
|
197 |
+
st.progress(contract['نسبة الإنجاز (%)'] / 100)
|
198 |
+
st.markdown("---")
|
199 |
+
else:
|
200 |
+
st.info("لا توجد عقود على وشك الانتهاء خلال الشهر القادم")
|
201 |
+
|
202 |
+
def show_purchase_orders():
|
203 |
+
"""
|
204 |
+
عرض أوامر الشراء
|
205 |
+
"""
|
206 |
+
st.markdown("## أوامر الشراء")
|
207 |
+
|
208 |
+
# إنشاء بيانات توضيحية لأوامر الشراء
|
209 |
+
current_date = datetime.now().date()
|
210 |
+
|
211 |
+
po_data = {
|
212 |
+
"رقم أمر الشراء": [f"PO-{2025}-{i:04d}" for i in range(1001, 1011)],
|
213 |
+
"المورد": [
|
214 |
+
"شركة الصناعات السعودية", "مؤسسة الخليج للمقاولات", "شركة الرياض للإنشاءات",
|
215 |
+
"الشركة العربية للمعدات", "مصنع المنتجات الإسمنتية", "شركة تقنيات البناء",
|
216 |
+
"مؤسسة المدار للتوريدات", "شركة البنية التحتية المتكاملة", "مصنع الصلب السعودي",
|
217 |
+
"شركة الأنابيب الوطنية"
|
218 |
+
],
|
219 |
+
"المشروع": [
|
220 |
+
"مشروع توسعة شبكة الطرق", "بناء المدارس", "تطوير البنية التحتية",
|
221 |
+
"تحديث شبكة المياه", "بناء المستشفى التخصصي", "إنشاء مركز البيانات",
|
222 |
+
"توسعة المطار", "تطوير الحدائق ال��امة", "بناء المجمع السكني",
|
223 |
+
"تطوير شبكة الصرف الصحي"
|
224 |
+
],
|
225 |
+
"تاريخ الطلب": [
|
226 |
+
current_date - timedelta(days=np.random.randint(5, 60)) for _ in range(10)
|
227 |
+
],
|
228 |
+
"تاريخ التسليم المتوقع": [
|
229 |
+
current_date + timedelta(days=np.random.randint(5, 45)) for _ in range(10)
|
230 |
+
],
|
231 |
+
"القيمة (ريال)": [
|
232 |
+
np.random.randint(50000, 5000000) for _ in range(10)
|
233 |
+
],
|
234 |
+
"الحالة": np.random.choice(
|
235 |
+
["جديد", "قيد المعالجة", "تم الشحن", "تم الاستلام", "مغلق"],
|
236 |
+
size=10,
|
237 |
+
p=[0.2, 0.3, 0.2, 0.2, 0.1]
|
238 |
+
)
|
239 |
+
}
|
240 |
+
|
241 |
+
# إنشاء DataFrame
|
242 |
+
po_df = pd.DataFrame(po_data)
|
243 |
+
|
244 |
+
# عرض فلاتر البحث
|
245 |
+
col1, col2 = st.columns(2)
|
246 |
+
|
247 |
+
with col1:
|
248 |
+
status_filter = st.selectbox(
|
249 |
+
"حالة أمر الشراء",
|
250 |
+
["الكل"] + sorted(po_df["الحالة"].unique().tolist())
|
251 |
+
)
|
252 |
+
|
253 |
+
with col2:
|
254 |
+
vendor_filter = st.selectbox(
|
255 |
+
"المورد",
|
256 |
+
["الكل"] + sorted(po_df["المورد"].unique().tolist())
|
257 |
+
)
|
258 |
+
|
259 |
+
# تطبيق الفلاتر
|
260 |
+
filtered_po = po_df.copy()
|
261 |
+
|
262 |
+
if status_filter != "الكل":
|
263 |
+
filtered_po = filtered_po[filtered_po["الحالة"] == status_filter]
|
264 |
+
|
265 |
+
if vendor_filter != "الكل":
|
266 |
+
filtered_po = filtered_po[filtered_po["المورد"] == vendor_filter]
|
267 |
+
|
268 |
+
# عرض أوامر الشراء المصفاة
|
269 |
+
st.dataframe(filtered_po, use_container_width=True)
|
270 |
+
|
271 |
+
# تحليلات أوامر الشراء
|
272 |
+
st.markdown("### تحليلات أوامر الشراء")
|
273 |
+
|
274 |
+
col1, col2 = st.columns(2)
|
275 |
+
|
276 |
+
with col1:
|
277 |
+
# توزيع أوامر الشراء حسب الحالة
|
278 |
+
status_counts = po_df.groupby("الحالة").size().reset_index(name="العدد")
|
279 |
+
|
280 |
+
fig1 = px.pie(
|
281 |
+
status_counts,
|
282 |
+
values="العدد",
|
283 |
+
names="الحالة",
|
284 |
+
title="توزيع أوامر الشراء حسب الحالة",
|
285 |
+
color_discrete_sequence=px.colors.qualitative.Bold
|
286 |
+
)
|
287 |
+
|
288 |
+
fig1.update_traces(textposition="inside", textinfo="percent+label")
|
289 |
+
|
290 |
+
st.plotly_chart(fig1, use_container_width=True)
|
291 |
+
|
292 |
+
with col2:
|
293 |
+
# قيمة أوامر الشراء حسب المورد
|
294 |
+
vendor_values = po_df.groupby("المورد")["القيمة (ريال)"].sum().reset_index()
|
295 |
+
vendor_values = vendor_values.sort_values("القيمة (ريال)", ascending=False).head(5)
|
296 |
+
|
297 |
+
fig2 = px.bar(
|
298 |
+
vendor_values,
|
299 |
+
x="المورد",
|
300 |
+
y="القيمة (ريال)",
|
301 |
+
title="أعلى 5 موردين حسب قيمة أوامر الشراء",
|
302 |
+
color="القيمة (ريال)",
|
303 |
+
color_continuous_scale="Viridis"
|
304 |
+
)
|
305 |
+
|
306 |
+
fig2.update_yaxes(title_text="القيمة (ريال)")
|
307 |
+
|
308 |
+
st.plotly_chart(fig2, use_container_width=True)
|
309 |
+
|
310 |
+
# إضافة أمر شراء جديد
|
311 |
+
st.markdown("### إضافة أمر شراء جديد")
|
312 |
+
|
313 |
+
with st.expander("إضافة أمر شراء جديد"):
|
314 |
+
col1, col2 = st.columns(2)
|
315 |
+
|
316 |
+
with col1:
|
317 |
+
new_vendor = st.selectbox("المورد", sorted(po_df["المورد"].unique().tolist()))
|
318 |
+
new_project = st.selectbox("المشروع", sorted(po_df["المشروع"].unique().tolist()))
|
319 |
+
new_value = st.number_input("القيمة (ريال)", min_value=1000, max_value=10000000, value=100000)
|
320 |
+
|
321 |
+
with col2:
|
322 |
+
new_delivery_date = st.date_input("تاريخ التسليم المتوقع", value=current_date + timedelta(days=30))
|
323 |
+
new_description = st.text_area("وصف الطلب", height=100)
|
324 |
+
|
325 |
+
if st.button("إضافة أمر الشراء"):
|
326 |
+
st.success(f"تم إضافة أمر الشراء بنجاح للمورد {new_vendor} بقيمة {new_value:,} ريال")
|
327 |
+
|
328 |
+
def show_internal_tenders():
|
329 |
+
"""
|
330 |
+
عرض المناقصات الداخلية
|
331 |
+
"""
|
332 |
+
st.markdown("## المناقصات الداخلية")
|
333 |
+
|
334 |
+
# إنشاء بيانات توضيحية للمناقصات الداخلية
|
335 |
+
current_date = datetime.now().date()
|
336 |
+
|
337 |
+
tenders_data = {
|
338 |
+
"رقم المناقصة": [f"IT-{2025}-{i:04d}" for i in range(1001, 1009)],
|
339 |
+
"العنوان": [
|
340 |
+
"توريد معدات بناء ثقيلة",
|
341 |
+
"شراء مواد إنشائية",
|
342 |
+
"خدمات نقل وشحن",
|
343 |
+
"توريد أنظمة تكييف",
|
344 |
+
"خدمات تركيب كهربائية",
|
345 |
+
"توريد محولات كهربائية",
|
346 |
+
"خدمات أمن وسلامة",
|
347 |
+
"توريد أنظمة مراقبة"
|
348 |
+
],
|
349 |
+
"المشروع": [
|
350 |
+
"مشروع توسعة شبكة الطرق", "بناء المدارس", "تطوير البنية التحتية",
|
351 |
+
"تحديث شبكة المياه", "بناء المستشفى التخصصي", "إنشاء مركز البيانات",
|
352 |
+
"توسعة المطار", "تطوير الحدائق العامة"
|
353 |
+
],
|
354 |
+
"تاريخ النشر": [current_date - timedelta(days=np.random.randint(5, 30)) for _ in range(8)],
|
355 |
+
"الموعد النهائي": [current_date + timedelta(days=np.random.randint(10, 45)) for _ in range(8)],
|
356 |
+
"القيمة التقديرية (ريال)": [
|
357 |
+
np.random.randint(200000, 10000000) for _ in range(8)
|
358 |
+
],
|
359 |
+
"عدد العروض المستلمة": [np.random.randint(0, 10) for _ in range(8)],
|
360 |
+
"الحالة": np.random.choice(
|
361 |
+
["مفتوحة", "مغلقة", "قيد التقييم", "تم الترسية", "ملغاة"],
|
362 |
+
size=8,
|
363 |
+
p=[0.4, 0.1, 0.2, 0.2, 0.1]
|
364 |
+
)
|
365 |
+
}
|
366 |
+
|
367 |
+
# إنشاء DataFrame
|
368 |
+
tenders_df = pd.DataFrame(tenders_data)
|
369 |
+
|
370 |
+
# عرض فلاتر البحث
|
371 |
+
col1, col2 = st.columns(2)
|
372 |
+
|
373 |
+
with col1:
|
374 |
+
status_filter = st.selectbox(
|
375 |
+
"حالة المناقصة",
|
376 |
+
["الكل"] + sorted(tenders_df["الحالة"].unique().tolist())
|
377 |
+
)
|
378 |
+
|
379 |
+
with col2:
|
380 |
+
project_filter = st.selectbox(
|
381 |
+
"المشروع",
|
382 |
+
["الكل"] + sorted(tenders_df["المشروع"].unique().tolist())
|
383 |
+
)
|
384 |
+
|
385 |
+
# تطبيق الفلاتر
|
386 |
+
filtered_tenders = tenders_df.copy()
|
387 |
+
|
388 |
+
if status_filter != "الكل":
|
389 |
+
filtered_tenders = filtered_tenders[filtered_tenders["الحالة"] == status_filter]
|
390 |
+
|
391 |
+
if project_filter != "الكل":
|
392 |
+
filtered_tenders = filtered_tenders[filtered_tenders["المشروع"] == project_filter]
|
393 |
+
|
394 |
+
# عرض المناقصات المصفاة
|
395 |
+
st.dataframe(filtered_tenders, use_container_width=True)
|
396 |
+
|
397 |
+
# تحليلات المناقصات
|
398 |
+
st.markdown("### تحليلات المناقصات الداخلية")
|
399 |
+
|
400 |
+
col1, col2 = st.columns(2)
|
401 |
+
|
402 |
+
with col1:
|
403 |
+
# توزيع المناقصات حسب الحالة
|
404 |
+
status_counts = tenders_df.groupby("الحالة").size().reset_index(name="العدد")
|
405 |
+
|
406 |
+
fig1 = px.pie(
|
407 |
+
status_counts,
|
408 |
+
values="العدد",
|
409 |
+
names="الحالة",
|
410 |
+
title="توزيع المناقصات حسب الحالة",
|
411 |
+
color_discrete_sequence=px.colors.qualitative.Bold
|
412 |
+
)
|
413 |
+
|
414 |
+
fig1.update_traces(textposition="inside", textinfo="percent+label")
|
415 |
+
|
416 |
+
st.plotly_chart(fig1, use_container_width=True)
|
417 |
+
|
418 |
+
with col2:
|
419 |
+
# متوسط عدد العروض حسب نوع المناقصة
|
420 |
+
tenders_df["نوع المناقصة"] = tenders_df["العنوان"].apply(
|
421 |
+
lambda x: "توريد" if "توريد" in x else "خدمات" if "خدمات" in x else "أخرى"
|
422 |
+
)
|
423 |
+
|
424 |
+
avg_offers = tenders_df.groupby("نوع المناقصة")["عدد العروض المستلمة"].mean().reset_index()
|
425 |
+
avg_offers["عدد العروض المستلمة"] = avg_offers["عدد العروض المستلمة"].round(1)
|
426 |
+
|
427 |
+
fig2 = px.bar(
|
428 |
+
avg_offers,
|
429 |
+
x="نوع المناقصة",
|
430 |
+
y="عدد العروض المستلمة",
|
431 |
+
title="متوسط عدد العروض حسب نوع المناقصة",
|
432 |
+
color="نوع المناقصة",
|
433 |
+
text="عدد العروض المستلمة"
|
434 |
+
)
|
435 |
+
|
436 |
+
fig2.update_traces(texttemplate="%{text}", textposition="outside")
|
437 |
+
|
438 |
+
st.plotly_chart(fig2, use_container_width=True)
|
439 |
+
|
440 |
+
# المناقصات القريبة من الإغلاق
|
441 |
+
st.markdown("### المناقصات القريبة من الموعد النهائي")
|
442 |
+
|
443 |
+
closing_soon = tenders_df[
|
444 |
+
(tenders_df["الموعد النهائي"] > current_date) &
|
445 |
+
(tenders_df["الموعد النهائي"] <= current_date + timedelta(days=7)) &
|
446 |
+
(tenders_df["الحالة"] == "مفتوحة")
|
447 |
+
].sort_values("الموعد النهائي")
|
448 |
+
|
449 |
+
if not closing_soon.empty:
|
450 |
+
for _, tender in closing_soon.iterrows():
|
451 |
+
days_left = (tender["الموعد النهائي"] - current_date).days
|
452 |
+
|
453 |
+
st.markdown(f"""
|
454 |
+
**{tender['رقم المناقصة']} - {tender['العنوان']}**
|
455 |
+
**المشروع:** {tender['المشروع']}
|
456 |
+
**الموعد النهائي:** {tender['الموعد النهائي'].strftime('%Y/%m/%d')} ({days_left} أيام متبقية)
|
457 |
+
**القيمة التقديرية:** {tender['القيمة التقديرية (ريال)']:,} ريال
|
458 |
+
**العروض المستلمة حتى الآن:** {tender['عدد العروض المستلمة']}
|
459 |
+
""")
|
460 |
+
st.markdown("---")
|
461 |
+
else:
|
462 |
+
st.info("لا توجد مناقصات على وشك الإغلاق خلال الأسبوع القادم")
|
463 |
+
|
464 |
+
def show_vendor_evaluation():
|
465 |
+
"""
|
466 |
+
عرض تقييم الموردين
|
467 |
+
"""
|
468 |
+
st.markdown("## تقييم الموردين")
|
469 |
+
|
470 |
+
# إنشاء بيانات توضيحية لتقييم الموردين
|
471 |
+
vendors_eval_data = {
|
472 |
+
"المورد": [
|
473 |
+
"شركة الصناعات السعودية", "مؤسسة الخليج للمقاولات", "شركة الرياض للإنشاءات",
|
474 |
+
"الشركة العربية للمعدات", "مصنع المنتجات الإسمنتية", "شركة تقنيات البناء",
|
475 |
+
"مؤسسة المدار للتوريدات", "شركة البنية التحتية المتكاملة", "مصنع الصلب السعودي",
|
476 |
+
"شركة الأنابيب الوطنية"
|
477 |
+
],
|
478 |
+
"الفئة": [
|
479 |
+
"مواد بناء", "مقاولات", "خدمات هندسية", "معدات", "مواد خام",
|
480 |
+
"تقنيات", "مواد متنوعة", "بنية تحتية", "صناعات معدنية", "أنابيب"
|
481 |
+
],
|
482 |
+
"جودة المنتجات (5)": [4.5, 3.8, 4.2, 3.2, 4.7, 3.5, 3.9, 4.3, 4.6, 4.1],
|
483 |
+
"الالتزام بالمواعيد (5)": [4.2, 3.5, 4.0, 2.8, 4.5, 3.7, 3.6, 4.4, 4.3, 3.9],
|
484 |
+
"التنافسية السعرية (5)": [3.8, 4.2, 3.5, 4.6, 3.7, 4.1, 4.4, 3.8, 3.6, 4.0],
|
485 |
+
"الاستجابة والتواصل (5)": [4.3, 3.9, 4.5, 3.5, 4.2, 3.6, 3.8, 4.1, 4.4, 4.0],
|
486 |
+
"نسبة المحتوى المحلي (%)": [85, 92, 78, 65, 100, 70, 88, 75, 95, 82],
|
487 |
+
"عدد المشاريع المنفذة": [12, 8, 10, 5, 7, 6, 4, 9, 11, 8]
|
488 |
+
}
|
489 |
+
|
490 |
+
# إنشاء DataFrame
|
491 |
+
vendors_eval_df = pd.DataFrame(vendors_eval_data)
|
492 |
+
|
493 |
+
# حساب التقييم العام
|
494 |
+
eval_weights = {
|
495 |
+
"جودة المنتجات (5)": 0.35,
|
496 |
+
"الالتزام بالمواعيد (5)": 0.25,
|
497 |
+
"التنافسية السعرية (5)": 0.2,
|
498 |
+
"الاستجابة والتواصل (5)": 0.2
|
499 |
+
}
|
500 |
+
|
501 |
+
# حساب التقييم المرجح
|
502 |
+
for col, weight in eval_weights.items():
|
503 |
+
vendors_eval_df[f"{col} (مرجح)"] = vendors_eval_df[col] * weight
|
504 |
+
|
505 |
+
vendors_eval_df["التقييم العام"] = vendors_eval_df[[f"{col} (مرجح)" for col in eval_weights.keys()]].sum(axis=1)
|
506 |
+
|
507 |
+
# تصنيف الموردين
|
508 |
+
conditions = [
|
509 |
+
(vendors_eval_df["التقييم العام"] >= 4.5),
|
510 |
+
(vendors_eval_df["التقييم العام"] >= 4.0),
|
511 |
+
(vendors_eval_df["التقييم العام"] >= 3.5),
|
512 |
+
(vendors_eval_df["التقييم العام"] >= 3.0),
|
513 |
+
(vendors_eval_df["التقييم العام"] < 3.0)
|
514 |
+
]
|
515 |
+
values = ["ممتاز", "جيد جداً", "جيد", "مقبول", "ضعيف"]
|
516 |
+
vendors_eval_df["التصنيف"] = np.select(conditions, values)
|
517 |
+
|
518 |
+
# عرض مقارنة الموردين
|
519 |
+
st.markdown("### مقارنة تقييمات الموردين")
|
520 |
+
|
521 |
+
selected_vendors = st.multiselect(
|
522 |
+
"اختر الموردين للمقارنة",
|
523 |
+
vendors_eval_df["المورد"].tolist(),
|
524 |
+
default=vendors_eval_df["المورد"].tolist()[:5]
|
525 |
+
)
|
526 |
+
|
527 |
+
if selected_vendors:
|
528 |
+
# تصفية البيانات
|
529 |
+
selected_df = vendors_eval_df[vendors_eval_df["المورد"].isin(selected_vendors)]
|
530 |
+
|
531 |
+
# تصفية البيانات
|
532 |
+
if selected_vendors:
|
533 |
+
selected_df = vendors_eval_df[vendors_eval_df["المورد"].isin(selected_vendors)]
|
534 |
+
st.dataframe(selected_df)
|
535 |
+
else:
|
536 |
+
st.write("يرجى اختيار مورد لعرض البيانات.")
|