Spaces:
Paused
Paused
Create web/pages/tender_analysis.py
Browse files- web/pages/tender_analysis.py +290 -0
web/pages/tender_analysis.py
ADDED
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import numpy as np
|
4 |
+
import plotly.express as px
|
5 |
+
from datetime import datetime
|
6 |
+
import os
|
7 |
+
|
8 |
+
def show_tender_analysis():
|
9 |
+
"""
|
10 |
+
عرض صفحة تحليل المناقصات
|
11 |
+
"""
|
12 |
+
st.subheader("تحليل المناقصات")
|
13 |
+
|
14 |
+
# تقسيم الشاشة إلى جزئين
|
15 |
+
col1, col2 = st.columns([1, 2])
|
16 |
+
|
17 |
+
with col1:
|
18 |
+
# قسم رفع الملفات
|
19 |
+
st.markdown("### رفع ملفات المناقصة")
|
20 |
+
|
21 |
+
uploaded_files = st.file_uploader(
|
22 |
+
"قم برفع ملفات المناقصة (PDF, DOCX, XLSX)",
|
23 |
+
type=["pdf", "docx", "xlsx", "csv", "json"],
|
24 |
+
accept_multiple_files=True
|
25 |
+
)
|
26 |
+
|
27 |
+
if uploaded_files:
|
28 |
+
st.session_state.uploaded_files = uploaded_files
|
29 |
+
st.success(f"تم رفع {len(uploaded_files)} ملفات بنجاح")
|
30 |
+
|
31 |
+
# عرض قائمة الملفات
|
32 |
+
st.markdown("### الملفات المرفوعة")
|
33 |
+
for file in uploaded_files:
|
34 |
+
st.markdown(f"- {file.name} ({file.size / 1024:.1f} KB)")
|
35 |
+
|
36 |
+
# خيارات التحليل
|
37 |
+
st.markdown("### خيارات التحليل")
|
38 |
+
|
39 |
+
analysis_options = st.multiselect(
|
40 |
+
"اختر أنواع التحليل",
|
41 |
+
[
|
42 |
+
"استخراج المتطلبات الرئيسية",
|
43 |
+
"تحليل التكاليف التقديرية",
|
44 |
+
"تحليل المخاطر",
|
45 |
+
"تحليل المحتوى المحلي",
|
46 |
+
"تحليل سلاسل الإمداد",
|
47 |
+
"التحليل الزمني",
|
48 |
+
"توقع احتمالية النجاح"
|
49 |
+
],
|
50 |
+
default=["استخراج المتطلبات الرئيسية", "تحليل التكاليف التقديرية"]
|
51 |
+
)
|
52 |
+
|
53 |
+
# زر بدء التحليل
|
54 |
+
if st.button("بدء التحليل"):
|
55 |
+
if not uploaded_files:
|
56 |
+
st.error("يرجى رفع ملفات المناقصة أولاً")
|
57 |
+
elif not analysis_options:
|
58 |
+
st.error("يرجى اختيار نوع التحليل المطلوب")
|
59 |
+
else:
|
60 |
+
# هنا سيتم استدعاء عمليات التحليل الفعلية
|
61 |
+
# نستخدم هنا بيانات توضيحية للعرض فقط
|
62 |
+
with st.spinner("جاري تحليل المناقصة... قد تستغرق العملية بضع دقائق..."):
|
63 |
+
# محاكاة وقت المعالجة
|
64 |
+
import time
|
65 |
+
time.sleep(2)
|
66 |
+
|
67 |
+
# تخزين نتائج التحليل في حالة الجلسة
|
68 |
+
st.session_state.analysis_results = {
|
69 |
+
"tender_id": "T-2025-" + str(np.random.randint(1000, 9999)),
|
70 |
+
"analyzed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
71 |
+
"requirements": [
|
72 |
+
"توريد وتركيب معدات البنية التحتية",
|
73 |
+
"صيانة الشبكات لمدة سنتين",
|
74 |
+
"تدريب الموظفين على الأنظمة الجديدة",
|
75 |
+
"توفير قطع الغيار اللازمة",
|
76 |
+
"الالتزام بمعايير الجودة ISO 9001"
|
77 |
+
],
|
78 |
+
"cost_estimate": {
|
79 |
+
"total": np.random.uniform(80, 150, 1)[0].round(2),
|
80 |
+
"breakdown": {
|
81 |
+
"مواد": np.random.uniform(30, 60, 1)[0].round(2),
|
82 |
+
"عمالة": np.random.uniform(20, 40, 1)[0].round(2),
|
83 |
+
"معدات": np.random.uniform(10, 30, 1)[0].round(2),
|
84 |
+
"إدارة": np.random.uniform(5, 15, 1)[0].round(2),
|
85 |
+
"أخرى": np.random.uniform(5, 10, 1)[0].round(2)
|
86 |
+
}
|
87 |
+
},
|
88 |
+
"risks": [
|
89 |
+
{"name": "تأخر التوريدات", "probability": 0.4, "impact": 0.7, "score": 0.28},
|
90 |
+
{"name": "تغيير المواصفات", "probability": 0.3, "impact": 0.6, "score": 0.18},
|
91 |
+
{"name": "نقص العمالة الماهرة", "probability": 0.5, "impact": 0.5, "score": 0.25},
|
92 |
+
{"name": "تقلبات أسعار المواد", "probability": 0.6, "impact": 0.4, "score": 0.24},
|
93 |
+
{"name": "ظروف جوية غير مناسبة", "probability": 0.2, "impact": 0.3, "score": 0.06}
|
94 |
+
],
|
95 |
+
"local_content": {
|
96 |
+
"estimated_percentage": np.random.uniform(50, 80, 1)[0].round(2),
|
97 |
+
"required_percentage": np.random.uniform(40, 60, 1)[0].round(2),
|
98 |
+
"breakdown": {
|
99 |
+
"عمالة محلية": np.random.uniform(60, 90, 1)[0].round(2),
|
100 |
+
"مواد محلية": np.random.uniform(40, 70, 1)[0].round(2),
|
101 |
+
"خدمات محلية": np.random.uniform(50, 80, 1)[0].round(2),
|
102 |
+
"تدريب وتطوير": np.random.uniform(30, 60, 1)[0].round(2)
|
103 |
+
}
|
104 |
+
},
|
105 |
+
"success_probability": np.random.uniform(60, 90, 1)[0].round(2)
|
106 |
+
}
|
107 |
+
|
108 |
+
st.success("تم الانتهاء من تحليل المناقصة بنجاح!")
|
109 |
+
|
110 |
+
with col2:
|
111 |
+
# عرض نتائج التحليل إذا كانت متوفرة
|
112 |
+
if "analysis_results" in st.session_state and st.session_state.analysis_results:
|
113 |
+
results = st.session_state.analysis_results
|
114 |
+
|
115 |
+
# عرض معلومات المناقصة
|
116 |
+
st.markdown("### معلومات المناقصة")
|
117 |
+
st.markdown(f"**رقم المناقصة:** {results['tender_id']}")
|
118 |
+
st.markdown(f"**تاريخ التحليل:** {results['analyzed_at']}")
|
119 |
+
|
120 |
+
# تبويب لعرض مختلف أنواع التحليل
|
121 |
+
tabs = st.tabs([
|
122 |
+
"المتطلبات",
|
123 |
+
"التكاليف",
|
124 |
+
"المخاطر",
|
125 |
+
"المحتوى المحلي",
|
126 |
+
"احتمالية النجاح"
|
127 |
+
])
|
128 |
+
|
129 |
+
# تبويب المتطلبات
|
130 |
+
with tabs[0]:
|
131 |
+
st.markdown("### المتطلبات الرئيسية للمناقصة")
|
132 |
+
for i, req in enumerate(results["requirements"]):
|
133 |
+
st.markdown(f"{i+1}. {req}")
|
134 |
+
|
135 |
+
# تبويب التكاليف
|
136 |
+
with tabs[1]:
|
137 |
+
st.markdown("### تحليل التكاليف التقديرية")
|
138 |
+
|
139 |
+
# إجمالي التكلفة
|
140 |
+
st.markdown(f"**إجمالي التكلفة التقديرية:** {results['cost_estimate']['total']} مليون ريال")
|
141 |
+
|
142 |
+
# رسم بياني لتوزيع التكاليف
|
143 |
+
cost_data = {
|
144 |
+
"الفئة": list(results["cost_estimate"]["breakdown"].keys()),
|
145 |
+
"القيمة (مليون ريال)": list(results["cost_estimate"]["breakdown"].values())
|
146 |
+
}
|
147 |
+
|
148 |
+
cost_df = pd.DataFrame(cost_data)
|
149 |
+
|
150 |
+
fig = px.pie(
|
151 |
+
cost_df,
|
152 |
+
values="القيمة (مليون ريال)",
|
153 |
+
names="الفئة",
|
154 |
+
title="توزيع التكاليف التقديرية",
|
155 |
+
color_discrete_sequence=px.colors.qualitative.Bold
|
156 |
+
)
|
157 |
+
|
158 |
+
st.plotly_chart(fig, use_container_width=True)
|
159 |
+
|
160 |
+
# تبويب المخاطر
|
161 |
+
with tabs[2]:
|
162 |
+
st.markdown("### تحليل المخاطر")
|
163 |
+
|
164 |
+
# جدول المخاطر
|
165 |
+
risk_data = pd.DataFrame(results["risks"])
|
166 |
+
risk_data.columns = ["المخاطرة", "الاحتمالية", "التأثير", "الدرجة"]
|
167 |
+
|
168 |
+
st.table(risk_data.style.format({
|
169 |
+
"الاحتمالية": "{:.1%}",
|
170 |
+
"التأثير": "{:.1%}",
|
171 |
+
"الدرجة": "{:.1%}"
|
172 |
+
}))
|
173 |
+
|
174 |
+
# مصفوفة المخاطر
|
175 |
+
st.markdown("### مصفوفة المخاطر")
|
176 |
+
|
177 |
+
fig = px.scatter(
|
178 |
+
risk_data,
|
179 |
+
x="الاحتمالية",
|
180 |
+
y="التأثير",
|
181 |
+
size="الدرجة",
|
182 |
+
text="المخاطرة",
|
183 |
+
size_max=60,
|
184 |
+
color="الدرجة",
|
185 |
+
color_continuous_scale=px.colors.sequential.Reds,
|
186 |
+
title="مصفوفة المخاطر",
|
187 |
+
range_x=[0, 1],
|
188 |
+
range_y=[0, 1]
|
189 |
+
)
|
190 |
+
|
191 |
+
fig.update_traces(textposition="top center")
|
192 |
+
fig.update_layout(
|
193 |
+
xaxis_title="احتمالية الحدوث",
|
194 |
+
yaxis_title="مستوى التأثير"
|
195 |
+
)
|
196 |
+
|
197 |
+
st.plotly_chart(fig, use_container_width=True)
|
198 |
+
|
199 |
+
# تبويب المحتوى المحلي
|
200 |
+
with tabs[3]:
|
201 |
+
st.markdown("### تحليل المحتوى المحلي")
|
202 |
+
|
203 |
+
# نسب المحتوى المحلي
|
204 |
+
est_pct = results["local_content"]["estimated_percentage"]
|
205 |
+
req_pct = results["local_content"]["required_percentage"]
|
206 |
+
|
207 |
+
col1, col2 = st.columns(2)
|
208 |
+
|
209 |
+
with col1:
|
210 |
+
# متطلبات المحتوى المحلي
|
211 |
+
st.markdown(f"**نسبة المحتوى المحلي المطلوبة:** {req_pct}%")
|
212 |
+
st.progress(req_pct / 100)
|
213 |
+
|
214 |
+
with col2:
|
215 |
+
# النسبة المتوقعة
|
216 |
+
st.markdown(f"**نسبة المحتوى المحلي المتوقعة:** {est_pct}%")
|
217 |
+
st.progress(est_pct / 100)
|
218 |
+
|
219 |
+
# حالة المحتوى المحلي (هل يلبي المتطلبات)
|
220 |
+
if est_pct >= req_pct:
|
221 |
+
st.success(f"المحتوى المحلي المتوقع يتجاوز المتطلبات بنسبة {est_pct - req_pct:.1f}%")
|
222 |
+
else:
|
223 |
+
st.error(f"المحتوى المحلي المتوقع أقل من المتطلبات بنسبة {req_pct - est_pct:.1f}%")
|
224 |
+
|
225 |
+
# رسم بياني لمكونات المحتوى المحلي
|
226 |
+
local_data = {
|
227 |
+
"الفئة": list(results["local_content"]["breakdown"].keys()),
|
228 |
+
"النسبة (%)": list(results["local_content"]["breakdown"].values())
|
229 |
+
}
|
230 |
+
|
231 |
+
local_df = pd.DataFrame(local_data)
|
232 |
+
|
233 |
+
fig = px.bar(
|
234 |
+
local_df,
|
235 |
+
x="الفئة",
|
236 |
+
y="النسبة (%)",
|
237 |
+
color="النسبة (%)",
|
238 |
+
color_continuous_scale=px.colors.sequential.Viridis,
|
239 |
+
title="مكونات المحتوى المحلي حسب الفئة"
|
240 |
+
)
|
241 |
+
|
242 |
+
st.plotly_chart(fig, use_container_width=True)
|
243 |
+
|
244 |
+
# تبويب احتمالية النجاح
|
245 |
+
with tabs[4]:
|
246 |
+
st.markdown("### توقع احتمالية النجاح")
|
247 |
+
|
248 |
+
# عرض احتمالية النجاح
|
249 |
+
success_prob = results["success_probability"]
|
250 |
+
|
251 |
+
# اختيار اللون حسب النسبة
|
252 |
+
color = "green" if success_prob >= 80 else "orange" if success_prob >= 60 else "red"
|
253 |
+
|
254 |
+
st.markdown(f"<h1 style='text-align: center; color: {color};'>{success_prob}%</h1>", unsafe_allow_html=True)
|
255 |
+
st.progress(success_prob / 100)
|
256 |
+
|
257 |
+
# نصائح لتحسين الاحتمالية
|
258 |
+
st.markdown("### توصيات لتحسين فرص النجاح")
|
259 |
+
|
260 |
+
recommendations = [
|
261 |
+
"زيادة نسبة المحتوى المحلي بنسبة 5-10%",
|
262 |
+
"تعزيز فريق المشروع بخبرات في مجال التقنية",
|
263 |
+
"البحث عن موردين محليين بديلين للمواد الرئيسية",
|
264 |
+
"وضع خطة واضحة للتعامل مع المخاطر ذات التأثير العالي",
|
265 |
+
"تقديم حلول مبتكرة في المجالات التقنية"
|
266 |
+
]
|
267 |
+
|
268 |
+
for rec in recommendations:
|
269 |
+
st.markdown(f"- {rec}")
|
270 |
+
|
271 |
+
# زر لحفظ التقرير
|
272 |
+
if st.button("حفظ تقرير التحليل"):
|
273 |
+
st.session_state.latest_analysis = results
|
274 |
+
st.success("تم حفظ تقرير التحليل بنجاح!")
|
275 |
+
else:
|
276 |
+
# توجيهات للمستخدم
|
277 |
+
st.info("قم برفع ملفات المناقصة واختر خيارات التحليل المطلوبة، ثم اضغط على زر 'بدء التحليل' لعرض النتائج هنا.")
|
278 |
+
|
279 |
+
# عرض مثال توضيحي
|
280 |
+
st.markdown("### مثال توضيحي لنتائج التحليل")
|
281 |
+
st.image("https://via.placeholder.com/800x500?text=مثال+لنتائج+تحليل+المناقصة", caption="مثال لنتائج تحليل المناقصة")
|
282 |
+
|
283 |
+
# اختبار مستقل للصفحة
|
284 |
+
if __name__ == "__main__":
|
285 |
+
st.set_page_config(
|
286 |
+
page_title="نظام تحليل المناقصات - تحليل المناقصات",
|
287 |
+
page_icon="📊",
|
288 |
+
layout="wide",
|
289 |
+
)
|
290 |
+
show_tender_analysis()
|