Delete utils
Browse files- utils/assets/logo.svg +0 -84
- utils/components/about_system.py +0 -262
- utils/components/credits.py +0 -115
- utils/components/header.py +0 -146
- utils/components/sidebar.py +0 -171
- utils/components/system_innovation.py +0 -81
- utils/css/enhanced.css +0 -400
- utils/css/main.css +0 -405
- utils/css/rtl.css +0 -223
- utils/dwg_handler.py +0 -223
- utils/excel_handler.py +0 -486
- utils/helpers.py +0 -332
- utils/helpers/__init__.py +0 -70
- utils/helpers/utils.py +0 -282
- utils/pdf_handler.py +0 -847
- utils/pdf_handler.py.bak +0 -476
- utils/session_state.py +0 -247
utils/assets/logo.svg
DELETED
utils/components/about_system.py
DELETED
@@ -1,262 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
مكون عرض معلومات حول النظام
|
3 |
-
"""
|
4 |
-
|
5 |
-
import streamlit as st
|
6 |
-
|
7 |
-
|
8 |
-
def display_about_system():
|
9 |
-
"""
|
10 |
-
عرض معلومات حول النظام وميزاته
|
11 |
-
"""
|
12 |
-
# مربع معلومات النظام
|
13 |
-
st.markdown("""
|
14 |
-
<div class="about-system">
|
15 |
-
<h2>نبذة عن النظام</h2>
|
16 |
-
<p>
|
17 |
-
نظام WAHBi AI هو نظام متكامل لتحليل العقود والمناقصات باستخدام تقنيات الذكاء الاصطناعي المتقدمة.
|
18 |
-
تم تطوير النظام خصيصًا لشركة شبه الجزيرة للمقاولات لتمكينها من تحليل وثائق المناقصات، وتسعير المشاريع، وتقييم المخاطر،
|
19 |
-
وإدارة الموارد بكفاءة عالية.
|
20 |
-
</p>
|
21 |
-
|
22 |
-
<h2>المميزات الرئيسية</h2>
|
23 |
-
<ul>
|
24 |
-
<li><strong>تحليل المستندات ذكيًا</strong> - استخراج البنود والكميات والمخاطر من المستندات تلقائيًا</li>
|
25 |
-
<li><strong>حاسبة تكاليف البناء المتكاملة</strong> - حساب التكاليف التفصيلية للمواد الخام والمعدات والعمالة والمصاريف الإدارية وهوامش الربح</li>
|
26 |
-
<li><strong>تحليل الأسعار غير المتزنة</strong> - تحليل ومقارنة الأسعار مع متوسطات السوق لتحقيق أقصى استفادة</li>
|
27 |
-
<li><strong>تتبع المعدات والموارد</strong> - متابعة وتنظيم موارد الشركة وتخصيصها للمشاريع المختلفة</li>
|
28 |
-
<li><strong>تحليل المخاطر</strong> - تحديد المخاطر المحتملة في المشاريع وتقييم تأثيرها واقتراح إجراءات التخفيف</li>
|
29 |
-
<li><strong>مؤشرات أداء المشاريع</strong> - متابعة الأداء المالي والفني للمشاريع وقياس الإنجاز</li>
|
30 |
-
<li><strong>حاسبة المحتوى المحلي</strong> - حساب وتحسين نسبة المحتوى المحلي في المشاريع لتلبية متطلبات المملكة</li>
|
31 |
-
<li><strong>التنبؤ بأسعار المواد</strong> - استخدام نماذج التعلم الآلي للتنبؤ بتغيرات الأسعار المستقبلية</li>
|
32 |
-
<li><strong>مؤقت مواعيد التسليم</strong> - متابعة مواعيد بدء الدراسة ومواعيد تسليم العروض للمشاريع</li>
|
33 |
-
<li><strong>دعم اللغة العربية</strong> - واجهة مستخدم باللغة العربية مع إمكانية التبديل للغة الإنجليزية</li>
|
34 |
-
<li><strong>توافق متعدد الأجهزة</strong> - يعمل على جميع الأجهزة (أيفون، أندرويد، أجهزة لوحية، حواسيب مكتبية)</li>
|
35 |
-
</ul>
|
36 |
-
|
37 |
-
<h2>معلومات الاتصال</h2>
|
38 |
-
<div class="contact-info">
|
39 |
-
<p><strong>العنوان:</strong> المملكة العربية السعودية - الرياض - حي الملز - شارع السبالة</p>
|
40 |
-
<p><strong>البريد الإلكتروني:</strong> [email protected]</p>
|
41 |
-
<p><strong>هاتف:</strong> +966 12 345 6789</p>
|
42 |
-
<p><strong>الموقع الإلكتروني:</strong> www.peninsula-contracting.com</p>
|
43 |
-
</div>
|
44 |
-
|
45 |
-
<h2>فريق التطوير</h2>
|
46 |
-
<p>
|
47 |
-
تم تطوير هذا النظام بواسطة فريق متخصص من المهندسين والمطورين بقيادة م. بدر وهبي،
|
48 |
-
بالتعاون مع فريق التسعير والمشاريع في شركة شبه الجزيرة للمقاولات.
|
49 |
-
</p>
|
50 |
-
|
51 |
-
<h2>الإصدار والترخيص</h2>
|
52 |
-
<p>
|
53 |
-
<strong>رقم الإصدار:</strong> 2.5.0 (مارس 2025)<br>
|
54 |
-
<strong>الترخيص:</strong> جميع الحقوق محفوظة © 2025 شركة شبه الجزيرة للمقاولات
|
55 |
-
</p>
|
56 |
-
</div>
|
57 |
-
""", unsafe_allow_html=True)
|
58 |
-
|
59 |
-
# أضف مساحة أسفل المحتوى
|
60 |
-
st.markdown("<br><br>", unsafe_allow_html=True)
|
61 |
-
|
62 |
-
# إضافة أزرار تفاعلية
|
63 |
-
col1, col2, col3 = st.columns(3)
|
64 |
-
|
65 |
-
with col1:
|
66 |
-
st.button("تحميل دليل المستخدم")
|
67 |
-
|
68 |
-
with col2:
|
69 |
-
st.button("الدعم الفني")
|
70 |
-
|
71 |
-
with col3:
|
72 |
-
st.button("التحديثات والمزايا القادمة")
|
73 |
-
|
74 |
-
|
75 |
-
def display_settings():
|
76 |
-
"""
|
77 |
-
عرض صفحة الإعدادات
|
78 |
-
"""
|
79 |
-
st.markdown("<h2 class='module-title'>إعدادات النظام</h2>", unsafe_allow_html=True)
|
80 |
-
|
81 |
-
# تقسيم الصفحة إلى قسمين
|
82 |
-
col1, col2 = st.columns(2)
|
83 |
-
|
84 |
-
with col1:
|
85 |
-
st.markdown("""
|
86 |
-
<div class="settings-form">
|
87 |
-
<div class="settings-group">
|
88 |
-
<h3>خيارات اللغة</h3>
|
89 |
-
<div class="settings-item">
|
90 |
-
<label>لغة الواجهة</label>
|
91 |
-
</div>
|
92 |
-
</div>
|
93 |
-
</div>
|
94 |
-
""", unsafe_allow_html=True)
|
95 |
-
|
96 |
-
language = st.selectbox(
|
97 |
-
"اختر لغة الواجهة",
|
98 |
-
options=["العربية", "English"],
|
99 |
-
index=0,
|
100 |
-
label_visibility="collapsed"
|
101 |
-
)
|
102 |
-
|
103 |
-
st.markdown("""
|
104 |
-
<div class="settings-form">
|
105 |
-
<div class="settings-group">
|
106 |
-
<h3>الإشعارات</h3>
|
107 |
-
<div class="settings-item">
|
108 |
-
<label>إعدادات الإشعارات</label>
|
109 |
-
</div>
|
110 |
-
</div>
|
111 |
-
</div>
|
112 |
-
""", unsafe_allow_html=True)
|
113 |
-
|
114 |
-
notifications_email = st.checkbox("إشعارات البريد الإلكتروني", value=True)
|
115 |
-
notifications_sms = st.checkbox("إشعارات الرسائل النصية SMS", value=False)
|
116 |
-
notifications_system = st.checkbox("إشعارات النظام", value=True)
|
117 |
-
|
118 |
-
if st.button("حفظ الإعدادات"):
|
119 |
-
st.success("تم حفظ الإعدادات بنجاح")
|
120 |
-
|
121 |
-
with col2:
|
122 |
-
st.markdown("""
|
123 |
-
<div class="settings-form">
|
124 |
-
<div class="settings-group">
|
125 |
-
<h3>معلومات الحساب</h3>
|
126 |
-
</div>
|
127 |
-
</div>
|
128 |
-
""", unsafe_allow_html=True)
|
129 |
-
|
130 |
-
username = st.text_input("اسم المستخدم", value="admin")
|
131 |
-
email = st.text_input("البريد الإلكتروني", value="[email protected]")
|
132 |
-
|
133 |
-
st.markdown("""
|
134 |
-
<div class="settings-form" style="margin-top: 20px;">
|
135 |
-
<div class="settings-group">
|
136 |
-
<h3>تغيير كلمة المرور</h3>
|
137 |
-
</div>
|
138 |
-
</div>
|
139 |
-
""", unsafe_allow_html=True)
|
140 |
-
|
141 |
-
current_password = st.text_input("كلمة المرور الحالية", type="password")
|
142 |
-
new_password = st.text_input("كلمة المرور الجديدة", type="password")
|
143 |
-
confirm_password = st.text_input("تأكيد كلمة المرور الجديدة", type="password")
|
144 |
-
|
145 |
-
if st.button("تغيير كلمة المرور"):
|
146 |
-
if not current_password or not new_password or not confirm_password:
|
147 |
-
st.error("يرجى ملء جميع الحقول")
|
148 |
-
elif new_password != confirm_password:
|
149 |
-
st.error("كلمات المرور غير متطابقة")
|
150 |
-
else:
|
151 |
-
st.success("تم تغيير كلمة المرور بنجاح")
|
152 |
-
|
153 |
-
# إضافة مزيد من الإعدادات
|
154 |
-
st.markdown("<hr>", unsafe_allow_html=True)
|
155 |
-
st.markdown("<h3>إعدادات النظام المتقدمة</h3>", unsafe_allow_html=True)
|
156 |
-
|
157 |
-
col3, col4 = st.columns(2)
|
158 |
-
|
159 |
-
with col3:
|
160 |
-
theme = st.selectbox(
|
161 |
-
"سمة النظام",
|
162 |
-
options=["الافتراضية", "الوضع الفاتح", "الوضع الداكن"]
|
163 |
-
)
|
164 |
-
|
165 |
-
date_format = st.selectbox(
|
166 |
-
"تنسيق التاريخ",
|
167 |
-
options=["DD/MM/YYYY", "MM/DD/YYYY", "YYYY-MM-DD"]
|
168 |
-
)
|
169 |
-
|
170 |
-
with col4:
|
171 |
-
currency = st.selectbox(
|
172 |
-
"العملة الافتراضية",
|
173 |
-
options=["ريال سعودي (SAR)", "دولار أمريكي (USD)", "يورو (EUR)"]
|
174 |
-
)
|
175 |
-
|
176 |
-
notifications_frequency = st.selectbox(
|
177 |
-
"تكرار الإشعارات",
|
178 |
-
options=["فوري", "يومي", "أسبوعي"]
|
179 |
-
)
|
180 |
-
|
181 |
-
|
182 |
-
def display_countdown_timer():
|
183 |
-
"""
|
184 |
-
عرض مؤقت العد التنازلي للمواعيد النهائية
|
185 |
-
"""
|
186 |
-
st.markdown("<h3>مواعيد المناقصات</h3>", unsafe_allow_html=True)
|
187 |
-
|
188 |
-
# بيانات المواعيد
|
189 |
-
deadlines = [
|
190 |
-
{
|
191 |
-
"name": "مناقصة توسعة مستشفى الملك فهد",
|
192 |
-
"submission_date": "15 أبريل 2025",
|
193 |
-
"days_left": 15,
|
194 |
-
"start_date": "1 مارس 2025"
|
195 |
-
},
|
196 |
-
{
|
197 |
-
"name": "مناقصة إنشاء مبنى كلية الطب",
|
198 |
-
"submission_date": "30 مارس 2025",
|
199 |
-
"days_left": 0,
|
200 |
-
"start_date": "15 فبراير 2025"
|
201 |
-
},
|
202 |
-
{
|
203 |
-
"name": "مناقصة طريق الدائري الشمالي",
|
204 |
-
"submission_date": "10 مايو 2025",
|
205 |
-
"days_left": 40,
|
206 |
-
"start_date": "5 مارس 2025"
|
207 |
-
}
|
208 |
-
]
|
209 |
-
|
210 |
-
for i, deadline in enumerate(deadlines):
|
211 |
-
# تحديد لون المؤقت بناءً على عدد الأيام المتبقية
|
212 |
-
color_class = "danger" if deadline["days_left"] <= 5 else "warning" if deadline["days_left"] <= 15 else "success"
|
213 |
-
|
214 |
-
# عرض معلومات الموعد والمؤقت
|
215 |
-
st.markdown(f"""
|
216 |
-
<div class="card" style="margin-bottom: 15px;">
|
217 |
-
<h4>{deadline["name"]}</h4>
|
218 |
-
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
|
219 |
-
<div>
|
220 |
-
<small style="color: var(--text-light);">بدء الدراسة: {deadline["start_date"]}</small>
|
221 |
-
</div>
|
222 |
-
<div>
|
223 |
-
<small style="color: var(--text-light);">تاريخ التسليم: {deadline["submission_date"]}</small>
|
224 |
-
</div>
|
225 |
-
</div>
|
226 |
-
<div class="progress" style="margin-bottom: 5px;">
|
227 |
-
<div class="progress-bar progress-{color_class}" style="width: {100 - min(100, deadline["days_left"] * 2)}%;"></div>
|
228 |
-
</div>
|
229 |
-
<div style="display: flex; justify-content: space-between; align-items: center;">
|
230 |
-
<div>
|
231 |
-
<small>متبقي: <span class="bold">{deadline["days_left"]} أيام</span></small>
|
232 |
-
</div>
|
233 |
-
<div>
|
234 |
-
<button class="btn-secondary btn-sm">تفاصيل المناقصة</button>
|
235 |
-
</div>
|
236 |
-
</div>
|
237 |
-
</div>
|
238 |
-
""", unsafe_allow_html=True)
|
239 |
-
|
240 |
-
# عرض مؤقت تفصيلي للمناقصة الأولى
|
241 |
-
st.markdown("<h4>العد التنازلي للتسليم</h4>", unsafe_allow_html=True)
|
242 |
-
|
243 |
-
st.markdown("""
|
244 |
-
<div class="countdown-timer">
|
245 |
-
<div class="time-block">
|
246 |
-
<div class="time-value">15</div>
|
247 |
-
<div class="time-label">يوم</div>
|
248 |
-
</div>
|
249 |
-
<div class="time-block">
|
250 |
-
<div class="time-value">08</div>
|
251 |
-
<div class="time-label">ساعة</div>
|
252 |
-
</div>
|
253 |
-
<div class="time-block">
|
254 |
-
<div class="time-value">45</div>
|
255 |
-
<div class="time-label">دقيقة</div>
|
256 |
-
</div>
|
257 |
-
<div class="time-block">
|
258 |
-
<div class="time-value">20</div>
|
259 |
-
<div class="time-label">ثانية</div>
|
260 |
-
</div>
|
261 |
-
</div>
|
262 |
-
""", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/components/credits.py
DELETED
@@ -1,115 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
مكون عرض معلومات فريق التطوير
|
3 |
-
"""
|
4 |
-
|
5 |
-
import streamlit as st
|
6 |
-
|
7 |
-
# تصدير واضح للدالة لجعلها متاحة للاستيراد
|
8 |
-
__all__ = ['render_credits', 'display_credits']
|
9 |
-
|
10 |
-
|
11 |
-
def render_credits():
|
12 |
-
"""
|
13 |
-
عرض معلومات فريق التطوير (واجهة للاستخدام مع وحدات النظام المختلفة)
|
14 |
-
"""
|
15 |
-
display_credits()
|
16 |
-
|
17 |
-
|
18 |
-
def display_credits():
|
19 |
-
"""
|
20 |
-
عرض معلومات فريق التطوير
|
21 |
-
"""
|
22 |
-
# تعريف بيانات الفريق
|
23 |
-
team_members = [
|
24 |
-
{
|
25 |
-
"name": "م بدر وهبي",
|
26 |
-
"role": "مدير المشروع",
|
27 |
-
"image": "badr.jpg",
|
28 |
-
"bio": "مهندس برمجيات ذو خبرة 15 عامًا في تطوير أنظمة المقاولات والتسعير"
|
29 |
-
},
|
30 |
-
{
|
31 |
-
"name": "م تامر الجوهري",
|
32 |
-
"role": "مهندسة الذكاء الاصطناعي",
|
33 |
-
"image": "tamer.jpg",
|
34 |
-
"bio": "متخصصة في معالجة اللغة العربية الطبيعية وتحليل البيانات"
|
35 |
-
},
|
36 |
-
{
|
37 |
-
"name": "م اسلام عيسي",
|
38 |
-
"role": "أخصائي تطوير التسعير",
|
39 |
-
"image": "Islam.jpg",
|
40 |
-
"bio": "خبير في أنظمة التسعير المتقدمة والتحليل المالي للمشاريع"
|
41 |
-
}
|
42 |
-
]
|
43 |
-
|
44 |
-
# عرض معلومات الفريق في صفوف
|
45 |
-
# كل صف يحتوي على 3 أعضاء
|
46 |
-
cols_per_row = 3
|
47 |
-
|
48 |
-
for i in range(0, len(team_members), cols_per_row):
|
49 |
-
# إنشاء الأعمدة
|
50 |
-
cols = st.columns(cols_per_row)
|
51 |
-
|
52 |
-
# عرض الأعضاء في هذا الصف
|
53 |
-
for j in range(cols_per_row):
|
54 |
-
idx = i + j
|
55 |
-
if idx < len(team_members):
|
56 |
-
member = team_members[idx]
|
57 |
-
|
58 |
-
with cols[j]:
|
59 |
-
# عرض صورة العضو (استخدام صورة افتراضية إذا لم تكن متوفرة)
|
60 |
-
try:
|
61 |
-
st.image(f"static/images/team/{member['image']}", width=150)
|
62 |
-
except:
|
63 |
-
# استخدام الأحرف الأولى من الاسم كصورة افتراضية
|
64 |
-
initials = ''.join([name[0] for name in member['name'].split() if name.startswith('م.') == False])
|
65 |
-
st.markdown(f"""
|
66 |
-
<div class="avatar">
|
67 |
-
<span>{initials}</span>
|
68 |
-
</div>
|
69 |
-
""", unsafe_allow_html=True)
|
70 |
-
|
71 |
-
# عرض معلومات العضو
|
72 |
-
st.markdown(f"""
|
73 |
-
<div class="team-member">
|
74 |
-
<h3>{member['name']}</h3>
|
75 |
-
<h4>{member['role']}</h4>
|
76 |
-
<p>{member['bio']}</p>
|
77 |
-
</div>
|
78 |
-
""", unsafe_allow_html=True)
|
79 |
-
|
80 |
-
# إضافة أسلوب CSS للعرض
|
81 |
-
st.markdown("""
|
82 |
-
<style>
|
83 |
-
.team-member {
|
84 |
-
text-align: center;
|
85 |
-
margin-bottom: 20px;
|
86 |
-
}
|
87 |
-
.team-member h3 {
|
88 |
-
color: #333;
|
89 |
-
margin-bottom: 5px;
|
90 |
-
font-size: 18px;
|
91 |
-
}
|
92 |
-
.team-member h4 {
|
93 |
-
color: #ff9a3c;
|
94 |
-
margin-top: 0;
|
95 |
-
margin-bottom: 10px;
|
96 |
-
font-size: 14px;
|
97 |
-
}
|
98 |
-
.team-member p {
|
99 |
-
color: #666;
|
100 |
-
font-size: 12px;
|
101 |
-
}
|
102 |
-
.avatar {
|
103 |
-
background-color: #ff9a3c;
|
104 |
-
color: white;
|
105 |
-
width: 100px;
|
106 |
-
height: 100px;
|
107 |
-
border-radius: 50%;
|
108 |
-
display: flex;
|
109 |
-
justify-content: center;
|
110 |
-
align-items: center;
|
111 |
-
margin: 0 auto 15px auto;
|
112 |
-
font-size: 36px;
|
113 |
-
}
|
114 |
-
</style>
|
115 |
-
""", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/components/header.py
DELETED
@@ -1,146 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
مكون ترويسة الصفحة المطور 2025
|
3 |
-
"""
|
4 |
-
|
5 |
-
import streamlit as st
|
6 |
-
from datetime import datetime
|
7 |
-
import config
|
8 |
-
|
9 |
-
|
10 |
-
def render_header(page_title=None):
|
11 |
-
"""
|
12 |
-
عرض ترويسة الصفحة المحسنة
|
13 |
-
|
14 |
-
الوسيطات:
|
15 |
-
page_title: عنوان الصفحة المعروضة (اختياري)
|
16 |
-
"""
|
17 |
-
# إنشاء مكون الترويسة باستخدام HTML
|
18 |
-
title_display = "نظام تحليل العقود والمناقصات"
|
19 |
-
# إذا تم تمرير عنوان للصفحة، قم بإضافته للعنوان الرئيسي
|
20 |
-
if page_title:
|
21 |
-
title_display = f"نظام تحليل العقود والمناقصات: {page_title}"
|
22 |
-
|
23 |
-
header_html = """
|
24 |
-
<div class="header-container">
|
25 |
-
<div class="header-title">
|
26 |
-
<div class="logo">
|
27 |
-
<span class="logo-text">WAHBi AI</span>
|
28 |
-
</div>
|
29 |
-
<h1>{title}</h1>
|
30 |
-
<p>الحلول الشاملة للتسعير والتحليل بالذكاء الاصطناعي - شركة شبه الجزيرة للمقاولات</p>
|
31 |
-
</div>
|
32 |
-
<div class="header-info">
|
33 |
-
<div class="date-box">
|
34 |
-
<div class="date-day">{day}</div>
|
35 |
-
<div class="date-info">
|
36 |
-
<div class="date-month">{month}</div>
|
37 |
-
<div class="date-year">{year}</div>
|
38 |
-
</div>
|
39 |
-
</div>
|
40 |
-
</div>
|
41 |
-
</div>
|
42 |
-
"""
|
43 |
-
|
44 |
-
# الحصول على معلومات التاريخ الحالي
|
45 |
-
today = datetime.now()
|
46 |
-
day = today.day
|
47 |
-
month_names = [
|
48 |
-
"يناير", "فبراير", "مارس", "إبريل", "مايو", "يونيو",
|
49 |
-
"يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"
|
50 |
-
]
|
51 |
-
month = month_names[today.month - 1]
|
52 |
-
year = today.year
|
53 |
-
|
54 |
-
# استبدال القيم في قالب HTML
|
55 |
-
header_html = header_html.format(title=title_display, day=day, month=month, year=year)
|
56 |
-
|
57 |
-
# عرض الترويسة
|
58 |
-
st.markdown(header_html, unsafe_allow_html=True)
|
59 |
-
|
60 |
-
# إضافة شريط التنقل الرئيسي
|
61 |
-
if 'is_authenticated' in st.session_state and st.session_state.is_authenticated:
|
62 |
-
render_navigation_menu()
|
63 |
-
else:
|
64 |
-
# عرض قائمة التنقل المختصرة للمستخدمين غير المسجلين
|
65 |
-
render_basic_navigation_menu()
|
66 |
-
|
67 |
-
# إضافة خط فاصل
|
68 |
-
st.markdown("<hr>", unsafe_allow_html=True)
|
69 |
-
|
70 |
-
|
71 |
-
def render_navigation_menu():
|
72 |
-
"""
|
73 |
-
عرض قائمة التنقل الرئيسية المحسنة
|
74 |
-
"""
|
75 |
-
# إنشاء قائمة التنقل المختصرة
|
76 |
-
menu_items = [
|
77 |
-
{"icon": "🏠", "label": "الرئيسية", "url": "/?page=home"},
|
78 |
-
{"icon": "📊", "label": "لوحة المعلومات", "url": "/?page=dashboard"},
|
79 |
-
{"icon": "📝", "label": "المناقصات", "url": "/?page=tenders"},
|
80 |
-
{"icon": "💰", "label": "التسعير", "url": "/?page=pricing"},
|
81 |
-
{"icon": "📈", "label": "التقارير", "url": "/?page=reports"},
|
82 |
-
{"icon": "⚙️", "label": "الإعدادات", "url": "/?page=settings"},
|
83 |
-
{"icon": "❓", "label": "المساعدة", "url": "/?page=help"},
|
84 |
-
{"icon": "ℹ️", "label": "حول النظام", "url": "/?page=about"},
|
85 |
-
]
|
86 |
-
|
87 |
-
# إنشاء قائمة HTML
|
88 |
-
menu_html = """
|
89 |
-
<div class="nav-menu">
|
90 |
-
<ul>
|
91 |
-
"""
|
92 |
-
|
93 |
-
for item in menu_items:
|
94 |
-
menu_html += f"""
|
95 |
-
<li>
|
96 |
-
<a href="{item['url']}">
|
97 |
-
<span class="nav-icon">{item['icon']}</span>
|
98 |
-
<span class="nav-label">{item['label']}</span>
|
99 |
-
</a>
|
100 |
-
</li>
|
101 |
-
"""
|
102 |
-
|
103 |
-
menu_html += """
|
104 |
-
</ul>
|
105 |
-
</div>
|
106 |
-
"""
|
107 |
-
|
108 |
-
# عرض قائمة التنقل
|
109 |
-
st.markdown(menu_html, unsafe_allow_html=True)
|
110 |
-
|
111 |
-
|
112 |
-
def render_basic_navigation_menu():
|
113 |
-
"""
|
114 |
-
عرض قائمة تنقل بسيطة للمستخدمين غير المسجلين
|
115 |
-
"""
|
116 |
-
# إنشاء قائمة التنقل المختصرة
|
117 |
-
menu_items = [
|
118 |
-
{"icon": "🏠", "label": "الرئيسية", "url": "/?page=home"},
|
119 |
-
{"icon": "🔐", "label": "تسجيل الدخول", "url": "/?page=login"},
|
120 |
-
{"icon": "❓", "label": "المساعدة", "url": "/?page=help"},
|
121 |
-
{"icon": "ℹ️", "label": "حول النظام", "url": "/?page=about"},
|
122 |
-
]
|
123 |
-
|
124 |
-
# إنشاء قائمة HTML
|
125 |
-
menu_html = """
|
126 |
-
<div class="nav-menu">
|
127 |
-
<ul>
|
128 |
-
"""
|
129 |
-
|
130 |
-
for item in menu_items:
|
131 |
-
menu_html += f"""
|
132 |
-
<li>
|
133 |
-
<a href="{item['url']}">
|
134 |
-
<span class="nav-icon">{item['icon']}</span>
|
135 |
-
<span class="nav-label">{item['label']}</span>
|
136 |
-
</a>
|
137 |
-
</li>
|
138 |
-
"""
|
139 |
-
|
140 |
-
menu_html += """
|
141 |
-
</ul>
|
142 |
-
</div>
|
143 |
-
"""
|
144 |
-
|
145 |
-
# عرض قائمة التنقل
|
146 |
-
st.markdown(menu_html, unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/components/sidebar.py
DELETED
@@ -1,171 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
مكون الشريط الجانبي - تصميم محسن مع دعم كامل للغة العربية
|
3 |
-
"""
|
4 |
-
|
5 |
-
import streamlit as st
|
6 |
-
from datetime import datetime
|
7 |
-
import os
|
8 |
-
import config
|
9 |
-
from streamlit_option_menu import option_menu
|
10 |
-
|
11 |
-
|
12 |
-
def render_sidebar():
|
13 |
-
"""
|
14 |
-
عرض وإدارة الشريط الجانبي
|
15 |
-
|
16 |
-
الإرجاع:
|
17 |
-
اسم الوحدة المحددة
|
18 |
-
"""
|
19 |
-
with st.sidebar:
|
20 |
-
# تطبيق الأنماط المحسنة على الشريط الجانبي
|
21 |
-
st.markdown("""
|
22 |
-
<style>
|
23 |
-
.sidebar .sidebar-content {
|
24 |
-
background: linear-gradient(135deg, #0B6E4F 0%, #08603a 100%);
|
25 |
-
}
|
26 |
-
/* المزيد من التنسيقات المخصصة للشريط الجانبي */
|
27 |
-
.sidebar .block-container {
|
28 |
-
padding-top: 0rem;
|
29 |
-
}
|
30 |
-
div[data-testid="stSidebarNav"] > div:first-child {
|
31 |
-
margin-top: -2rem;
|
32 |
-
}
|
33 |
-
</style>
|
34 |
-
""", unsafe_allow_html=True)
|
35 |
-
|
36 |
-
# عرض الشعار المحسن
|
37 |
-
logo_path = "utils/assets/logo.svg"
|
38 |
-
if os.path.exists(logo_path):
|
39 |
-
st.image(logo_path, width=180, use_column_width=False, output_format="SVG", clamp=False)
|
40 |
-
else:
|
41 |
-
# احتياطي إذا لم يتم العثور على الشعار
|
42 |
-
st.markdown("""
|
43 |
-
<div style="text-align: center; margin-bottom: 2rem;">
|
44 |
-
<div style="font-size: 2rem; font-weight: bold; color: white; margin-bottom: 0.5rem;">WAHBI</div>
|
45 |
-
<div style="font-size: 1rem; color: rgba(255,255,255,0.8);">نظام العقود الذكي</div>
|
46 |
-
</div>
|
47 |
-
""", unsafe_allow_html=True)
|
48 |
-
|
49 |
-
# إضافة العنوان قبل القائمة
|
50 |
-
st.markdown("""
|
51 |
-
<div class="app-title" style="text-align: center; margin-bottom: 1.5rem; color: white; font-size: 1.3rem; font-weight: bold;">
|
52 |
-
نظام تحليل العقود والمناقصات
|
53 |
-
</div>
|
54 |
-
""", unsafe_allow_html=True)
|
55 |
-
|
56 |
-
# إنشاء قائمة الخيارات باستخدام مكتبة streamlit_option_menu بتصميم محسن
|
57 |
-
selected_module = option_menu(
|
58 |
-
"", # إزالة العنوان لأننا أضفناه فوق القائمة
|
59 |
-
[
|
60 |
-
"الرئيسية",
|
61 |
-
"إدارة المشاريع",
|
62 |
-
"التسعير المتكاملة",
|
63 |
-
"الموارد والتكاليف",
|
64 |
-
"تحليل المستندات",
|
65 |
-
"مقارنة المستندات",
|
66 |
-
"تقييم مخاطر العقود",
|
67 |
-
"التقارير والتحليلات",
|
68 |
-
"متتبع حالة المشروع",
|
69 |
-
"خريطة المشاريع",
|
70 |
-
"نظام الإشعارات",
|
71 |
-
"الترجمة الصوتية",
|
72 |
-
"نظام الإنجازات",
|
73 |
-
"المساعد الذكي",
|
74 |
-
"ضبط نماذج الذكاء الاصطناعي"
|
75 |
-
],
|
76 |
-
icons=[
|
77 |
-
'house-fill',
|
78 |
-
'folder-fill',
|
79 |
-
'calculator-fill',
|
80 |
-
'tools',
|
81 |
-
'file-earmark-text-fill',
|
82 |
-
'files',
|
83 |
-
'exclamation-triangle-fill',
|
84 |
-
'bar-chart-fill',
|
85 |
-
'kanban-fill',
|
86 |
-
'geo-alt-fill',
|
87 |
-
'bell-fill',
|
88 |
-
'mic-fill',
|
89 |
-
'trophy-fill',
|
90 |
-
'robot',
|
91 |
-
'gear-fill'
|
92 |
-
],
|
93 |
-
menu_icon="cast",
|
94 |
-
default_index=0,
|
95 |
-
styles={
|
96 |
-
"container": {
|
97 |
-
"padding": "0px",
|
98 |
-
"background-color": "transparent",
|
99 |
-
"direction": "rtl",
|
100 |
-
"border-radius": "10px",
|
101 |
-
"margin-bottom": "20px"
|
102 |
-
},
|
103 |
-
"icon": {
|
104 |
-
"color": "#FFB100",
|
105 |
-
"font-size": "16px",
|
106 |
-
"margin-left": "10px",
|
107 |
-
"border-radius": "5px"
|
108 |
-
},
|
109 |
-
"nav-link": {
|
110 |
-
"font-size": "15px",
|
111 |
-
"text-align": "right",
|
112 |
-
"margin": "0px 0px 5px 0px",
|
113 |
-
"padding": "10px 15px",
|
114 |
-
"direction": "rtl",
|
115 |
-
"justify-content": "flex-end",
|
116 |
-
"border-radius": "7px",
|
117 |
-
"color": "rgba(255, 255, 255, 0.8)",
|
118 |
-
"font-weight": "500",
|
119 |
-
"transition": "all 0.3s ease"
|
120 |
-
},
|
121 |
-
"nav-link-selected": {
|
122 |
-
"background-color": "rgba(255, 255, 255, 0.1)",
|
123 |
-
"color": "#ffffff",
|
124 |
-
"font-weight": "700",
|
125 |
-
"border-right": "3px solid #FFB100"
|
126 |
-
},
|
127 |
-
"separator": {
|
128 |
-
"background-color": "rgba(255, 255, 255, 0.1)"
|
129 |
-
}
|
130 |
-
}
|
131 |
-
)
|
132 |
-
|
133 |
-
# إضافة فاصل
|
134 |
-
st.markdown("---")
|
135 |
-
|
136 |
-
# إضافة معلومات المشروع الحالي
|
137 |
-
if 'current_project' in st.session_state and st.session_state.current_project:
|
138 |
-
project = st.session_state.current_project
|
139 |
-
|
140 |
-
st.markdown("### المشروع الحالي")
|
141 |
-
st.markdown(f"**{project['name']}**")
|
142 |
-
st.markdown(f"رقم المناقصة: {project['number']}")
|
143 |
-
st.markdown(f"الجهة المالكة: {project['client']}")
|
144 |
-
|
145 |
-
# إضافة زر للتبديل بين المشاريع
|
146 |
-
if st.button("تبديل المشروع"):
|
147 |
-
# لتنفيذ في مرحلة لاحقة
|
148 |
-
pass
|
149 |
-
|
150 |
-
# إضافة معلومات المستخدم
|
151 |
-
if 'user_info' in st.session_state and st.session_state.user_info:
|
152 |
-
user = st.session_state.user_info
|
153 |
-
|
154 |
-
st.markdown("---")
|
155 |
-
st.markdown("### معلومات المستخدم")
|
156 |
-
st.markdown(f"**{user['full_name']}**")
|
157 |
-
st.markdown(f"الدور: {user['role']}")
|
158 |
-
|
159 |
-
# إضافة زر لتسجيل الخروج
|
160 |
-
if st.button("تسجيل الخروج"):
|
161 |
-
st.session_state.is_authenticated = False
|
162 |
-
st.session_state.user_info = None
|
163 |
-
st.rerun()
|
164 |
-
|
165 |
-
# إضافة معلومات النسخة
|
166 |
-
st.markdown("---")
|
167 |
-
st.markdown(f"الإصدار: 1.0.0")
|
168 |
-
st.markdown(f"تاريخ الإصدار: 2025-03-15")
|
169 |
-
st.markdown(f"© 2025 شركة شبه الجزيرة للمقاولات")
|
170 |
-
|
171 |
-
return selected_module
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/components/system_innovation.py
DELETED
@@ -1,81 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
مكون عرض ابتكارات النظام
|
3 |
-
"""
|
4 |
-
|
5 |
-
import streamlit as st
|
6 |
-
|
7 |
-
|
8 |
-
def display_innovations():
|
9 |
-
"""
|
10 |
-
عرض ابتكارات النظام
|
11 |
-
"""
|
12 |
-
# تعريف الابتكارات الرئيسية
|
13 |
-
innovations = [
|
14 |
-
{
|
15 |
-
"title": "تحليل المستندات بالذكاء الاصطناعي",
|
16 |
-
"description": "استخراج البنود والكميات والمخاطر من المستندات تلقائيًا باستخدام تقنيات الذكاء الاصطناعي ومعالجة اللغة الطبيعية، مما يوفر الوقت والجهد ويقلل من الأخطاء البشرية.",
|
17 |
-
"icon": "📄"
|
18 |
-
},
|
19 |
-
{
|
20 |
-
"title": "تقنية التسعير غير المتزن",
|
21 |
-
"description": "آلية متطورة لتحليل وتطبيق استراتيجيات التسعير غير المتزن، مع ضمان الحفاظ على القيمة الإجمالية للعرض، وزيادة فرص الربحية وتحسين التدفق النقدي.",
|
22 |
-
"icon": "💰"
|
23 |
-
},
|
24 |
-
{
|
25 |
-
"title": "حاسبة المحتوى المحلي الذكية",
|
26 |
-
"description": "حساب وتحسين نسبة المحتوى المحلي في المشاريع بطريقة آلية، مع اقتراح بدائل محلية للمنتجات والخدمات المستوردة لتحقيق متطلبات المحتوى المحلي.",
|
27 |
-
"icon": "🏭"
|
28 |
-
},
|
29 |
-
{
|
30 |
-
"title": "نظام التنبؤ بالأسعار",
|
31 |
-
"description": "التنبؤ بأسعار المواد والخدمات باستخدام خوارزميات التعلم الآلي والبيانات التاريخية، مما يساعد في اتخاذ قرارات التسعير بدقة أكبر.",
|
32 |
-
"icon": "📊"
|
33 |
-
},
|
34 |
-
{
|
35 |
-
"title": "تحليل المخاطر الاستباقي",
|
36 |
-
"description": "تحديد وتحليل المخاطر المحتملة في المشاريع بشكل استباقي، مع توفير استراتيجيات المعالجة المناسبة لكل مخاطرة وتقدير تأثيرها على التكلفة.",
|
37 |
-
"icon": "⚠️"
|
38 |
-
}
|
39 |
-
]
|
40 |
-
|
41 |
-
# عرض الابتكارات في صفوف
|
42 |
-
col1, col2 = st.columns(2)
|
43 |
-
|
44 |
-
for i, innovation in enumerate(innovations):
|
45 |
-
# توزيع الابتكارات على عمودين
|
46 |
-
current_col = col1 if i % 2 == 0 else col2
|
47 |
-
|
48 |
-
with current_col:
|
49 |
-
st.markdown(f"""
|
50 |
-
<div class="innovation-card">
|
51 |
-
<div class="innovation-icon">{innovation["icon"]}</div>
|
52 |
-
<h3>{innovation["title"]}</h3>
|
53 |
-
<p>{innovation["description"]}</p>
|
54 |
-
</div>
|
55 |
-
""", unsafe_allow_html=True)
|
56 |
-
|
57 |
-
# إضافة أسلوب CSS للبطاقات
|
58 |
-
st.markdown("""
|
59 |
-
<style>
|
60 |
-
.innovation-card {
|
61 |
-
background-color: #f8f9fa;
|
62 |
-
border-radius: 10px;
|
63 |
-
padding: 15px;
|
64 |
-
margin-bottom: 20px;
|
65 |
-
border-right: 5px solid #ff9a3c;
|
66 |
-
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
67 |
-
}
|
68 |
-
.innovation-icon {
|
69 |
-
font-size: 24px;
|
70 |
-
margin-bottom: 10px;
|
71 |
-
}
|
72 |
-
.innovation-card h3 {
|
73 |
-
color: #333;
|
74 |
-
margin-bottom: 10px;
|
75 |
-
}
|
76 |
-
.innovation-card p {
|
77 |
-
color: #666;
|
78 |
-
font-size: 14px;
|
79 |
-
}
|
80 |
-
</style>
|
81 |
-
""", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/css/enhanced.css
DELETED
@@ -1,400 +0,0 @@
|
|
1 |
-
/*
|
2 |
-
enhanced.css - نظام تحليل المناقصات والعقود
|
3 |
-
تحسينات متقدمة وتأثيرات خاصة للواجهة
|
4 |
-
*/
|
5 |
-
|
6 |
-
/* ===== تأثيرات حركية ===== */
|
7 |
-
|
8 |
-
/* تأثير التلاشي عند إضافة أي عنصر */
|
9 |
-
@keyframes fadeIn {
|
10 |
-
from {
|
11 |
-
opacity: 0;
|
12 |
-
transform: translateY(10px);
|
13 |
-
}
|
14 |
-
to {
|
15 |
-
opacity: 1;
|
16 |
-
transform: translateY(0);
|
17 |
-
}
|
18 |
-
}
|
19 |
-
|
20 |
-
/* تأثير النبض للتنبيهات */
|
21 |
-
@keyframes pulse {
|
22 |
-
0% {
|
23 |
-
box-shadow: 0 0 0 0 rgba(255, 82, 82, 0.4);
|
24 |
-
}
|
25 |
-
70% {
|
26 |
-
box-shadow: 0 0 0 10px rgba(255, 82, 82, 0);
|
27 |
-
}
|
28 |
-
100% {
|
29 |
-
box-shadow: 0 0 0 0 rgba(255, 82, 82, 0);
|
30 |
-
}
|
31 |
-
}
|
32 |
-
|
33 |
-
/* تأثير الظهور التدريجي */
|
34 |
-
@keyframes slideIn {
|
35 |
-
from {
|
36 |
-
transform: translateX(30px);
|
37 |
-
opacity: 0;
|
38 |
-
}
|
39 |
-
to {
|
40 |
-
transform: translateX(0);
|
41 |
-
opacity: 1;
|
42 |
-
}
|
43 |
-
}
|
44 |
-
|
45 |
-
/* ===== تطبيق التأثيرات ===== */
|
46 |
-
|
47 |
-
/* تطبيق تأثير التلاشي على البطاقات الرئيسية */
|
48 |
-
.card,
|
49 |
-
.section-card,
|
50 |
-
.dashboard-card {
|
51 |
-
animation: fadeIn 0.5s ease-out;
|
52 |
-
}
|
53 |
-
|
54 |
-
/* تطبيق تأثير النبض على التنبيهات المهمة */
|
55 |
-
.alert-critical {
|
56 |
-
animation: pulse 2s infinite;
|
57 |
-
}
|
58 |
-
|
59 |
-
/* تطبيق تأثير الظهور التدريجي على القوائم */
|
60 |
-
.menu-item {
|
61 |
-
animation: slideIn 0.3s ease-out;
|
62 |
-
}
|
63 |
-
|
64 |
-
/* ===== تحسينات متقدمة للمكونات ===== */
|
65 |
-
|
66 |
-
/* تحسين تفاعلية البطاقات */
|
67 |
-
.card {
|
68 |
-
transition: all 0.3s ease;
|
69 |
-
border-right: 4px solid transparent;
|
70 |
-
}
|
71 |
-
|
72 |
-
.card:hover {
|
73 |
-
transform: translateY(-5px);
|
74 |
-
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.08);
|
75 |
-
border-right: 4px solid var(--primary-color);
|
76 |
-
}
|
77 |
-
|
78 |
-
/* تحسينات العناوين مع تأثيرات التدرجات */
|
79 |
-
.special-heading {
|
80 |
-
font-size: 2rem;
|
81 |
-
font-weight: 700;
|
82 |
-
background: linear-gradient(120deg, var(--primary-color), var(--accent-color));
|
83 |
-
-webkit-background-clip: text;
|
84 |
-
-webkit-text-fill-color: transparent;
|
85 |
-
position: relative;
|
86 |
-
display: inline-block;
|
87 |
-
margin-bottom: 1.5rem;
|
88 |
-
}
|
89 |
-
|
90 |
-
.special-heading::after {
|
91 |
-
content: '';
|
92 |
-
position: absolute;
|
93 |
-
bottom: -10px;
|
94 |
-
left: 0;
|
95 |
-
width: 60px;
|
96 |
-
height: 4px;
|
97 |
-
background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
|
98 |
-
border-radius: 2px;
|
99 |
-
}
|
100 |
-
|
101 |
-
/* قسم مع خلفية تدرج */
|
102 |
-
.gradient-section {
|
103 |
-
background: linear-gradient(120deg, rgba(11, 110, 79, 0.05), rgba(87, 84, 255, 0.05));
|
104 |
-
border-radius: var(--border-radius-lg);
|
105 |
-
padding: var(--spacing-lg);
|
106 |
-
margin-bottom: var(--spacing-lg);
|
107 |
-
}
|
108 |
-
|
109 |
-
/* أزرار بتأثيرات متقدمة */
|
110 |
-
.enhanced-button {
|
111 |
-
border: none;
|
112 |
-
padding: 10px 20px;
|
113 |
-
border-radius: var(--border-radius-md);
|
114 |
-
font-weight: bold;
|
115 |
-
transition: all 0.3s ease;
|
116 |
-
position: relative;
|
117 |
-
overflow: hidden;
|
118 |
-
z-index: 1;
|
119 |
-
color: var(--text-light);
|
120 |
-
background-color: var(--primary-color);
|
121 |
-
}
|
122 |
-
|
123 |
-
.enhanced-button::before {
|
124 |
-
content: '';
|
125 |
-
position: absolute;
|
126 |
-
top: 0;
|
127 |
-
left: 0;
|
128 |
-
width: 0;
|
129 |
-
height: 100%;
|
130 |
-
background-color: rgba(255, 255, 255, 0.1);
|
131 |
-
transition: width 0.3s ease;
|
132 |
-
z-index: -1;
|
133 |
-
}
|
134 |
-
|
135 |
-
.enhanced-button:hover::before {
|
136 |
-
width: 100%;
|
137 |
-
}
|
138 |
-
|
139 |
-
.enhanced-button:hover {
|
140 |
-
transform: translateY(-2px);
|
141 |
-
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
142 |
-
}
|
143 |
-
|
144 |
-
/* ===== مؤشرات وأشرطة التقدم المتقدمة ===== */
|
145 |
-
|
146 |
-
/* مؤشر تقدم دائري */
|
147 |
-
.circle-progress {
|
148 |
-
position: relative;
|
149 |
-
width: 100px;
|
150 |
-
height: 100px;
|
151 |
-
border-radius: 50%;
|
152 |
-
background: conic-gradient(var(--primary-color) 0%, var(--primary-light) var(--progress, 75%), #f0f0f0 var(--progress, 75%), #f0f0f0 100%);
|
153 |
-
display: flex;
|
154 |
-
align-items: center;
|
155 |
-
justify-content: center;
|
156 |
-
}
|
157 |
-
|
158 |
-
.circle-progress::before {
|
159 |
-
content: '';
|
160 |
-
position: absolute;
|
161 |
-
width: 70px;
|
162 |
-
height: 70px;
|
163 |
-
border-radius: 50%;
|
164 |
-
background-color: white;
|
165 |
-
}
|
166 |
-
|
167 |
-
.circle-progress-value {
|
168 |
-
position: relative;
|
169 |
-
z-index: 10;
|
170 |
-
font-weight: bold;
|
171 |
-
font-size: 1.2rem;
|
172 |
-
color: var(--primary-color);
|
173 |
-
}
|
174 |
-
|
175 |
-
/* مؤشر شريطي بتأثيرات */
|
176 |
-
.enhanced-progress {
|
177 |
-
height: 10px;
|
178 |
-
background-color: #f0f0f0;
|
179 |
-
border-radius: 5px;
|
180 |
-
overflow: hidden;
|
181 |
-
margin: 10px 0;
|
182 |
-
}
|
183 |
-
|
184 |
-
.enhanced-progress-bar {
|
185 |
-
height: 100%;
|
186 |
-
background: linear-gradient(90deg, var(--primary-color), var(--primary-light));
|
187 |
-
border-radius: 5px;
|
188 |
-
transition: width 0.5s ease;
|
189 |
-
position: relative;
|
190 |
-
overflow: hidden;
|
191 |
-
}
|
192 |
-
|
193 |
-
.enhanced-progress-bar::after {
|
194 |
-
content: '';
|
195 |
-
position: absolute;
|
196 |
-
top: 0;
|
197 |
-
left: 0;
|
198 |
-
right: 0;
|
199 |
-
bottom: 0;
|
200 |
-
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
201 |
-
animation: shimmer 1.5s infinite;
|
202 |
-
}
|
203 |
-
|
204 |
-
@keyframes shimmer {
|
205 |
-
0% {
|
206 |
-
transform: translateX(-100%);
|
207 |
-
}
|
208 |
-
100% {
|
209 |
-
transform: translateX(100%);
|
210 |
-
}
|
211 |
-
}
|
212 |
-
|
213 |
-
/* ===== بطاقات المؤشرات الإحصائية المطورة ===== */
|
214 |
-
|
215 |
-
.stat-card {
|
216 |
-
border-radius: var(--border-radius-md);
|
217 |
-
background-color: white;
|
218 |
-
padding: 1.5rem;
|
219 |
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
220 |
-
display: flex;
|
221 |
-
align-items: center;
|
222 |
-
transition: all 0.3s ease;
|
223 |
-
position: relative;
|
224 |
-
overflow: hidden;
|
225 |
-
}
|
226 |
-
|
227 |
-
.stat-card:hover {
|
228 |
-
transform: translateY(-5px);
|
229 |
-
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
230 |
-
}
|
231 |
-
|
232 |
-
.stat-card::before {
|
233 |
-
content: '';
|
234 |
-
position: absolute;
|
235 |
-
top: 0;
|
236 |
-
left: 0;
|
237 |
-
width: 6px;
|
238 |
-
height: 100%;
|
239 |
-
background-color: var(--primary-color);
|
240 |
-
}
|
241 |
-
|
242 |
-
.stat-card-icon {
|
243 |
-
width: 50px;
|
244 |
-
height: 50px;
|
245 |
-
display: flex;
|
246 |
-
align-items: center;
|
247 |
-
justify-content: center;
|
248 |
-
border-radius: 12px;
|
249 |
-
background-color: rgba(11, 110, 79, 0.1);
|
250 |
-
color: var(--primary-color);
|
251 |
-
margin-right: 1rem;
|
252 |
-
}
|
253 |
-
|
254 |
-
.stat-card-content {
|
255 |
-
flex-grow: 1;
|
256 |
-
}
|
257 |
-
|
258 |
-
.stat-card-value {
|
259 |
-
font-size: 2rem;
|
260 |
-
font-weight: bold;
|
261 |
-
color: var(--text-primary);
|
262 |
-
line-height: 1;
|
263 |
-
margin-bottom: 0.3rem;
|
264 |
-
}
|
265 |
-
|
266 |
-
.stat-card-label {
|
267 |
-
color: var(--text-muted);
|
268 |
-
font-size: 0.9rem;
|
269 |
-
}
|
270 |
-
|
271 |
-
.stat-card-trend {
|
272 |
-
display: flex;
|
273 |
-
align-items: center;
|
274 |
-
font-size: 0.8rem;
|
275 |
-
margin-top: 0.5rem;
|
276 |
-
}
|
277 |
-
|
278 |
-
.stat-card-trend.up {
|
279 |
-
color: var(--success-color);
|
280 |
-
}
|
281 |
-
|
282 |
-
.stat-card-trend.down {
|
283 |
-
color: var(--error-color);
|
284 |
-
}
|
285 |
-
|
286 |
-
/* ===== أشكال الخلفيات المحسنة ===== */
|
287 |
-
|
288 |
-
.dots-background {
|
289 |
-
background-color: white;
|
290 |
-
background-image: radial-gradient(rgba(11, 110, 79, 0.1) 1px, transparent 1px);
|
291 |
-
background-size: 20px 20px;
|
292 |
-
background-position: 0 0;
|
293 |
-
}
|
294 |
-
|
295 |
-
.geometric-pattern {
|
296 |
-
position: relative;
|
297 |
-
overflow: hidden;
|
298 |
-
z-index: 1;
|
299 |
-
}
|
300 |
-
|
301 |
-
.geometric-pattern::before {
|
302 |
-
content: "";
|
303 |
-
position: absolute;
|
304 |
-
top: -50%;
|
305 |
-
left: -50%;
|
306 |
-
width: 200%;
|
307 |
-
height: 200%;
|
308 |
-
background: repeating-linear-gradient(
|
309 |
-
45deg,
|
310 |
-
rgba(11, 110, 79, 0.03),
|
311 |
-
rgba(11, 110, 79, 0.03) 10px,
|
312 |
-
rgba(11, 110, 79, 0.05) 10px,
|
313 |
-
rgba(11, 110, 79, 0.05) 20px
|
314 |
-
);
|
315 |
-
transform: rotate(-45deg);
|
316 |
-
z-index: -1;
|
317 |
-
}
|
318 |
-
|
319 |
-
/* ===== تأثيرات تكميلية ===== */
|
320 |
-
|
321 |
-
/* تأثير الومضة على الأزرار عند النقر */
|
322 |
-
.button-flash {
|
323 |
-
position: relative;
|
324 |
-
overflow: hidden;
|
325 |
-
}
|
326 |
-
|
327 |
-
.button-flash::after {
|
328 |
-
content: '';
|
329 |
-
position: absolute;
|
330 |
-
top: 50%;
|
331 |
-
left: 50%;
|
332 |
-
width: 5px;
|
333 |
-
height: 5px;
|
334 |
-
background: rgba(255, 255, 255, 0.5);
|
335 |
-
opacity: 0;
|
336 |
-
border-radius: 100%;
|
337 |
-
transform: scale(1, 1) translate(-50%, -50%);
|
338 |
-
transform-origin: 50% 50%;
|
339 |
-
}
|
340 |
-
|
341 |
-
.button-flash:active::after {
|
342 |
-
animation: flash 0.6s ease-out;
|
343 |
-
}
|
344 |
-
|
345 |
-
@keyframes flash {
|
346 |
-
0% {
|
347 |
-
opacity: 1;
|
348 |
-
transform: scale(0, 0) translate(-50%, -50%);
|
349 |
-
}
|
350 |
-
100% {
|
351 |
-
opacity: 0;
|
352 |
-
transform: scale(50, 50) translate(-50%, -50%);
|
353 |
-
}
|
354 |
-
}
|
355 |
-
|
356 |
-
/* ===== تحسينات المسافات والهوامش لسهولة القراءة ===== */
|
357 |
-
|
358 |
-
/* إضافة مسافات أكبر بين الفقرات */
|
359 |
-
p {
|
360 |
-
margin-bottom: 1.2rem;
|
361 |
-
line-height: 1.6;
|
362 |
-
}
|
363 |
-
|
364 |
-
/* تحسين النقاط والقوائم */
|
365 |
-
li {
|
366 |
-
margin-bottom: 0.5rem;
|
367 |
-
}
|
368 |
-
|
369 |
-
/* ===== تعديلات خاصة للهواتف المحمولة ===== */
|
370 |
-
|
371 |
-
@media (max-width: 768px) {
|
372 |
-
.dashboard-grid {
|
373 |
-
grid-template-columns: 1fr;
|
374 |
-
}
|
375 |
-
|
376 |
-
.stat-card {
|
377 |
-
padding: 1rem;
|
378 |
-
}
|
379 |
-
|
380 |
-
.stat-card-icon {
|
381 |
-
width: 40px;
|
382 |
-
height: 40px;
|
383 |
-
}
|
384 |
-
|
385 |
-
.stat-card-value {
|
386 |
-
font-size: 1.5rem;
|
387 |
-
}
|
388 |
-
|
389 |
-
h1 {
|
390 |
-
font-size: 1.8rem;
|
391 |
-
}
|
392 |
-
|
393 |
-
h2 {
|
394 |
-
font-size: 1.5rem;
|
395 |
-
}
|
396 |
-
|
397 |
-
.indicator-value {
|
398 |
-
font-size: 1.5rem;
|
399 |
-
}
|
400 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/css/main.css
DELETED
@@ -1,405 +0,0 @@
|
|
1 |
-
/*
|
2 |
-
main.css - نظام تحليل المناقصات والعقود
|
3 |
-
الأنماط الرئيسية للتطبيق
|
4 |
-
*/
|
5 |
-
|
6 |
-
/* ===== المتغيرات العامة ===== */
|
7 |
-
:root {
|
8 |
-
/* الألوان الأساسية */
|
9 |
-
--primary-color: #0B6E4F;
|
10 |
-
--primary-light: #08A45C;
|
11 |
-
--primary-dark: #074D37;
|
12 |
-
--secondary-color: #FFB100;
|
13 |
-
--secondary-light: #FFCA4D;
|
14 |
-
--secondary-dark: #D99000;
|
15 |
-
--accent-color: #5754FF;
|
16 |
-
--accent-light: #6F6CFF;
|
17 |
-
--accent-dark: #4240DD;
|
18 |
-
|
19 |
-
/* ألوان محايدة */
|
20 |
-
--background-color: #F8F9FA;
|
21 |
-
--surface-color: #FFFFFF;
|
22 |
-
--card-color: #FAFAFA;
|
23 |
-
--border-color: #E0E0E0;
|
24 |
-
|
25 |
-
/* ألوان النص */
|
26 |
-
--text-primary: #222222;
|
27 |
-
--text-secondary: #555555;
|
28 |
-
--text-muted: #888888;
|
29 |
-
--text-light: #FFFFFF;
|
30 |
-
|
31 |
-
/* ألوان مؤشرات الحالة */
|
32 |
-
--success-color: #43A047;
|
33 |
-
--warning-color: #FB8C00;
|
34 |
-
--error-color: #E53935;
|
35 |
-
--info-color: #1E88E5;
|
36 |
-
|
37 |
-
/* القياسات */
|
38 |
-
--border-radius-sm: 4px;
|
39 |
-
--border-radius-md: 8px;
|
40 |
-
--border-radius-lg: 16px;
|
41 |
-
--spacing-xs: 4px;
|
42 |
-
--spacing-sm: 8px;
|
43 |
-
--spacing-md: 16px;
|
44 |
-
--spacing-lg: 24px;
|
45 |
-
--spacing-xl: 32px;
|
46 |
-
|
47 |
-
/* الظلال */
|
48 |
-
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05);
|
49 |
-
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1);
|
50 |
-
--shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.1);
|
51 |
-
|
52 |
-
/* الخطوط */
|
53 |
-
--font-family: 'Cairo', 'Tajawal', 'Readex Pro', sans-serif;
|
54 |
-
}
|
55 |
-
|
56 |
-
/* ===== إعدادات عامة ===== */
|
57 |
-
html, body {
|
58 |
-
font-family: var(--font-family);
|
59 |
-
color: var(--text-primary);
|
60 |
-
background-color: var(--background-color);
|
61 |
-
}
|
62 |
-
|
63 |
-
/* ===== العناوين ===== */
|
64 |
-
h1, h2, h3, h4, h5, h6 {
|
65 |
-
color: var(--primary-color);
|
66 |
-
font-weight: 700;
|
67 |
-
margin-bottom: var(--spacing-md);
|
68 |
-
}
|
69 |
-
|
70 |
-
h1 {
|
71 |
-
font-size: 2.2rem;
|
72 |
-
background: linear-gradient(90deg, var(--primary-color), var(--primary-light));
|
73 |
-
-webkit-background-clip: text;
|
74 |
-
-webkit-text-fill-color: transparent;
|
75 |
-
display: inline-block;
|
76 |
-
margin-bottom: var(--spacing-lg);
|
77 |
-
}
|
78 |
-
|
79 |
-
h2 {
|
80 |
-
font-size: 1.8rem;
|
81 |
-
position: relative;
|
82 |
-
padding-bottom: var(--spacing-sm);
|
83 |
-
}
|
84 |
-
|
85 |
-
h2::after {
|
86 |
-
content: '';
|
87 |
-
position: absolute;
|
88 |
-
bottom: 0;
|
89 |
-
right: 0;
|
90 |
-
width: 60px;
|
91 |
-
height: 3px;
|
92 |
-
background: var(--secondary-color);
|
93 |
-
border-radius: var(--border-radius-sm);
|
94 |
-
}
|
95 |
-
|
96 |
-
h3 {
|
97 |
-
font-size: 1.5rem;
|
98 |
-
color: var(--primary-dark);
|
99 |
-
}
|
100 |
-
|
101 |
-
h4 {
|
102 |
-
font-size: 1.2rem;
|
103 |
-
color: var(--text-primary);
|
104 |
-
}
|
105 |
-
|
106 |
-
/* ===== عنوان التطبيق ===== */
|
107 |
-
.app-title {
|
108 |
-
font-size: 2.2rem;
|
109 |
-
font-weight: bold;
|
110 |
-
text-align: center;
|
111 |
-
color: var(--primary-color);
|
112 |
-
margin-bottom: var(--spacing-lg);
|
113 |
-
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
|
114 |
-
background: linear-gradient(90deg, var(--primary-color), var(--primary-light));
|
115 |
-
-webkit-background-clip: text;
|
116 |
-
-webkit-text-fill-color: transparent;
|
117 |
-
}
|
118 |
-
|
119 |
-
/* ===== الأقسام والبطاقات ===== */
|
120 |
-
.section-card {
|
121 |
-
background-color: var(--surface-color);
|
122 |
-
border-radius: var(--border-radius-lg);
|
123 |
-
padding: var(--spacing-lg);
|
124 |
-
box-shadow: var(--shadow-md);
|
125 |
-
margin-bottom: var(--spacing-lg);
|
126 |
-
border-right: 4px solid var(--primary-color);
|
127 |
-
}
|
128 |
-
|
129 |
-
.card {
|
130 |
-
background-color: var(--surface-color);
|
131 |
-
border-radius: var(--border-radius-md);
|
132 |
-
padding: var(--spacing-md);
|
133 |
-
box-shadow: var(--shadow-sm);
|
134 |
-
margin-bottom: var(--spacing-md);
|
135 |
-
border-right: 3px solid var(--primary-color);
|
136 |
-
transition: all 0.3s ease;
|
137 |
-
}
|
138 |
-
|
139 |
-
.card:hover {
|
140 |
-
box-shadow: var(--shadow-md);
|
141 |
-
transform: translateY(-2px);
|
142 |
-
}
|
143 |
-
|
144 |
-
.card-title {
|
145 |
-
font-weight: bold;
|
146 |
-
color: var(--primary-color);
|
147 |
-
margin-bottom: var(--spacing-sm);
|
148 |
-
font-size: 1.2rem;
|
149 |
-
}
|
150 |
-
|
151 |
-
.card-metrics {
|
152 |
-
display: flex;
|
153 |
-
justify-content: space-between;
|
154 |
-
margin-top: var(--spacing-sm);
|
155 |
-
}
|
156 |
-
|
157 |
-
.card-metric {
|
158 |
-
text-align: center;
|
159 |
-
}
|
160 |
-
|
161 |
-
.card-metric-value {
|
162 |
-
font-weight: bold;
|
163 |
-
font-size: 1.5rem;
|
164 |
-
}
|
165 |
-
|
166 |
-
.card-metric-label {
|
167 |
-
font-size: 0.8rem;
|
168 |
-
color: var(--text-muted);
|
169 |
-
}
|
170 |
-
|
171 |
-
/* ===== المؤشرات ===== */
|
172 |
-
.indicator {
|
173 |
-
padding: var(--spacing-md);
|
174 |
-
border-radius: var(--border-radius-md);
|
175 |
-
background-color: var(--surface-color);
|
176 |
-
text-align: center;
|
177 |
-
box-shadow: var(--shadow-sm);
|
178 |
-
border-top: 3px solid var(--primary-color);
|
179 |
-
height: 100%;
|
180 |
-
}
|
181 |
-
|
182 |
-
.indicator-value {
|
183 |
-
font-size: 2rem;
|
184 |
-
font-weight: bold;
|
185 |
-
margin-bottom: var(--spacing-xs);
|
186 |
-
}
|
187 |
-
|
188 |
-
.indicator-label {
|
189 |
-
font-size: 1rem;
|
190 |
-
color: var(--text-muted);
|
191 |
-
}
|
192 |
-
|
193 |
-
/* ===== الأزرار ===== */
|
194 |
-
.stButton > button {
|
195 |
-
border-radius: var(--border-radius-md);
|
196 |
-
background-color: var(--primary-color);
|
197 |
-
color: var(--text-light);
|
198 |
-
font-weight: bold;
|
199 |
-
border: none;
|
200 |
-
padding: 0.5rem 1rem;
|
201 |
-
transition: all 0.3s ease;
|
202 |
-
}
|
203 |
-
|
204 |
-
.stButton > button:hover {
|
205 |
-
background-color: var(--primary-dark);
|
206 |
-
box-shadow: var(--shadow-sm);
|
207 |
-
}
|
208 |
-
|
209 |
-
/* زر ثانوي */
|
210 |
-
.stButton.secondary > button {
|
211 |
-
background-color: var(--secondary-color);
|
212 |
-
color: var(--text-primary);
|
213 |
-
}
|
214 |
-
|
215 |
-
.stButton.secondary > button:hover {
|
216 |
-
background-color: var(--secondary-dark);
|
217 |
-
}
|
218 |
-
|
219 |
-
/* زر محايد */
|
220 |
-
.stButton.neutral > button {
|
221 |
-
background-color: var(--text-muted);
|
222 |
-
color: var(--text-light);
|
223 |
-
}
|
224 |
-
|
225 |
-
.stButton.neutral > button:hover {
|
226 |
-
background-color: var(--text-secondary);
|
227 |
-
}
|
228 |
-
|
229 |
-
/* زر خطر */
|
230 |
-
.stButton.danger > button {
|
231 |
-
background-color: var(--error-color);
|
232 |
-
}
|
233 |
-
|
234 |
-
.stButton.danger > button:hover {
|
235 |
-
background-color: #C62828;
|
236 |
-
}
|
237 |
-
|
238 |
-
/* ===== النماذج ===== */
|
239 |
-
.stTextInput > div > div > input,
|
240 |
-
.stTextArea > div > div > textarea {
|
241 |
-
border-radius: var(--border-radius-md);
|
242 |
-
border: 1px solid var(--border-color);
|
243 |
-
padding: var(--spacing-sm);
|
244 |
-
transition: all 0.3s ease;
|
245 |
-
}
|
246 |
-
|
247 |
-
.stTextInput > div > div > input:focus,
|
248 |
-
.stTextArea > div > div > textarea:focus {
|
249 |
-
border-color: var(--primary-color);
|
250 |
-
box-shadow: 0 0 0 2px rgba(11, 110, 79, 0.2);
|
251 |
-
}
|
252 |
-
|
253 |
-
.stSelectbox > div > div > div {
|
254 |
-
border-radius: var(--border-radius-md);
|
255 |
-
border: 1px solid var(--border-color);
|
256 |
-
}
|
257 |
-
|
258 |
-
/* ===== الفواصل ===== */
|
259 |
-
hr {
|
260 |
-
border: none;
|
261 |
-
height: 1px;
|
262 |
-
background-color: var(--border-color);
|
263 |
-
margin: var(--spacing-lg) 0;
|
264 |
-
}
|
265 |
-
|
266 |
-
/* ===== الرسوم البيانية ===== */
|
267 |
-
.stPlot {
|
268 |
-
background-color: var(--surface-color);
|
269 |
-
border-radius: var(--border-radius-md);
|
270 |
-
padding: var(--spacing-md);
|
271 |
-
box-shadow: var(--shadow-sm);
|
272 |
-
}
|
273 |
-
|
274 |
-
/* ===== التنبيهات ===== */
|
275 |
-
.stAlert {
|
276 |
-
border-radius: var(--border-radius-md);
|
277 |
-
box-shadow: var(--shadow-sm);
|
278 |
-
}
|
279 |
-
|
280 |
-
/* ===== الصور ===== */
|
281 |
-
.stImage > img {
|
282 |
-
border-radius: var(--border-radius-md);
|
283 |
-
box-shadow: var(--shadow-sm);
|
284 |
-
}
|
285 |
-
|
286 |
-
/* ===== تعديلات خاصة بالواجهة ===== */
|
287 |
-
.main-content {
|
288 |
-
padding: var(--spacing-md);
|
289 |
-
}
|
290 |
-
|
291 |
-
/* ===== أنماط القائمة ===== */
|
292 |
-
.menu-container {
|
293 |
-
background-color: var(--surface-color);
|
294 |
-
border-radius: var(--border-radius-md);
|
295 |
-
box-shadow: var(--shadow-sm);
|
296 |
-
padding: var(--spacing-sm);
|
297 |
-
margin-bottom: var(--spacing-md);
|
298 |
-
}
|
299 |
-
|
300 |
-
.menu-item {
|
301 |
-
padding: var(--spacing-sm);
|
302 |
-
border-radius: var(--border-radius-sm);
|
303 |
-
cursor: pointer;
|
304 |
-
transition: all 0.3s ease;
|
305 |
-
}
|
306 |
-
|
307 |
-
.menu-item:hover {
|
308 |
-
background-color: rgba(11, 110, 79, 0.1);
|
309 |
-
}
|
310 |
-
|
311 |
-
.menu-item.active {
|
312 |
-
background-color: var(--primary-color);
|
313 |
-
color: var(--text-light);
|
314 |
-
}
|
315 |
-
|
316 |
-
/* ===== لوحات المعلومات ===== */
|
317 |
-
.dashboard-grid {
|
318 |
-
display: grid;
|
319 |
-
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
320 |
-
gap: var(--spacing-md);
|
321 |
-
margin-bottom: var(--spacing-lg);
|
322 |
-
}
|
323 |
-
|
324 |
-
.dashboard-card {
|
325 |
-
background-color: var(--surface-color);
|
326 |
-
border-radius: var(--border-radius-md);
|
327 |
-
padding: var(--spacing-md);
|
328 |
-
box-shadow: var(--shadow-sm);
|
329 |
-
display: flex;
|
330 |
-
flex-direction: column;
|
331 |
-
height: 100%;
|
332 |
-
}
|
333 |
-
|
334 |
-
.dashboard-card-header {
|
335 |
-
display: flex;
|
336 |
-
justify-content: space-between;
|
337 |
-
align-items: center;
|
338 |
-
margin-bottom: var(--spacing-sm);
|
339 |
-
padding-bottom: var(--spacing-sm);
|
340 |
-
border-bottom: 1px solid var(--border-color);
|
341 |
-
}
|
342 |
-
|
343 |
-
.dashboard-card-title {
|
344 |
-
font-weight: bold;
|
345 |
-
color: var(--primary-color);
|
346 |
-
}
|
347 |
-
|
348 |
-
.dashboard-card-icon {
|
349 |
-
color: var(--accent-color);
|
350 |
-
background-color: rgba(87, 84, 255, 0.1);
|
351 |
-
width: 40px;
|
352 |
-
height: 40px;
|
353 |
-
display: flex;
|
354 |
-
align-items: center;
|
355 |
-
justify-content: center;
|
356 |
-
border-radius: 50%;
|
357 |
-
}
|
358 |
-
|
359 |
-
.dashboard-card-content {
|
360 |
-
flex-grow: 1;
|
361 |
-
}
|
362 |
-
|
363 |
-
.dashboard-card-footer {
|
364 |
-
margin-top: var(--spacing-sm);
|
365 |
-
padding-top: var(--spacing-sm);
|
366 |
-
border-top: 1px solid var(--border-color);
|
367 |
-
display: flex;
|
368 |
-
justify-content: space-between;
|
369 |
-
align-items: center;
|
370 |
-
}
|
371 |
-
|
372 |
-
/* ===== المحتوى المشروط ===== */
|
373 |
-
.conditional-section {
|
374 |
-
transition: all 0.3s ease;
|
375 |
-
animation: fadeIn 0.3s ease-in-out;
|
376 |
-
}
|
377 |
-
|
378 |
-
@keyframes fadeIn {
|
379 |
-
from {
|
380 |
-
opacity: 0;
|
381 |
-
transform: translateY(10px);
|
382 |
-
}
|
383 |
-
to {
|
384 |
-
opacity: 1;
|
385 |
-
transform: translateY(0);
|
386 |
-
}
|
387 |
-
}
|
388 |
-
|
389 |
-
/* ===== نمط تذييل الصفحة ===== */
|
390 |
-
.footer {
|
391 |
-
text-align: center;
|
392 |
-
padding: var(--spacing-lg) 0;
|
393 |
-
color: var(--text-muted);
|
394 |
-
font-size: 0.9rem;
|
395 |
-
margin-top: var(--spacing-xl);
|
396 |
-
}
|
397 |
-
|
398 |
-
.footer a {
|
399 |
-
color: var(--primary-color);
|
400 |
-
text-decoration: none;
|
401 |
-
}
|
402 |
-
|
403 |
-
.footer a:hover {
|
404 |
-
text-decoration: underline;
|
405 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/css/rtl.css
DELETED
@@ -1,223 +0,0 @@
|
|
1 |
-
/*
|
2 |
-
rtl.css - نظام تحليل المناقصات والعقود
|
3 |
-
تخصيصات الواجهة للغة العربية ودعم RTL
|
4 |
-
*/
|
5 |
-
|
6 |
-
/* ===== تعديلات عامة لتنسيق RTL ===== */
|
7 |
-
|
8 |
-
/* توجيه النص العام والمكونات */
|
9 |
-
.css-18e3th9,
|
10 |
-
.css-1d391kg,
|
11 |
-
.stMarkdown,
|
12 |
-
.stTextArea,
|
13 |
-
.stButton,
|
14 |
-
.stTextInput,
|
15 |
-
.stSelectbox,
|
16 |
-
.stRadio,
|
17 |
-
.stCheckbox,
|
18 |
-
.stTabs,
|
19 |
-
.stWidgetLabel,
|
20 |
-
.stPlotlyChart,
|
21 |
-
.stDataFrame,
|
22 |
-
.stTable {
|
23 |
-
direction: rtl;
|
24 |
-
text-align: right;
|
25 |
-
}
|
26 |
-
|
27 |
-
/* توجيه صحيح للرسوم البيانية */
|
28 |
-
.plotly-graph-div,
|
29 |
-
.stPlot {
|
30 |
-
direction: ltr;
|
31 |
-
}
|
32 |
-
|
33 |
-
/* تعديل على عناصر القوائم */
|
34 |
-
.stSelectbox > div > div > div {
|
35 |
-
text-align: right;
|
36 |
-
}
|
37 |
-
|
38 |
-
/* إصلاح اتجاه المدخلات الرقمية */
|
39 |
-
input[type="number"] {
|
40 |
-
direction: ltr;
|
41 |
-
text-align: right;
|
42 |
-
}
|
43 |
-
|
44 |
-
/* تعديل نص الأزرار للتوجيه الصحيح */
|
45 |
-
.stButton > button {
|
46 |
-
direction: rtl;
|
47 |
-
display: flex;
|
48 |
-
align-items: center;
|
49 |
-
justify-content: center;
|
50 |
-
}
|
51 |
-
|
52 |
-
/* تعديل الشاشة الرئيسية */
|
53 |
-
.main .block-container {
|
54 |
-
direction: rtl;
|
55 |
-
text-align: right;
|
56 |
-
}
|
57 |
-
|
58 |
-
/* ===== تعديلات أيقونات وعلامات خاصة ===== */
|
59 |
-
|
60 |
-
/* عكس اتجاه أيقونات السهم والأيقونات الموجهة الأخرى */
|
61 |
-
.fa-arrow-left,
|
62 |
-
.fa-chevron-left {
|
63 |
-
transform: rotate(180deg);
|
64 |
-
}
|
65 |
-
|
66 |
-
.fa-arrow-right,
|
67 |
-
.fa-chevron-right {
|
68 |
-
transform: rotate(180deg);
|
69 |
-
}
|
70 |
-
|
71 |
-
/* تعديل اتجاه علامات التبويب */
|
72 |
-
.stTabs [role="tablist"] {
|
73 |
-
flex-direction: row-reverse;
|
74 |
-
}
|
75 |
-
|
76 |
-
/* تعديل على مؤشرات التحميل */
|
77 |
-
.stProgress > div > div > div {
|
78 |
-
flex-direction: row-reverse;
|
79 |
-
}
|
80 |
-
|
81 |
-
/* ===== تعديلات خاصة للجداول والبيانات ===== */
|
82 |
-
|
83 |
-
/* إصلاح اتجاه الجداول */
|
84 |
-
.stTable {
|
85 |
-
direction: rtl;
|
86 |
-
}
|
87 |
-
|
88 |
-
.stTable th {
|
89 |
-
text-align: right;
|
90 |
-
}
|
91 |
-
|
92 |
-
/* رؤوس الجداول */
|
93 |
-
.stTable thead {
|
94 |
-
text-align: right;
|
95 |
-
}
|
96 |
-
|
97 |
-
/* صفوف البيانات */
|
98 |
-
.stTable tbody {
|
99 |
-
text-align: right;
|
100 |
-
}
|
101 |
-
|
102 |
-
/* تعديل على مكون التاريخ */
|
103 |
-
.stDateInput > div {
|
104 |
-
flex-direction: row-reverse;
|
105 |
-
}
|
106 |
-
|
107 |
-
/* ===== تحسينات إضافية ===== */
|
108 |
-
|
109 |
-
/* تعديل مكونات التفاعل */
|
110 |
-
.stWidgetLabel p {
|
111 |
-
text-align: right;
|
112 |
-
}
|
113 |
-
|
114 |
-
/* تعديل على الهوامش - تبديل الهوامش اليمنى واليسرى */
|
115 |
-
.stButton,
|
116 |
-
.stTextInput,
|
117 |
-
.stNumberInput,
|
118 |
-
.stSelectbox,
|
119 |
-
.stTextArea {
|
120 |
-
margin-right: 0;
|
121 |
-
margin-left: auto;
|
122 |
-
}
|
123 |
-
|
124 |
-
/* تعديل على الحدود - تبديل الحدود اليمنى واليسرى */
|
125 |
-
.stMarkdown blockquote {
|
126 |
-
border-right: 4px solid var(--primary-color);
|
127 |
-
border-left: none;
|
128 |
-
padding-right: 20px;
|
129 |
-
padding-left: 0;
|
130 |
-
}
|
131 |
-
|
132 |
-
/* إضافة دعم أفضل للخطوط العربية */
|
133 |
-
body {
|
134 |
-
font-family: 'Cairo', 'Tajawal', 'Readex Pro', -apple-system, BlinkMacSystemFont, sans-serif;
|
135 |
-
}
|
136 |
-
|
137 |
-
/* تعديل صناديق الإدخال */
|
138 |
-
textarea, input {
|
139 |
-
font-family: 'Cairo', 'Tajawal', 'Readex Pro', -apple-system, BlinkMacSystemFont, sans-serif;
|
140 |
-
}
|
141 |
-
|
142 |
-
/* تعديل القوائم ذات التعداد النقطي للاتجاه RTL */
|
143 |
-
ul, ol {
|
144 |
-
padding-right: 20px;
|
145 |
-
padding-left: 0;
|
146 |
-
}
|
147 |
-
|
148 |
-
/* ===== تصحيحات محددة للمكونات ===== */
|
149 |
-
|
150 |
-
/* تصحيح التنسيق للأعمدة */
|
151 |
-
div.row-widget.stRadio > div {
|
152 |
-
flex-direction: row-reverse;
|
153 |
-
}
|
154 |
-
|
155 |
-
/* تصحيح مربعات الاختيار */
|
156 |
-
.stCheckbox > div {
|
157 |
-
flex-direction: row-reverse;
|
158 |
-
}
|
159 |
-
|
160 |
-
/* تحسين مظهر وتجربة المستخدم العربية */
|
161 |
-
.stExpander > div > div > div {
|
162 |
-
text-align: right;
|
163 |
-
}
|
164 |
-
|
165 |
-
.stFileUploader > div {
|
166 |
-
direction: rtl;
|
167 |
-
text-align: right;
|
168 |
-
}
|
169 |
-
|
170 |
-
/* تصحيح ظهور القوائم المنسدلة لتلائم RTL */
|
171 |
-
.stSelectbox > div[data-baseweb="select"] > div {
|
172 |
-
direction: rtl;
|
173 |
-
}
|
174 |
-
|
175 |
-
/* تصحيح اتجاه النص في التنبيهات */
|
176 |
-
.stAlert > div {
|
177 |
-
flex-direction: row-reverse;
|
178 |
-
text-align: right;
|
179 |
-
}
|
180 |
-
|
181 |
-
/* تصحيح اتجاه صناديق النص القابلة للتحرير */
|
182 |
-
.css-1hynsf2 .stTextArea label,
|
183 |
-
.css-1hynsf2 .stTextInput label {
|
184 |
-
text-align: right;
|
185 |
-
}
|
186 |
-
|
187 |
-
/* تصحيح مربعات الاختيار المتعددة */
|
188 |
-
div.row-widget.stMultiselect > div {
|
189 |
-
direction: rtl;
|
190 |
-
}
|
191 |
-
|
192 |
-
/* تصحيح محاذاة محاذاة المحتوى وترتيب الأيقونات */
|
193 |
-
.stAlert > div > div:first-child {
|
194 |
-
margin-left: 10px;
|
195 |
-
margin-right: 0;
|
196 |
-
}
|
197 |
-
|
198 |
-
/* تصحيحات عامة للتأكد من عدم انعكاس أي شيء بشكل غير صحيح */
|
199 |
-
.stMarkdown img {
|
200 |
-
direction: ltr;
|
201 |
-
}
|
202 |
-
|
203 |
-
/* ===== تصحيحات لحالات خاصة ===== */
|
204 |
-
|
205 |
-
/* تصحيح اتجاه مكونات التاريخ والوقت */
|
206 |
-
.stDateInput, .stTimeInput {
|
207 |
-
direction: rtl;
|
208 |
-
}
|
209 |
-
|
210 |
-
/* تصحيح محدد اللون */
|
211 |
-
.stColorPicker > div {
|
212 |
-
direction: ltr;
|
213 |
-
}
|
214 |
-
|
215 |
-
/* تصحيح مكون التصفية بالنوع */
|
216 |
-
.stType > div {
|
217 |
-
direction: rtl;
|
218 |
-
}
|
219 |
-
|
220 |
-
/* تصحيح مكون الشريط الجان��ي */
|
221 |
-
.css-1v3fvcr {
|
222 |
-
direction: rtl;
|
223 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/dwg_handler.py
DELETED
@@ -1,223 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
معالج ملفات DWG
|
3 |
-
"""
|
4 |
-
|
5 |
-
import os
|
6 |
-
import re
|
7 |
-
import subprocess
|
8 |
-
import shutil
|
9 |
-
import tempfile
|
10 |
-
import traceback
|
11 |
-
import pandas as pd
|
12 |
-
from pathlib import Path
|
13 |
-
import config
|
14 |
-
from utils.helpers import create_directory_if_not_exists
|
15 |
-
|
16 |
-
|
17 |
-
class DWGHandler:
|
18 |
-
"""معالج ملفات DWG (AutoCAD)"""
|
19 |
-
|
20 |
-
def __init__(self, converter_path=None):
|
21 |
-
"""
|
22 |
-
تهيئة معالج ملفات DWG
|
23 |
-
|
24 |
-
المعلمات:
|
25 |
-
converter_path: مسار برنامج تحويل DWG (اختياري)
|
26 |
-
"""
|
27 |
-
# محاولة تحديد مسار برنامج تحويل DWG
|
28 |
-
self.converter_path = converter_path
|
29 |
-
|
30 |
-
if not self.converter_path:
|
31 |
-
# البحث عن المسار في إعدادات النظام
|
32 |
-
if hasattr(config, 'DWG_CONVERTER_PATH') and config.DWG_CONVERTER_PATH:
|
33 |
-
self.converter_path = config.DWG_CONVERTER_PATH
|
34 |
-
else:
|
35 |
-
# محاولة البحث عن البرامج المعروفة
|
36 |
-
possible_paths = [
|
37 |
-
r"C:\Program Files\Autodesk\AutoCAD 2022\accoreconsole.exe",
|
38 |
-
r"C:\Program Files\Autodesk\AutoCAD 2021\accoreconsole.exe",
|
39 |
-
r"C:\Program Files\Autodesk\AutoCAD 2020\accoreconsole.exe",
|
40 |
-
r"C:\Program Files\ODA\ODAFileConverter\ODAFileConverter.exe"
|
41 |
-
]
|
42 |
-
|
43 |
-
for path in possible_paths:
|
44 |
-
if os.path.exists(path):
|
45 |
-
self.converter_path = path
|
46 |
-
break
|
47 |
-
|
48 |
-
def convert_to_pdf(self, dwg_path, output_path=None):
|
49 |
-
"""
|
50 |
-
تحويل ملف DWG إلى PDF
|
51 |
-
|
52 |
-
المعلمات:
|
53 |
-
dwg_path: مسار ملف DWG
|
54 |
-
output_path: مسار ملف الإخراج (اختياري)
|
55 |
-
|
56 |
-
الإرجاع:
|
57 |
-
مسار ملف PDF الناتج
|
58 |
-
"""
|
59 |
-
try:
|
60 |
-
# التحقق من وجود الملف
|
61 |
-
if not os.path.exists(dwg_path):
|
62 |
-
raise FileNotFoundError(f"ملف DWG غير موجود: {dwg_path}")
|
63 |
-
|
64 |
-
# التحقق من وجود برنامج التحويل
|
65 |
-
if not self.converter_path or not os.path.exists(self.converter_path):
|
66 |
-
raise FileNotFoundError("لم يتم العثور على برنامج تحويل DWG")
|
67 |
-
|
68 |
-
# تحديد مسار الإخراج
|
69 |
-
if not output_path:
|
70 |
-
output_path = os.path.splitext(dwg_path)[0] + ".pdf"
|
71 |
-
|
72 |
-
# إنشاء مجلد الإخراج إذا لم يكن موجودًا
|
73 |
-
output_dir = os.path.dirname(output_path)
|
74 |
-
create_directory_if_not_exists(output_dir)
|
75 |
-
|
76 |
-
# تنفيذ عملية التحويل باستخدام برنامج التحويل المناسب
|
77 |
-
if "accoreconsole.exe" in self.converter_path.lower():
|
78 |
-
# استخدام AutoCAD Core Console لتحويل الملف
|
79 |
-
script_content = f"""
|
80 |
-
/i "{dwg_path}"
|
81 |
-
/e
|
82 |
-
/o "{output_path}"
|
83 |
-
"""
|
84 |
-
script_path = tempfile.mktemp(suffix=".scr")
|
85 |
-
with open(script_path, "w") as f:
|
86 |
-
f.write(script_content)
|
87 |
-
|
88 |
-
cmd = [self.converter_path, "/s", script_path]
|
89 |
-
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
90 |
-
|
91 |
-
os.unlink(script_path) # حذف ملف النص البرمجي المؤقت
|
92 |
-
|
93 |
-
if result.returncode != 0:
|
94 |
-
raise RuntimeError(f"فشل في تحويل ملف DWG: {result.stderr}")
|
95 |
-
|
96 |
-
elif "odafileconverter.exe" in self.converter_path.lower():
|
97 |
-
# استخدام ODA File Converter لتحويل الملف
|
98 |
-
input_dir = os.path.dirname(dwg_path)
|
99 |
-
output_dir = os.path.dirname(output_path)
|
100 |
-
input_filename = os.path.basename(dwg_path)
|
101 |
-
output_format = "PDF"
|
102 |
-
|
103 |
-
cmd = [
|
104 |
-
self.converter_path,
|
105 |
-
input_dir,
|
106 |
-
output_dir,
|
107 |
-
"DWG",
|
108 |
-
output_format,
|
109 |
-
"1",
|
110 |
-
"1",
|
111 |
-
input_filename
|
112 |
-
]
|
113 |
-
|
114 |
-
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
115 |
-
|
116 |
-
if result.returncode != 0:
|
117 |
-
raise RuntimeError(f"فشل في تحويل ملف DWG: {result.stderr}")
|
118 |
-
|
119 |
-
else:
|
120 |
-
raise NotImplementedError(f"برنامج التحويل غير مدعوم: {self.converter_path}")
|
121 |
-
|
122 |
-
# التحقق من وجود ملف الإخراج
|
123 |
-
if not os.path.exists(output_path):
|
124 |
-
raise FileNotFoundError(f"لم يتم إنشاء ملف PDF: {output_path}")
|
125 |
-
|
126 |
-
return output_path
|
127 |
-
|
128 |
-
except Exception as e:
|
129 |
-
error_msg = f"خطأ في تحويل ملف DWG إلى PDF: {str(e)}"
|
130 |
-
print(error_msg)
|
131 |
-
traceback.print_exc()
|
132 |
-
raise Exception(error_msg)
|
133 |
-
|
134 |
-
def extract_quantities(self, dwg_path):
|
135 |
-
"""
|
136 |
-
استخراج الكميات من ملف DWG
|
137 |
-
|
138 |
-
المعلمات:
|
139 |
-
dwg_path: مسار ملف DWG
|
140 |
-
|
141 |
-
الإرجاع:
|
142 |
-
DataFrame يحتوي على الكميات المستخرجة
|
143 |
-
"""
|
144 |
-
try:
|
145 |
-
# تحويل ملف DWG إلى PDF أولاً
|
146 |
-
pdf_path = self.convert_to_pdf(dwg_path)
|
147 |
-
|
148 |
-
# استخدام معالج ملفات PDF لاستخراج الكميات
|
149 |
-
from utils.pdf_handler import extract_quantities_from_pdf
|
150 |
-
return extract_quantities_from_pdf(pdf_path)
|
151 |
-
|
152 |
-
except Exception as e:
|
153 |
-
error_msg = f"خطأ في استخراج الكميات من ملف DWG: {str(e)}"
|
154 |
-
print(error_msg)
|
155 |
-
traceback.print_exc()
|
156 |
-
raise Exception(error_msg)
|
157 |
-
|
158 |
-
def extract_text(self, dwg_path):
|
159 |
-
"""
|
160 |
-
استخراج النص من ملف DWG
|
161 |
-
|
162 |
-
المعلمات:
|
163 |
-
dwg_path: مسار ملف DWG
|
164 |
-
|
165 |
-
الإرجاع:
|
166 |
-
النص المستخرج من ملف DWG
|
167 |
-
"""
|
168 |
-
try:
|
169 |
-
# تحويل ملف DWG إلى PDF أولاً
|
170 |
-
pdf_path = self.convert_to_pdf(dwg_path)
|
171 |
-
|
172 |
-
# استخدام معالج ملفات PDF لاستخراج النص
|
173 |
-
from utils.pdf_handler import extract_text_from_pdf
|
174 |
-
return extract_text_from_pdf(pdf_path)
|
175 |
-
|
176 |
-
except Exception as e:
|
177 |
-
error_msg = f"خطأ في استخراج النص من ملف DWG: {str(e)}"
|
178 |
-
print(error_msg)
|
179 |
-
traceback.print_exc()
|
180 |
-
raise Exception(error_msg)
|
181 |
-
|
182 |
-
def get_dwg_info(self, dwg_path):
|
183 |
-
"""
|
184 |
-
الحصول على معلومات ملف DWG
|
185 |
-
|
186 |
-
المعلمات:
|
187 |
-
dwg_path: مسار ملف DWG
|
188 |
-
|
189 |
-
الإرجاع:
|
190 |
-
قاموس يحتوي على معلومات الملف
|
191 |
-
"""
|
192 |
-
try:
|
193 |
-
# التحقق من وجود الملف
|
194 |
-
if not os.path.exists(dwg_path):
|
195 |
-
raise FileNotFoundError(f"ملف DWG غير موجود: {dwg_path}")
|
196 |
-
|
197 |
-
# الحصول على معلومات الملف الأساسية
|
198 |
-
file_info = {
|
199 |
-
'filename': os.path.basename(dwg_path),
|
200 |
-
'path': dwg_path,
|
201 |
-
'size': os.path.getsize(dwg_path),
|
202 |
-
'modified_date': os.path.getmtime(dwg_path)
|
203 |
-
}
|
204 |
-
|
205 |
-
# محاولة استخراج معلومات إضافية من الملف
|
206 |
-
# ملاحظة: هذا يتطلب مكتبات إضافية أو استخدام برامج خارجية
|
207 |
-
|
208 |
-
return file_info
|
209 |
-
|
210 |
-
except Exception as e:
|
211 |
-
error_msg = f"خطأ في الحصول على معلومات ملف DWG: {str(e)}"
|
212 |
-
print(error_msg)
|
213 |
-
traceback.print_exc()
|
214 |
-
raise Exception(error_msg)
|
215 |
-
|
216 |
-
def is_converter_available(self):
|
217 |
-
"""
|
218 |
-
التحقق من توفر برنامج تحويل DWG
|
219 |
-
|
220 |
-
الإرجاع:
|
221 |
-
True إذا كان برنامج التحويل متوفرًا، False خلاف ذلك
|
222 |
-
"""
|
223 |
-
return self.converter_path is not None and os.path.exists(self.converter_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/excel_handler.py
DELETED
@@ -1,486 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
معالج ملفات Excel
|
3 |
-
"""
|
4 |
-
|
5 |
-
import pandas as pd
|
6 |
-
import os
|
7 |
-
import numpy as np
|
8 |
-
import xlsxwriter
|
9 |
-
from datetime import datetime
|
10 |
-
import traceback
|
11 |
-
import config
|
12 |
-
from utils.helpers import create_directory_if_not_exists, get_file_extension, format_number
|
13 |
-
|
14 |
-
|
15 |
-
def read_excel_file(file_path, sheet_name=0, header=0, skip_rows=None):
|
16 |
-
"""
|
17 |
-
قراءة ملف Excel
|
18 |
-
|
19 |
-
المعلمات:
|
20 |
-
file_path: مسار ملف Excel
|
21 |
-
sheet_name: اسم أو رقم الصفحة (افتراضي: 0)
|
22 |
-
header: رقم الصف الذي يحتوي على العناوين (افتراضي: 0)
|
23 |
-
skip_rows: قائمة بأرقام الصفوف للتخطي (افتراضي: None)
|
24 |
-
|
25 |
-
الإرجاع:
|
26 |
-
DataFrame من البيانات المقروءة
|
27 |
-
"""
|
28 |
-
try:
|
29 |
-
# التحقق من وجود الملف
|
30 |
-
if not os.path.exists(file_path):
|
31 |
-
raise FileNotFoundError(f"الملف غير موجود: {file_path}")
|
32 |
-
|
33 |
-
# التحقق من امتداد الملف
|
34 |
-
ext = get_file_extension(file_path)
|
35 |
-
if ext not in ['.xlsx', '.xls', '.xlsm']:
|
36 |
-
raise ValueError(f"نوع الملف غير مدعوم: {ext}. يجب أن يكون الملف بامتداد .xlsx أو .xls أو .xlsm")
|
37 |
-
|
38 |
-
# قراءة الملف
|
39 |
-
df = pd.read_excel(
|
40 |
-
file_path,
|
41 |
-
sheet_name=sheet_name,
|
42 |
-
header=header,
|
43 |
-
skiprows=skip_rows
|
44 |
-
)
|
45 |
-
|
46 |
-
return df
|
47 |
-
|
48 |
-
except Exception as e:
|
49 |
-
error_msg = f"خطأ في قراءة ملف Excel: {str(e)}"
|
50 |
-
print(error_msg)
|
51 |
-
traceback.print_exc()
|
52 |
-
raise Exception(error_msg)
|
53 |
-
|
54 |
-
|
55 |
-
def write_excel_file(df, file_path, sheet_name="Sheet1", index=False, freeze_panes=None, column_widths=None, formats=None):
|
56 |
-
"""
|
57 |
-
كتابة DataFrame إلى ملف Excel
|
58 |
-
|
59 |
-
المعلمات:
|
60 |
-
df: DataFrame المراد كتابته
|
61 |
-
file_path: مسار ملف Excel
|
62 |
-
sheet_name: اسم الصفحة (افتراضي: Sheet1)
|
63 |
-
index: ما إذا كان سيتم تضمين الفهرس (افتراضي: False)
|
64 |
-
freeze_panes: صف وعمود لتجميد الألواح (افتراضي: None)
|
65 |
-
column_widths: قاموس لعرض الأعمدة {column_name: width}
|
66 |
-
formats: قاموس لتنسيقات الأعمدة {column_name: format_function}
|
67 |
-
|
68 |
-
الإرجاع:
|
69 |
-
True في حالة النجاح
|
70 |
-
"""
|
71 |
-
try:
|
72 |
-
# التأكد من وجود المجلد
|
73 |
-
create_directory_if_not_exists(os.path.dirname(file_path))
|
74 |
-
|
75 |
-
# تحديد المكتب والورقة
|
76 |
-
writer = pd.ExcelWriter(file_path, engine='xlsxwriter')
|
77 |
-
df.to_excel(writer, sheet_name=sheet_name, index=index)
|
78 |
-
|
79 |
-
# الحصول على مرجع لورقة العمل
|
80 |
-
workbook = writer.book
|
81 |
-
worksheet = writer.sheets[sheet_name]
|
82 |
-
|
83 |
-
# إنشاء تنسيقات مخصصة
|
84 |
-
header_format = workbook.add_format({
|
85 |
-
'bold': True,
|
86 |
-
'bg_color': '#CCCCCC',
|
87 |
-
'border': 1,
|
88 |
-
'align': 'center',
|
89 |
-
'valign': 'vcenter',
|
90 |
-
'text_wrap': True
|
91 |
-
})
|
92 |
-
|
93 |
-
number_format = workbook.add_format({
|
94 |
-
'num_format': '#,##0.00',
|
95 |
-
'align': 'right'
|
96 |
-
})
|
97 |
-
|
98 |
-
currency_format = workbook.add_format({
|
99 |
-
'num_format': '_-* #,##0.00 [$ريال]_-;-* #,##0.00 [$ريال]_-;_-* "-" [$ريال]_-;_-@_-',
|
100 |
-
'align': 'right'
|
101 |
-
})
|
102 |
-
|
103 |
-
date_format = workbook.add_format({
|
104 |
-
'num_format': 'yyyy-mm-dd',
|
105 |
-
'align': 'center'
|
106 |
-
})
|
107 |
-
|
108 |
-
text_format = workbook.add_format({
|
109 |
-
'align': 'right',
|
110 |
-
'text_wrap': True
|
111 |
-
})
|
112 |
-
|
113 |
-
# تطبيق التنسيقات على العناوين
|
114 |
-
for col_num, value in enumerate(df.columns.values):
|
115 |
-
worksheet.write(0, col_num + (1 if index else 0), value, header_format)
|
116 |
-
|
117 |
-
# تعيين حجم الأعمدة
|
118 |
-
if column_widths:
|
119 |
-
for col_name, width in column_widths.items():
|
120 |
-
if col_name in df.columns:
|
121 |
-
col_idx = df.columns.get_loc(col_name) + (1 if index else 0)
|
122 |
-
worksheet.set_column(col_idx, col_idx, width)
|
123 |
-
else:
|
124 |
-
# ضبط عرض الأعمدة تلقائيًا
|
125 |
-
for col_num, col_name in enumerate(df.columns):
|
126 |
-
max_len = df[col_name].astype(str).map(len).max()
|
127 |
-
col_len = max(max_len, len(str(col_name))) + 2
|
128 |
-
worksheet.set_column(col_num + (1 if index else 0), col_num + (1 if index else 0), col_len)
|
129 |
-
|
130 |
-
# تطبيق التنسيقات حسب نوع البيانات
|
131 |
-
for row_num in range(len(df)):
|
132 |
-
for col_num, col_name in enumerate(df.columns):
|
133 |
-
cell_value = df.iloc[row_num, col_num]
|
134 |
-
cell_format = text_format
|
135 |
-
|
136 |
-
# تحديد التنسيق المناسب بناءً على نوع البيانات
|
137 |
-
if pd.api.types.is_numeric_dtype(df[col_name].dtype):
|
138 |
-
if any(curr in col_name.lower() for curr in ['سعر', 'تكلفة', 'قيمة', 'مبلغ', 'ريال']):
|
139 |
-
cell_format = currency_format
|
140 |
-
else:
|
141 |
-
cell_format = number_format
|
142 |
-
elif pd.api.types.is_datetime64_dtype(df[col_name].dtype):
|
143 |
-
cell_format = date_format
|
144 |
-
|
145 |
-
# استخدام تنسيق مخصص إذا تم توفيره
|
146 |
-
if formats and col_name in formats:
|
147 |
-
custom_format = formats[col_name]
|
148 |
-
if callable(custom_format):
|
149 |
-
# إذا كان دالة، استدعاها لتطبيق التنسيق
|
150 |
-
cell_value = custom_format(cell_value)
|
151 |
-
else:
|
152 |
-
# إذا كان تنسيق، استخدمه
|
153 |
-
cell_format = custom_format
|
154 |
-
|
155 |
-
worksheet.write(row_num + 1, col_num + (1 if index else 0), cell_value, cell_format)
|
156 |
-
|
157 |
-
# تجميد الألواح إذا تم تحديده
|
158 |
-
if freeze_panes:
|
159 |
-
worksheet.freeze_panes(*freeze_panes)
|
160 |
-
|
161 |
-
# حفظ الملف
|
162 |
-
writer.close()
|
163 |
-
|
164 |
-
return True
|
165 |
-
|
166 |
-
except Exception as e:
|
167 |
-
error_msg = f"خطأ في كتابة ملف Excel: {str(e)}"
|
168 |
-
print(error_msg)
|
169 |
-
traceback.print_exc()
|
170 |
-
raise Exception(error_msg)
|
171 |
-
|
172 |
-
|
173 |
-
def export_to_excel(data, file_path, sheet_name="Sheet1", customize_func=None):
|
174 |
-
"""
|
175 |
-
تصدير البيانات إلى ملف Excel مع خيارات تخصيص
|
176 |
-
|
177 |
-
المعلمات:
|
178 |
-
data: DataFrame أو قاموس من DataFrames للتصدير
|
179 |
-
file_path: مسار ملف Excel
|
180 |
-
sheet_name: اسم الصفحة (افتراضي: Sheet1)
|
181 |
-
customize_func: دالة لتخصيص المصنف قبل الحفظ (افتراضي: None)
|
182 |
-
|
183 |
-
الإرجاع:
|
184 |
-
True في حالة النجاح
|
185 |
-
"""
|
186 |
-
try:
|
187 |
-
# التأكد من وجود المجلد
|
188 |
-
create_directory_if_not_exists(os.path.dirname(file_path))
|
189 |
-
|
190 |
-
# إنشاء كائن الكاتب
|
191 |
-
writer = pd.ExcelWriter(file_path, engine='xlsxwriter')
|
192 |
-
|
193 |
-
# تصدير البيانات
|
194 |
-
if isinstance(data, pd.DataFrame):
|
195 |
-
# إذا كان DataFrame واحد
|
196 |
-
data.to_excel(writer, sheet_name=sheet_name, index=False)
|
197 |
-
elif isinstance(data, dict):
|
198 |
-
# إذا كان قاموس من DataFrames
|
199 |
-
for sheet, df in data.items():
|
200 |
-
if isinstance(df, pd.DataFrame):
|
201 |
-
df.to_excel(writer, sheet_name=sheet, index=False)
|
202 |
-
else:
|
203 |
-
raise ValueError("البيانات يجب أن تكون DataFrame أو قاموس من DataFrames")
|
204 |
-
|
205 |
-
# تطبيق التخصيص إذا تم توفيره
|
206 |
-
if customize_func and callable(customize_func):
|
207 |
-
customize_func(writer)
|
208 |
-
|
209 |
-
# حفظ الملف
|
210 |
-
writer.close()
|
211 |
-
|
212 |
-
return True
|
213 |
-
|
214 |
-
except Exception as e:
|
215 |
-
error_msg = f"خطأ في تصدير البيانات إلى Excel: {str(e)}"
|
216 |
-
print(error_msg)
|
217 |
-
traceback.print_exc()
|
218 |
-
raise Exception(error_msg)
|
219 |
-
|
220 |
-
|
221 |
-
def read_sheets_from_excel(file_path):
|
222 |
-
"""
|
223 |
-
قراءة جميع صفحات ملف Excel
|
224 |
-
|
225 |
-
المعلمات:
|
226 |
-
file_path: مسار ملف Excel
|
227 |
-
|
228 |
-
الإرجاع:
|
229 |
-
قاموس من DataFrames بأسماء الصفحات كمفاتيح
|
230 |
-
"""
|
231 |
-
try:
|
232 |
-
# التحقق من وجود الملف
|
233 |
-
if not os.path.exists(file_path):
|
234 |
-
raise FileNotFoundError(f"الملف غير موجود: {file_path}")
|
235 |
-
|
236 |
-
# التحقق من امتداد الملف
|
237 |
-
ext = get_file_extension(file_path)
|
238 |
-
if ext not in ['.xlsx', '.xls', '.xlsm']:
|
239 |
-
raise ValueError(f"نوع الملف غير مدعوم: {ext}. يجب أن يكون الملف بامتداد .xlsx أو .xls أو .xlsm")
|
240 |
-
|
241 |
-
# قراءة جميع الصفحات من الملف
|
242 |
-
excel_file = pd.ExcelFile(file_path)
|
243 |
-
sheets = {}
|
244 |
-
|
245 |
-
for sheet_name in excel_file.sheet_names:
|
246 |
-
sheets[sheet_name] = pd.read_excel(excel_file, sheet_name=sheet_name)
|
247 |
-
|
248 |
-
return sheets
|
249 |
-
|
250 |
-
except Exception as e:
|
251 |
-
error_msg = f"خطأ في قراءة صفحات ملف Excel: {str(e)}"
|
252 |
-
print(error_msg)
|
253 |
-
traceback.print_exc()
|
254 |
-
raise Exception(error_msg)
|
255 |
-
|
256 |
-
|
257 |
-
def create_excel_report(data_dict, file_path, formats=None, column_widths=None, title=None, subtitle=None):
|
258 |
-
"""
|
259 |
-
إنشاء تقرير Excel متقدم
|
260 |
-
|
261 |
-
المعلمات:
|
262 |
-
data_dict: قاموس من DataFrames للتصدير {sheet_name: DataFrame}
|
263 |
-
file_path: مسار ملف Excel
|
264 |
-
formats: قاموس للتنسيقات {sheet_name: {column_name: format}}
|
265 |
-
column_widths: قاموس لعرض الأعمدة {sheet_name: {column_name: width}}
|
266 |
-
title: عنوان التقرير
|
267 |
-
subtitle: العنوان الفرعي للتقرير
|
268 |
-
|
269 |
-
الإرجاع:
|
270 |
-
True في حالة النجاح
|
271 |
-
"""
|
272 |
-
try:
|
273 |
-
# التأكد من وجود المجلد
|
274 |
-
create_directory_if_not_exists(os.path.dirname(file_path))
|
275 |
-
|
276 |
-
# إنشاء كائن الكاتب
|
277 |
-
writer = pd.ExcelWriter(file_path, engine='xlsxwriter')
|
278 |
-
workbook = writer.book
|
279 |
-
|
280 |
-
# إنشاء التنسيقات العامة
|
281 |
-
header_format = workbook.add_format({
|
282 |
-
'bold': True,
|
283 |
-
'bg_color': '#CCCCCC',
|
284 |
-
'border': 1,
|
285 |
-
'align': 'center',
|
286 |
-
'valign': 'vcenter',
|
287 |
-
'text_wrap': True
|
288 |
-
})
|
289 |
-
|
290 |
-
title_format = workbook.add_format({
|
291 |
-
'bold': True,
|
292 |
-
'font_size': 16,
|
293 |
-
'align': 'center',
|
294 |
-
'valign': 'vcenter',
|
295 |
-
'bg_color': '#E0E0E0',
|
296 |
-
'border': 2
|
297 |
-
})
|
298 |
-
|
299 |
-
subtitle_format = workbook.add_format({
|
300 |
-
'font_size': 12,
|
301 |
-
'align': 'center',
|
302 |
-
'valign': 'vcenter',
|
303 |
-
'bg_color': '#E0E0E0',
|
304 |
-
'border': 1
|
305 |
-
})
|
306 |
-
|
307 |
-
date_format = workbook.add_format({
|
308 |
-
'num_format': 'yyyy-mm-dd',
|
309 |
-
'align': 'center'
|
310 |
-
})
|
311 |
-
|
312 |
-
number_format = workbook.add_format({
|
313 |
-
'num_format': '#,##0.00',
|
314 |
-
'align': 'right'
|
315 |
-
})
|
316 |
-
|
317 |
-
currency_format = workbook.add_format({
|
318 |
-
'num_format': '_-* #,##0.00 [$ريال]_-;-* #,##0.00 [$ريال]_-;_-* "-" [$ريال]_-;_-@_-',
|
319 |
-
'align': 'right'
|
320 |
-
})
|
321 |
-
|
322 |
-
percent_format = workbook.add_format({
|
323 |
-
'num_format': '0.00%',
|
324 |
-
'align': 'right'
|
325 |
-
})
|
326 |
-
|
327 |
-
text_format = workbook.add_format({
|
328 |
-
'align': 'right',
|
329 |
-
'text_wrap': True
|
330 |
-
})
|
331 |
-
|
332 |
-
# تصدير البيانات
|
333 |
-
current_row = 0
|
334 |
-
|
335 |
-
# إضافة العنوان والعنوان الفرعي إذا تم توفيرهما
|
336 |
-
if title or subtitle:
|
337 |
-
for sheet_name in data_dict.keys():
|
338 |
-
worksheet = workbook.add_worksheet(sheet_name)
|
339 |
-
current_row = 0
|
340 |
-
|
341 |
-
if title:
|
342 |
-
worksheet.merge_range('A1:J1', title, title_format)
|
343 |
-
current_row += 1
|
344 |
-
|
345 |
-
if subtitle:
|
346 |
-
worksheet.merge_range(f'A{current_row + 1}:J{current_row + 1}', subtitle, subtitle_format)
|
347 |
-
current_row += 1
|
348 |
-
|
349 |
-
# إضافة فاصل
|
350 |
-
current_row += 1
|
351 |
-
|
352 |
-
# كتابة البيانات
|
353 |
-
df = data_dict[sheet_name]
|
354 |
-
df.to_excel(writer, sheet_name=sheet_name, startrow=current_row, index=False)
|
355 |
-
|
356 |
-
# تنسيق العناوين
|
357 |
-
for col_num, value in enumerate(df.columns.values):
|
358 |
-
worksheet.write(current_row, col_num, value, header_format)
|
359 |
-
|
360 |
-
# تطبيق التنسيقات المخصصة
|
361 |
-
if formats and sheet_name in formats:
|
362 |
-
sheet_formats = formats[sheet_name]
|
363 |
-
for col_name, fmt in sheet_formats.items():
|
364 |
-
if col_name in df.columns:
|
365 |
-
col_idx = df.columns.get_loc(col_name)
|
366 |
-
for row_num in range(len(df)):
|
367 |
-
cell_value = df.iloc[row_num, col_idx]
|
368 |
-
worksheet.write(row_num + current_row + 1, col_idx, cell_value, fmt)
|
369 |
-
|
370 |
-
# تعيين عرض الأعمدة
|
371 |
-
if column_widths and sheet_name in column_widths:
|
372 |
-
sheet_widths = column_widths[sheet_name]
|
373 |
-
for col_name, width in sheet_widths.items():
|
374 |
-
if col_name in df.columns:
|
375 |
-
col_idx = df.columns.get_loc(col_name)
|
376 |
-
worksheet.set_column(col_idx, col_idx, width)
|
377 |
-
else:
|
378 |
-
# ضبط عرض الأعمدة تلقائيًا
|
379 |
-
for col_num, col_name in enumerate(df.columns):
|
380 |
-
max_len = df[col_name].astype(str).map(len).max()
|
381 |
-
col_len = max(max_len, len(str(col_name))) + 2
|
382 |
-
worksheet.set_column(col_num, col_num, col_len)
|
383 |
-
else:
|
384 |
-
# إذا لم يتم توفير عنوان أو عنوان فرعي، استخدم الطريقة العادية
|
385 |
-
for sheet_name, df in data_dict.items():
|
386 |
-
df.to_excel(writer, sheet_name=sheet_name, index=False)
|
387 |
-
worksheet = writer.sheets[sheet_name]
|
388 |
-
|
389 |
-
# تنسيق العناوين
|
390 |
-
for col_num, value in enumerate(df.columns.values):
|
391 |
-
worksheet.write(0, col_num, value, header_format)
|
392 |
-
|
393 |
-
# تطبيق التنسيقات المخصصة
|
394 |
-
if formats and sheet_name in formats:
|
395 |
-
sheet_formats = formats[sheet_name]
|
396 |
-
for col_name, fmt in sheet_formats.items():
|
397 |
-
if col_name in df.columns:
|
398 |
-
col_idx = df.columns.get_loc(col_name)
|
399 |
-
for row_num in range(len(df)):
|
400 |
-
cell_value = df.iloc[row_num, col_idx]
|
401 |
-
worksheet.write(row_num + 1, col_idx, cell_value, fmt)
|
402 |
-
|
403 |
-
# تعيين عرض الأعمدة
|
404 |
-
if column_widths and sheet_name in column_widths:
|
405 |
-
sheet_widths = column_widths[sheet_name]
|
406 |
-
for col_name, width in sheet_widths.items():
|
407 |
-
if col_name in df.columns:
|
408 |
-
col_idx = df.columns.get_loc(col_name)
|
409 |
-
worksheet.set_column(col_idx, col_idx, width)
|
410 |
-
else:
|
411 |
-
# ضبط عرض الأعمدة تلقائيًا
|
412 |
-
for col_num, col_name in enumerate(df.columns):
|
413 |
-
max_len = df[col_name].astype(str).map(len).max()
|
414 |
-
col_len = max(max_len, len(str(col_name))) + 2
|
415 |
-
worksheet.set_column(col_num, col_num, col_len)
|
416 |
-
|
417 |
-
# حفظ الملف
|
418 |
-
writer.close()
|
419 |
-
|
420 |
-
return True
|
421 |
-
|
422 |
-
except Exception as e:
|
423 |
-
error_msg = f"خطأ في إنشاء تقرير Excel: {str(e)}"
|
424 |
-
print(error_msg)
|
425 |
-
traceback.print_exc()
|
426 |
-
raise Exception(error_msg)
|
427 |
-
|
428 |
-
|
429 |
-
def extract_data_from_excel(file_path, columns_mapping=None, sheet_name=0, header_row=0, data_start_row=1):
|
430 |
-
"""
|
431 |
-
استخراج بيانات منظمة من ملف Excel
|
432 |
-
|
433 |
-
المعلمات:
|
434 |
-
file_path: مسار ملف Excel
|
435 |
-
columns_mapping: قاموس لتخطيط الأعمدة {اسم_العمود_الجديد: اسم_العمود_الأصلي}
|
436 |
-
sheet_name: اسم أو رقم الصفحة (افتراضي: 0)
|
437 |
-
header_row: رقم صف العناوين (افتراضي: 0)
|
438 |
-
data_start_row: رقم صف بداية البيانات (افتراضي: 1)
|
439 |
-
|
440 |
-
الإرجاع:
|
441 |
-
DataFrame من البيانات المستخرجة
|
442 |
-
"""
|
443 |
-
try:
|
444 |
-
# قراءة الملف
|
445 |
-
df = pd.read_excel(
|
446 |
-
file_path,
|
447 |
-
sheet_name=sheet_name,
|
448 |
-
header=header_row,
|
449 |
-
skiprows=range(1, data_start_row) if data_start_row > 1 else None
|
450 |
-
)
|
451 |
-
|
452 |
-
# تنظيف العناوين (إزالة المسافات الزائدة)
|
453 |
-
df.columns = df.columns.str.strip()
|
454 |
-
|
455 |
-
# إعادة تسمية الأعمدة إذا تم توفير تخطيط
|
456 |
-
if columns_mapping:
|
457 |
-
# التحقق من وجود جميع الأعمدة المطلوبة
|
458 |
-
missing_columns = [col for col in columns_mapping.values() if col not in df.columns]
|
459 |
-
if missing_columns:
|
460 |
-
raise ValueError(f"الأعمدة التالية غير موجودة في الملف: {', '.join(missing_columns)}")
|
461 |
-
|
462 |
-
# إعادة تسمية الأعمدة
|
463 |
-
df = df.rename(columns={v: k for k, v in columns_mapping.items()})
|
464 |
-
|
465 |
-
# اختيار الأعمدة المطلوبة فقط
|
466 |
-
df = df[list(columns_mapping.keys())]
|
467 |
-
|
468 |
-
# تنظيف البيانات
|
469 |
-
for col in df.columns:
|
470 |
-
# تحويل الأعمدة النصية
|
471 |
-
if df[col].dtype == 'object':
|
472 |
-
df[col] = df[col].astype(str).str.strip()
|
473 |
-
|
474 |
-
# محاولة تحويل الأعمدة الرقمية
|
475 |
-
try:
|
476 |
-
df[col] = pd.to_numeric(df[col], errors='ignore')
|
477 |
-
except:
|
478 |
-
pass
|
479 |
-
|
480 |
-
return df
|
481 |
-
|
482 |
-
except Exception as e:
|
483 |
-
error_msg = f"خطأ في استخراج البيانات من ملف Excel: {str(e)}"
|
484 |
-
print(error_msg)
|
485 |
-
traceback.print_exc()
|
486 |
-
raise Exception(error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/helpers.py
DELETED
@@ -1,332 +0,0 @@
|
|
1 |
-
#!/usr/bin/env python
|
2 |
-
# -*- coding: utf-8 -*-
|
3 |
-
|
4 |
-
"""
|
5 |
-
وحدة المساعدة المركزية للنظام
|
6 |
-
تحتوي على دوال مساعدة مشتركة تستخدم في جميع أنحاء التطبيق
|
7 |
-
"""
|
8 |
-
|
9 |
-
import os
|
10 |
-
import sys
|
11 |
-
import streamlit as st
|
12 |
-
import pandas as pd
|
13 |
-
import numpy as np
|
14 |
-
import json
|
15 |
-
import re
|
16 |
-
import time
|
17 |
-
import tempfile
|
18 |
-
from datetime import datetime, timedelta
|
19 |
-
import random
|
20 |
-
import secrets
|
21 |
-
import shutil
|
22 |
-
import base64
|
23 |
-
import logging
|
24 |
-
from pathlib import Path
|
25 |
-
|
26 |
-
# تكوين التسجيلات
|
27 |
-
logging.basicConfig(
|
28 |
-
level=logging.INFO,
|
29 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
30 |
-
)
|
31 |
-
logger = logging.getLogger("wahbi-ai")
|
32 |
-
|
33 |
-
|
34 |
-
def create_directory_if_not_exists(directory_path):
|
35 |
-
"""إنشاء مسار إذا لم يكن موجوداً"""
|
36 |
-
try:
|
37 |
-
if not os.path.exists(directory_path):
|
38 |
-
os.makedirs(directory_path)
|
39 |
-
logger.info(f"تم إنشاء المجلد: {directory_path}")
|
40 |
-
return True
|
41 |
-
except Exception as e:
|
42 |
-
logger.error(f"خطأ في إنشاء المجلد {directory_path}: {e}")
|
43 |
-
return False
|
44 |
-
|
45 |
-
|
46 |
-
def get_data_folder():
|
47 |
-
"""الحصول على مسار مجلد البيانات الرئيسي"""
|
48 |
-
# مسار بيانات النظام الرئيسي
|
49 |
-
data_folder = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data")
|
50 |
-
create_directory_if_not_exists(data_folder)
|
51 |
-
return data_folder
|
52 |
-
|
53 |
-
|
54 |
-
def load_config():
|
55 |
-
"""تحميل إعدادات التكوين من ملف config.json"""
|
56 |
-
config_path = os.path.join(get_data_folder(), "config.json")
|
57 |
-
|
58 |
-
# إذا لم يكن ملف التكوين موجوداً، قم بإنشاء ملف افتراضي
|
59 |
-
if not os.path.exists(config_path):
|
60 |
-
default_config = {
|
61 |
-
"system": {
|
62 |
-
"version": "1.0.0",
|
63 |
-
"release_date": "2025-03-30",
|
64 |
-
"company_name": "شركة شبه الجزيرة للمقاولات",
|
65 |
-
"company_logo": "",
|
66 |
-
"language": "ar",
|
67 |
-
"theme": "light",
|
68 |
-
"debug_mode": False
|
69 |
-
},
|
70 |
-
"ai_models": {
|
71 |
-
"default_model": "claude-3-7-sonnet-20250219",
|
72 |
-
"openai_model": "gpt-4o",
|
73 |
-
"huggingface_model": "mistralai/Mistral-7B-Instruct-v0.2"
|
74 |
-
},
|
75 |
-
"notifications": {
|
76 |
-
"enable_email": False,
|
77 |
-
"enable_browser": True,
|
78 |
-
"check_interval": 60
|
79 |
-
}
|
80 |
-
}
|
81 |
-
|
82 |
-
with open(config_path, 'w', encoding='utf-8') as f:
|
83 |
-
json.dump(default_config, f, ensure_ascii=False, indent=2)
|
84 |
-
|
85 |
-
return default_config
|
86 |
-
|
87 |
-
# تحميل ملف التكوين الموجود
|
88 |
-
try:
|
89 |
-
with open(config_path, 'r', encoding='utf-8') as f:
|
90 |
-
return json.load(f)
|
91 |
-
except Exception as e:
|
92 |
-
logger.error(f"خطأ في تحميل ملف التكوين: {e}")
|
93 |
-
return {}
|
94 |
-
|
95 |
-
|
96 |
-
def save_config(config):
|
97 |
-
"""حفظ إعدادات التكوين إلى ملف config.json"""
|
98 |
-
config_path = os.path.join(get_data_folder(), "config.json")
|
99 |
-
|
100 |
-
try:
|
101 |
-
with open(config_path, 'w', encoding='utf-8') as f:
|
102 |
-
json.dump(config, f, ensure_ascii=False, indent=2)
|
103 |
-
return True
|
104 |
-
except Exception as e:
|
105 |
-
logger.error(f"خطأ في حفظ ملف التكوين: {e}")
|
106 |
-
return False
|
107 |
-
|
108 |
-
|
109 |
-
def format_time(timestamp=None, format_str="%Y-%m-%d %H:%M:%S"):
|
110 |
-
"""تنسيق الوقت إلى صيغة معينة"""
|
111 |
-
if timestamp is None:
|
112 |
-
timestamp = datetime.now()
|
113 |
-
elif isinstance(timestamp, (int, float)):
|
114 |
-
timestamp = datetime.fromtimestamp(timestamp)
|
115 |
-
|
116 |
-
return timestamp.strftime(format_str)
|
117 |
-
|
118 |
-
|
119 |
-
def get_user_info():
|
120 |
-
"""الحصول على معلومات المستخدم الحالي"""
|
121 |
-
# في التطبيق الفعلي، يمكن استرداد معلومات المستخدم من قاعدة البيانات أو من حالة الجلسة
|
122 |
-
if "user_info" in st.session_state:
|
123 |
-
return st.session_state.user_info
|
124 |
-
|
125 |
-
# معلومات افتراضية للتطوير
|
126 |
-
return {
|
127 |
-
"id": 1,
|
128 |
-
"username": "admin",
|
129 |
-
"full_name": "مدير النظام",
|
130 |
-
"email": "[email protected]",
|
131 |
-
"role": "مدير",
|
132 |
-
"department": "الإدارة",
|
133 |
-
"last_login": format_time()
|
134 |
-
}
|
135 |
-
|
136 |
-
|
137 |
-
def get_current_project():
|
138 |
-
"""الحصول على معلومات المشروع الحالي"""
|
139 |
-
if "current_project" in st.session_state:
|
140 |
-
return st.session_state.current_project
|
141 |
-
|
142 |
-
# في حالة عدم وجود مشروع محدد
|
143 |
-
return None
|
144 |
-
|
145 |
-
|
146 |
-
def load_icons():
|
147 |
-
"""تحميل الأيقونات المستخدمة في النظام"""
|
148 |
-
icons_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets", "icons")
|
149 |
-
icons = {}
|
150 |
-
|
151 |
-
# التأكد من وجود مجلد ��لأيقونات
|
152 |
-
if not os.path.exists(icons_path):
|
153 |
-
create_directory_if_not_exists(icons_path)
|
154 |
-
return icons
|
155 |
-
|
156 |
-
# تحميل جميع الأيقونات
|
157 |
-
for icon_file in os.listdir(icons_path):
|
158 |
-
if icon_file.endswith(('.png', '.svg', '.jpg')):
|
159 |
-
icon_name = os.path.splitext(icon_file)[0]
|
160 |
-
icon_path = os.path.join(icons_path, icon_file)
|
161 |
-
|
162 |
-
try:
|
163 |
-
with open(icon_path, "rb") as f:
|
164 |
-
icons[icon_name] = base64.b64encode(f.read()).decode()
|
165 |
-
except Exception as e:
|
166 |
-
logger.error(f"خطأ في تحميل الأيقونة {icon_name}: {e}")
|
167 |
-
|
168 |
-
return icons
|
169 |
-
|
170 |
-
|
171 |
-
def get_random_id(length=8):
|
172 |
-
"""إنشاء معرف عشوائي بطول محدد"""
|
173 |
-
return secrets.token_hex(length // 2)
|
174 |
-
|
175 |
-
|
176 |
-
def compress_text(text, max_length=10000):
|
177 |
-
"""اختصار النص إلى حد أقصى محدد مع الحفاظ على المعنى"""
|
178 |
-
if not text or len(text) <= max_length:
|
179 |
-
return text
|
180 |
-
|
181 |
-
# تقسيم النص إلى جمل
|
182 |
-
sentences = re.split(r'(?<=[.!?])\s+', text)
|
183 |
-
|
184 |
-
# حساب متوسط طول الجملة
|
185 |
-
avg_sentence_length = len(text) / len(sentences)
|
186 |
-
|
187 |
-
# حساب عدد الجمل التي يمكن تضمينها
|
188 |
-
num_sentences_to_keep = int(max_length / avg_sentence_length)
|
189 |
-
|
190 |
-
# الاحتفاظ بالجمل الأولى والأخيرة للحفاظ على السياق
|
191 |
-
keep_first = num_sentences_to_keep // 2
|
192 |
-
keep_last = num_sentences_to_keep - keep_first
|
193 |
-
|
194 |
-
# دمج الجمل المختارة
|
195 |
-
compressed_text = ' '.join(sentences[:keep_first] + sentences[-keep_last:])
|
196 |
-
|
197 |
-
# إضافة إشارة إلى أن النص تم اختصاره
|
198 |
-
if len(compressed_text) < len(text):
|
199 |
-
compressed_text += " [...المزيد من النص تم اختصاره...]"
|
200 |
-
|
201 |
-
return compressed_text
|
202 |
-
|
203 |
-
|
204 |
-
def str_to_bool(text):
|
205 |
-
"""تحويل النص إلى قيمة منطقية"""
|
206 |
-
return text.lower() in ('yes', 'true', 'y', 't', '1', 'نعم', 'صحيح')
|
207 |
-
|
208 |
-
|
209 |
-
def handle_arabic_text(text):
|
210 |
-
"""معالجة النص العربي للعرض بشكل صحيح"""
|
211 |
-
if not text:
|
212 |
-
return ""
|
213 |
-
|
214 |
-
# إضافة علامة RTL لضمان عرض النص العربي بشكل صحيح
|
215 |
-
return f"<div dir='rtl'>{text}</div>"
|
216 |
-
|
217 |
-
|
218 |
-
def render_credits():
|
219 |
-
"""عرض معلومات النظام وحقوق الملكية"""
|
220 |
-
st.markdown("---")
|
221 |
-
|
222 |
-
config = load_config()
|
223 |
-
system_info = config.get("system", {})
|
224 |
-
|
225 |
-
col1, col2, col3 = st.columns([1, 2, 1])
|
226 |
-
|
227 |
-
with col2:
|
228 |
-
st.markdown(
|
229 |
-
f"""
|
230 |
-
<div style="text-align: center; color: #666;">
|
231 |
-
<p>{system_info.get('company_name', 'شركة شبه الجزيرة للمقاولات')}</p>
|
232 |
-
<p>الإصدار: {system_info.get('version', '1.0.0')}</p>
|
233 |
-
<p>© جميع الحقوق محفوظة 2025</p>
|
234 |
-
</div>
|
235 |
-
""",
|
236 |
-
unsafe_allow_html=True
|
237 |
-
)
|
238 |
-
|
239 |
-
|
240 |
-
# دالة للحصول على اتصال قاعدة البيانات
|
241 |
-
def get_connection():
|
242 |
-
"""
|
243 |
-
دالة للحصول على اتصال بقاعدة البيانات
|
244 |
-
|
245 |
-
ملاحظة: ينبغي أن تكون مستبدلة بالدالة من db_connector.py في البيئة الإنتاجية
|
246 |
-
"""
|
247 |
-
try:
|
248 |
-
# استيراد المستوى الفعلي للاتصال بقاعدة البيانات
|
249 |
-
from database.db_connector import get_connection as get_db_connection
|
250 |
-
return get_db_connection()
|
251 |
-
except ImportError:
|
252 |
-
# إذا كان الاتصال بقاعدة البيانات غير متاح
|
253 |
-
logger.warning("لم يتم العثور على وحدة اتصال قاعدة البيانات. استخدام مخزن بيانات مؤقت.")
|
254 |
-
# إرجاع None للإشارة إلى عدم وجود اتصال
|
255 |
-
return None
|
256 |
-
|
257 |
-
|
258 |
-
def load_css(file_name=None):
|
259 |
-
"""تحميل ملف CSS مخصص"""
|
260 |
-
try:
|
261 |
-
if file_name:
|
262 |
-
css_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets", "css", file_name)
|
263 |
-
|
264 |
-
if os.path.exists(css_file):
|
265 |
-
with open(css_file, "r", encoding="utf-8") as f:
|
266 |
-
css_content = f.read()
|
267 |
-
else:
|
268 |
-
logger.warning(f"ملف CSS غير موجود: {css_file}")
|
269 |
-
return
|
270 |
-
else:
|
271 |
-
# CSS افتراضي
|
272 |
-
css_content = """
|
273 |
-
.sidebar .sidebar-content {
|
274 |
-
direction: rtl;
|
275 |
-
text-align: right;
|
276 |
-
}
|
277 |
-
div[data-testid="stForm"] {
|
278 |
-
border: 1px solid #ddd;
|
279 |
-
padding: 10px;
|
280 |
-
border-radius: 10px;
|
281 |
-
}
|
282 |
-
.module-title {
|
283 |
-
color: #1E88E5;
|
284 |
-
text-align: center;
|
285 |
-
font-size: 1.8rem;
|
286 |
-
margin-bottom: 1rem;
|
287 |
-
}
|
288 |
-
.instructions {
|
289 |
-
background-color: #f8f9fa;
|
290 |
-
border-right: 3px solid #4CAF50;
|
291 |
-
padding: 10px;
|
292 |
-
margin-bottom: 15px;
|
293 |
-
}
|
294 |
-
.results-container {
|
295 |
-
background-color: #f5f5f5;
|
296 |
-
padding: 15px;
|
297 |
-
border-radius: 5px;
|
298 |
-
margin-top: 20px;
|
299 |
-
}
|
300 |
-
.risk-high {
|
301 |
-
color: #d32f2f;
|
302 |
-
font-weight: bold;
|
303 |
-
}
|
304 |
-
.risk-medium {
|
305 |
-
color: #f57c00;
|
306 |
-
font-weight: bold;
|
307 |
-
}
|
308 |
-
.risk-low {
|
309 |
-
color: #388e3c;
|
310 |
-
font-weight: bold;
|
311 |
-
}
|
312 |
-
.form-container {
|
313 |
-
background-color: #f9f9f9;
|
314 |
-
padding: 20px;
|
315 |
-
border-radius: 10px;
|
316 |
-
margin-bottom: 20px;
|
317 |
-
}
|
318 |
-
.section-header {
|
319 |
-
color: #2196F3;
|
320 |
-
font-size: 1.2rem;
|
321 |
-
font-weight: bold;
|
322 |
-
margin-top: 20px;
|
323 |
-
margin-bottom: 10px;
|
324 |
-
border-bottom: 1px solid #eee;
|
325 |
-
padding-bottom: 5px;
|
326 |
-
}
|
327 |
-
"""
|
328 |
-
|
329 |
-
st.markdown(f"<style>{css_content}</style>", unsafe_allow_html=True)
|
330 |
-
|
331 |
-
except Exception as e:
|
332 |
-
logger.error(f"خطأ في تحميل ملف CSS: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/helpers/__init__.py
DELETED
@@ -1,70 +0,0 @@
|
|
1 |
-
#!/usr/bin/env python
|
2 |
-
# -*- coding: utf-8 -*-
|
3 |
-
|
4 |
-
"""حزمة المساعدات العامة المستخدمة في النظام"""
|
5 |
-
|
6 |
-
import streamlit as st
|
7 |
-
import pandas as pd
|
8 |
-
import numpy as np
|
9 |
-
import os
|
10 |
-
import sqlite3
|
11 |
-
|
12 |
-
from .utils import (
|
13 |
-
create_directory_if_not_exists,
|
14 |
-
get_data_folder,
|
15 |
-
format_time,
|
16 |
-
get_user_info,
|
17 |
-
load_css,
|
18 |
-
render_credits,
|
19 |
-
load_icons,
|
20 |
-
format_number,
|
21 |
-
format_currency,
|
22 |
-
styled_button,
|
23 |
-
filter_dataframe,
|
24 |
-
get_file_extension,
|
25 |
-
extract_numbers_from_text
|
26 |
-
)
|
27 |
-
|
28 |
-
def get_connection():
|
29 |
-
"""
|
30 |
-
إنشاء اتصال وهمي لقاعدة البيانات للاستخدام عند عدم توفر اتصال PostgreSQL
|
31 |
-
|
32 |
-
الإرجاع:
|
33 |
-
اتصال وهمي لقاعدة البيانات
|
34 |
-
"""
|
35 |
-
# إنشاء مجلد البيانات إذا لم يكن موجوداً
|
36 |
-
data_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'data')
|
37 |
-
os.makedirs(data_dir, exist_ok=True)
|
38 |
-
|
39 |
-
# إنشاء اتصال قاعدة بيانات SQLite محلية
|
40 |
-
db_path = os.path.join(data_dir, 'local_db.sqlite')
|
41 |
-
conn = sqlite3.connect(db_path)
|
42 |
-
|
43 |
-
# إعادة محاكاة سلوك اتصال PostgreSQL
|
44 |
-
conn.execute = conn.cursor().execute
|
45 |
-
|
46 |
-
# إضافة وظيفة وهمية للاقتطاع (commit) والإغلاق
|
47 |
-
original_close = conn.close
|
48 |
-
def enhanced_close():
|
49 |
-
conn.commit()
|
50 |
-
original_close()
|
51 |
-
conn.close = enhanced_close
|
52 |
-
|
53 |
-
return conn
|
54 |
-
|
55 |
-
__all__ = [
|
56 |
-
'create_directory_if_not_exists',
|
57 |
-
'get_data_folder',
|
58 |
-
'format_time',
|
59 |
-
'get_user_info',
|
60 |
-
'load_css',
|
61 |
-
'render_credits',
|
62 |
-
'load_icons',
|
63 |
-
'format_number',
|
64 |
-
'format_currency',
|
65 |
-
'styled_button',
|
66 |
-
'filter_dataframe',
|
67 |
-
'get_file_extension',
|
68 |
-
'extract_numbers_from_text',
|
69 |
-
'get_connection'
|
70 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/helpers/utils.py
DELETED
@@ -1,282 +0,0 @@
|
|
1 |
-
#!/usr/bin/env python
|
2 |
-
# -*- coding: utf-8 -*-
|
3 |
-
|
4 |
-
"""
|
5 |
-
وحدة المساعدات العامة
|
6 |
-
توفر هذه الوحدة مجموعة من الدوال المساعدة المستخدمة في مختلف أجزاء النظام
|
7 |
-
"""
|
8 |
-
|
9 |
-
import os
|
10 |
-
import datetime
|
11 |
-
import json
|
12 |
-
import re
|
13 |
-
import streamlit as st
|
14 |
-
|
15 |
-
def create_directory_if_not_exists(directory_path):
|
16 |
-
"""إنشاء مجلد إذا لم يكن موجوداً بالفعل"""
|
17 |
-
if not os.path.exists(directory_path):
|
18 |
-
os.makedirs(directory_path)
|
19 |
-
return True
|
20 |
-
return False
|
21 |
-
|
22 |
-
def get_data_folder():
|
23 |
-
"""الحصول على مسار مجلد البيانات"""
|
24 |
-
data_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'data'))
|
25 |
-
create_directory_if_not_exists(data_folder)
|
26 |
-
return data_folder
|
27 |
-
|
28 |
-
def format_time(timestamp=None):
|
29 |
-
"""تنسيق الوقت بصيغة قابلة للقراءة"""
|
30 |
-
if timestamp is None:
|
31 |
-
timestamp = datetime.datetime.now()
|
32 |
-
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
33 |
-
|
34 |
-
def get_user_info():
|
35 |
-
"""الحصول على معلومات المستخدم الحالي"""
|
36 |
-
# في الوقت الحالي، نستخدم معلومات مستخدم افتراضية
|
37 |
-
# يمكن تعديل هذه الدالة لاحقاً للتكامل مع نظام المصادقة
|
38 |
-
return {
|
39 |
-
"id": 1,
|
40 |
-
"username": "admin",
|
41 |
-
"name": "مدير النظام",
|
42 |
-
"role": "admin"
|
43 |
-
}
|
44 |
-
|
45 |
-
def load_css():
|
46 |
-
"""تحميل أنماط CSS المخصصة"""
|
47 |
-
st.markdown("""
|
48 |
-
<style>
|
49 |
-
.module-title {
|
50 |
-
color: #1E88E5;
|
51 |
-
text-align: center;
|
52 |
-
margin-bottom: 20px;
|
53 |
-
font-weight: bold;
|
54 |
-
}
|
55 |
-
|
56 |
-
.section-title {
|
57 |
-
color: #1565C0;
|
58 |
-
margin-top: 20px;
|
59 |
-
margin-bottom: 10px;
|
60 |
-
font-weight: bold;
|
61 |
-
}
|
62 |
-
|
63 |
-
.info-box {
|
64 |
-
background-color: #E3F2FD;
|
65 |
-
padding: 15px;
|
66 |
-
border-radius: 5px;
|
67 |
-
margin-bottom: 15px;
|
68 |
-
}
|
69 |
-
|
70 |
-
.warning-box {
|
71 |
-
background-color: #FFF8E1;
|
72 |
-
padding: 15px;
|
73 |
-
border-radius: 5px;
|
74 |
-
margin-bottom: 15px;
|
75 |
-
}
|
76 |
-
|
77 |
-
.error-box {
|
78 |
-
background-color: #FFEBEE;
|
79 |
-
padding: 15px;
|
80 |
-
border-radius: 5px;
|
81 |
-
margin-bottom: 15px;
|
82 |
-
}
|
83 |
-
|
84 |
-
.success-box {
|
85 |
-
background-color: #E8F5E9;
|
86 |
-
padding: 15px;
|
87 |
-
border-radius: 5px;
|
88 |
-
margin-bottom: 15px;
|
89 |
-
}
|
90 |
-
|
91 |
-
.centered {
|
92 |
-
text-align: center;
|
93 |
-
}
|
94 |
-
|
95 |
-
.footer {
|
96 |
-
text-align: center;
|
97 |
-
margin-top: 30px;
|
98 |
-
color: #78909C;
|
99 |
-
font-size: 0.8em;
|
100 |
-
}
|
101 |
-
|
102 |
-
/* تحسين تصميم الجداول */
|
103 |
-
table {
|
104 |
-
width: 100%;
|
105 |
-
}
|
106 |
-
|
107 |
-
thead th {
|
108 |
-
background-color: #1976D2 !important;
|
109 |
-
color: white !important;
|
110 |
-
}
|
111 |
-
|
112 |
-
tbody tr:nth-child(even) {
|
113 |
-
background-color: #f2f2f2;
|
114 |
-
}
|
115 |
-
|
116 |
-
tbody tr:hover {
|
117 |
-
background-color: #E3F2FD;
|
118 |
-
}
|
119 |
-
|
120 |
-
/* تحسين ظهور الفورم */
|
121 |
-
input, select, textarea {
|
122 |
-
border-radius: 5px !important;
|
123 |
-
border: 1px solid #ddd !important;
|
124 |
-
padding: 10px !important;
|
125 |
-
}
|
126 |
-
|
127 |
-
.stButton>button {
|
128 |
-
border-radius: 5px !important;
|
129 |
-
}
|
130 |
-
|
131 |
-
/* زيادة حجم الخط للتوافق مع اللغة العربية */
|
132 |
-
html, body, [class*="css"] {
|
133 |
-
font-family: 'Tajawal', sans-serif;
|
134 |
-
}
|
135 |
-
</style>
|
136 |
-
|
137 |
-
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;700&display=swap">
|
138 |
-
""", unsafe_allow_html=True)
|
139 |
-
|
140 |
-
def render_credits():
|
141 |
-
"""عرض المعلومات عن حقوق الملكية وإصدار النظام"""
|
142 |
-
st.markdown("""
|
143 |
-
<div class="footer">
|
144 |
-
<p>هذا النظام يعمل لصالح شركة شبه الجزيرة للمقاولات، جميع الحقوق محفوظة 2025</p>
|
145 |
-
<p>نظام WAHBi AI - الإصدار 2.0</p>
|
146 |
-
</div>
|
147 |
-
""", unsafe_allow_html=True)
|
148 |
-
|
149 |
-
def load_icons():
|
150 |
-
"""تحميل الأيقونات المستخدمة في النظام"""
|
151 |
-
icons = {
|
152 |
-
"project": "🏗️",
|
153 |
-
"document": "📄",
|
154 |
-
"analysis": "🔍",
|
155 |
-
"warning": "⚠️",
|
156 |
-
"success": "✅",
|
157 |
-
"error": "❌",
|
158 |
-
"info": "ℹ️",
|
159 |
-
"settings": "⚙️",
|
160 |
-
"user": "👤",
|
161 |
-
"money": "💰",
|
162 |
-
"time": "⏱️",
|
163 |
-
"location": "📍",
|
164 |
-
"notification": "🔔",
|
165 |
-
"edit": "✏️",
|
166 |
-
"delete": "🗑️",
|
167 |
-
"upload": "📤",
|
168 |
-
"download": "📥",
|
169 |
-
"save": "💾",
|
170 |
-
"cancel": "❌",
|
171 |
-
"add": "➕",
|
172 |
-
"calendar": "📅",
|
173 |
-
"chat": "💬",
|
174 |
-
"search": "🔎",
|
175 |
-
"star": "⭐",
|
176 |
-
"trophy": "🏆",
|
177 |
-
"medal": "🥇",
|
178 |
-
"chart": "📊",
|
179 |
-
"map": "🗺️",
|
180 |
-
"building": "🏢",
|
181 |
-
"road": "🛣️",
|
182 |
-
"bridge": "🌉",
|
183 |
-
}
|
184 |
-
return icons
|
185 |
-
|
186 |
-
def format_number(number, decimal_places=2):
|
187 |
-
"""تنسيق الأرقام بطريقة أنيقة"""
|
188 |
-
if isinstance(number, (int, float)):
|
189 |
-
if decimal_places == 0:
|
190 |
-
return "{:,.0f}".format(number)
|
191 |
-
else:
|
192 |
-
return "{:,.{dp}f}".format(number, dp=decimal_places)
|
193 |
-
return str(number)
|
194 |
-
|
195 |
-
def format_currency(amount, currency="ريال", decimal_places=2):
|
196 |
-
"""تنسيق المبالغ المالية"""
|
197 |
-
if amount is None:
|
198 |
-
return "غير محدد"
|
199 |
-
formatted = format_number(amount, decimal_places)
|
200 |
-
return f"{formatted} {currency}"
|
201 |
-
|
202 |
-
def styled_button(label, key=None, type="primary", on_click=None, args=None, full_width=False, icon=None, is_link=False, help=None):
|
203 |
-
"""
|
204 |
-
إنشاء زر بتنسيق معين
|
205 |
-
:param label: نص الزر
|
206 |
-
:param key: مفتاح الزر الفريد
|
207 |
-
:param type: نوع التنسيق ('primary', 'secondary', 'success', 'warning', 'danger', 'info', 'glass', 'flat')
|
208 |
-
:param on_click: الدالة التي سيتم تنفيذها عند النقر
|
209 |
-
:param args: معاملات الدالة
|
210 |
-
:param full_width: هل يأخذ الزر العرض كاملاً
|
211 |
-
:param icon: أيقونة لعرضها قبل النص (emoji أو HTML)
|
212 |
-
:param is_link: إذا كان الزر رابطاً بدلاً من زر عادي
|
213 |
-
:param help: نص المساعدة للزر
|
214 |
-
:return: زر مُنسّق
|
215 |
-
"""
|
216 |
-
if is_link:
|
217 |
-
btn_class = f"{type}-btn"
|
218 |
-
if icon:
|
219 |
-
btn_class += " action-btn"
|
220 |
-
label_with_icon = f"{icon} {label}"
|
221 |
-
else:
|
222 |
-
label_with_icon = label
|
223 |
-
|
224 |
-
button_html = f"""
|
225 |
-
<div class="{btn_class}">
|
226 |
-
{label_with_icon}
|
227 |
-
</div>
|
228 |
-
"""
|
229 |
-
return st.markdown(button_html, unsafe_allow_html=True)
|
230 |
-
else:
|
231 |
-
with st.container():
|
232 |
-
btn_class = f"{type}-btn"
|
233 |
-
if icon:
|
234 |
-
btn_class += " action-btn"
|
235 |
-
label_with_icon = f"{icon} {label}"
|
236 |
-
else:
|
237 |
-
label_with_icon = label
|
238 |
-
|
239 |
-
st.markdown(f'<div class="{btn_class}">', unsafe_allow_html=True)
|
240 |
-
clicked = st.button(label_with_icon, key=key, on_click=on_click, args=args, use_container_width=full_width, help=help)
|
241 |
-
st.markdown('</div>', unsafe_allow_html=True)
|
242 |
-
return clicked
|
243 |
-
|
244 |
-
def filter_dataframe(df, column, value):
|
245 |
-
"""ترشيح إطار البيانات"""
|
246 |
-
if value == "الكل":
|
247 |
-
return df
|
248 |
-
return df[df[column] == value]
|
249 |
-
|
250 |
-
def get_file_extension(filename):
|
251 |
-
"""استخراج امتداد الملف"""
|
252 |
-
if not filename:
|
253 |
-
return ""
|
254 |
-
return os.path.splitext(filename)[-1].lower()
|
255 |
-
|
256 |
-
def extract_numbers_from_text(text):
|
257 |
-
"""استخراج الأرقام من النص
|
258 |
-
|
259 |
-
Args:
|
260 |
-
text (str): النص المراد استخراج الأرقام منه
|
261 |
-
|
262 |
-
Returns:
|
263 |
-
list: قائمة بالأرقام المستخرجة
|
264 |
-
"""
|
265 |
-
if not text:
|
266 |
-
return []
|
267 |
-
|
268 |
-
# نمط للعثور على الأرقام (صحيحة أو عشرية) في النص
|
269 |
-
pattern = r'[-+]?\d*\.\d+|\d+'
|
270 |
-
|
271 |
-
# استخراج جميع الأرقام من النص
|
272 |
-
numbers = re.findall(pattern, text)
|
273 |
-
|
274 |
-
# تحويل النصوص المستخرجة إلى أرقام (صحيحة أو عشرية)
|
275 |
-
converted_numbers = []
|
276 |
-
for num in numbers:
|
277 |
-
if '.' in num:
|
278 |
-
converted_numbers.append(float(num))
|
279 |
-
else:
|
280 |
-
converted_numbers.append(int(num))
|
281 |
-
|
282 |
-
return converted_numbers
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/pdf_handler.py
DELETED
@@ -1,847 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
معالج ملفات PDF
|
3 |
-
"""
|
4 |
-
|
5 |
-
import os
|
6 |
-
import io
|
7 |
-
import re
|
8 |
-
import PyPDF2
|
9 |
-
import fitz # PyMuPDF
|
10 |
-
import pdfplumber
|
11 |
-
import numpy as np
|
12 |
-
from PIL import Image
|
13 |
-
import pytesseract
|
14 |
-
import pandas as pd
|
15 |
-
import traceback
|
16 |
-
from reportlab.lib.pagesizes import A4, landscape
|
17 |
-
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image as ReportLabImage
|
18 |
-
from reportlab.lib import colors
|
19 |
-
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
20 |
-
from reportlab.pdfbase import pdfmetrics
|
21 |
-
from reportlab.pdfbase.ttfonts import TTFont
|
22 |
-
import matplotlib.pyplot as plt
|
23 |
-
|
24 |
-
from utils.helpers import create_directory_if_not_exists, extract_numbers_from_text
|
25 |
-
|
26 |
-
# محاولة تسجيل الخطوط العربية إذا كانت متوفرة
|
27 |
-
try:
|
28 |
-
font_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "Amiri-Regular.ttf")
|
29 |
-
bold_font_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "Amiri-Bold.ttf")
|
30 |
-
|
31 |
-
if os.path.exists(font_path) and os.path.exists(bold_font_path):
|
32 |
-
pdfmetrics.registerFont(TTFont('Arabic', font_path))
|
33 |
-
pdfmetrics.registerFont(TTFont('ArabicBold', bold_font_path))
|
34 |
-
except Exception as e:
|
35 |
-
print(f"تعذر تحميل الخطوط العربية: {str(e)}. سيتم استخدام الخطوط الافتراضية.")
|
36 |
-
|
37 |
-
|
38 |
-
def extract_text_from_pdf(file_path, method='pymupdf'):
|
39 |
-
"""
|
40 |
-
استخراج النص من ملف PDF
|
41 |
-
|
42 |
-
المعلمات:
|
43 |
-
file_path: مسار ملف PDF
|
44 |
-
method: طريقة الاستخراج ('pymupdf', 'pypdf2', 'pdfplumber')
|
45 |
-
|
46 |
-
الإرجاع:
|
47 |
-
نص مستخرج من ملف PDF
|
48 |
-
"""
|
49 |
-
try:
|
50 |
-
# التحقق من وجود الملف
|
51 |
-
if not os.path.exists(file_path):
|
52 |
-
raise FileNotFoundError(f"الملف غير موجود: {file_path}")
|
53 |
-
|
54 |
-
# استخراج النص حسب الطريقة المطلوبة
|
55 |
-
if method.lower() == 'pymupdf':
|
56 |
-
return _extract_text_with_pymupdf(file_path)
|
57 |
-
elif method.lower() == 'pypdf2':
|
58 |
-
return _extract_text_with_pypdf2(file_path)
|
59 |
-
elif method.lower() == 'pdfplumber':
|
60 |
-
return _extract_text_with_pdfplumber(file_path)
|
61 |
-
else:
|
62 |
-
# استخدام PyMuPDF كطريقة افتراضية
|
63 |
-
return _extract_text_with_pymupdf(file_path)
|
64 |
-
|
65 |
-
except Exception as e:
|
66 |
-
error_msg = f"خطأ في استخراج النص من ملف PDF: {str(e)}"
|
67 |
-
print(error_msg)
|
68 |
-
traceback.print_exc()
|
69 |
-
raise Exception(error_msg)
|
70 |
-
|
71 |
-
|
72 |
-
def _extract_text_with_pymupdf(file_path):
|
73 |
-
"""استخراج النص باستخدام PyMuPDF"""
|
74 |
-
document = fitz.open(file_path)
|
75 |
-
text = ""
|
76 |
-
|
77 |
-
for page_number in range(len(document)):
|
78 |
-
page = document.load_page(page_number)
|
79 |
-
text += page.get_text("text") + "\n\n"
|
80 |
-
|
81 |
-
document.close()
|
82 |
-
return text
|
83 |
-
|
84 |
-
|
85 |
-
def _extract_text_with_pypdf2(file_path):
|
86 |
-
"""استخراج النص باستخدام PyPDF2"""
|
87 |
-
with open(file_path, 'rb') as file:
|
88 |
-
reader = PyPDF2.PdfReader(file)
|
89 |
-
text = ""
|
90 |
-
|
91 |
-
for page_number in range(len(reader.pages)):
|
92 |
-
page = reader.pages[page_number]
|
93 |
-
text += page.extract_text() + "\n\n"
|
94 |
-
|
95 |
-
return text
|
96 |
-
|
97 |
-
|
98 |
-
def _extract_text_with_pdfplumber(file_path):
|
99 |
-
"""استخراج النص باستخدام pdfplumber"""
|
100 |
-
with pdfplumber.open(file_path) as pdf:
|
101 |
-
text = ""
|
102 |
-
|
103 |
-
for page in pdf.pages:
|
104 |
-
text += page.extract_text() + "\n\n"
|
105 |
-
|
106 |
-
return text
|
107 |
-
|
108 |
-
|
109 |
-
def extract_tables_from_pdf(file_path, page_numbers=None):
|
110 |
-
"""
|
111 |
-
استخراج الجداول من ملف PDF
|
112 |
-
|
113 |
-
المعلمات:
|
114 |
-
file_path: مسار ملف PDF
|
115 |
-
page_numbers: قائمة بأرقام الصفحات للاستخراج منها (افتراضي: None لجميع الصفحات)
|
116 |
-
|
117 |
-
الإرجاع:
|
118 |
-
قائمة من DataFrames تمثل الجداول المستخرجة
|
119 |
-
"""
|
120 |
-
try:
|
121 |
-
# التحقق من وجود الملف
|
122 |
-
if not os.path.exists(file_path):
|
123 |
-
raise FileNotFoundError(f"الملف غير موجود: {file_path}")
|
124 |
-
|
125 |
-
# استخراج الجداول باستخدام pdfplumber
|
126 |
-
tables = []
|
127 |
-
|
128 |
-
with pdfplumber.open(file_path) as pdf:
|
129 |
-
# تحديد الصفحات المراد استخراج الجداول منها
|
130 |
-
if page_numbers is None:
|
131 |
-
pages_to_extract = range(len(pdf.pages))
|
132 |
-
else:
|
133 |
-
pages_to_extract = [p-1 for p in page_numbers if 1 <= p <= len(pdf.pages)]
|
134 |
-
|
135 |
-
# استخراج الجداول من كل صفحة
|
136 |
-
for page_idx in pages_to_extract:
|
137 |
-
page = pdf.pages[page_idx]
|
138 |
-
page_tables = page.extract_tables()
|
139 |
-
|
140 |
-
if page_tables:
|
141 |
-
for table in page_tables:
|
142 |
-
if table: # التحقق من أن الجدول ليس فارغًا
|
143 |
-
# تحويل الجدول إلى DataFrame
|
144 |
-
df = pd.DataFrame(table[1:], columns=table[0])
|
145 |
-
|
146 |
-
# تنظيف البيانات
|
147 |
-
df = df.applymap(lambda x: x.strip() if isinstance(x, str) else x)
|
148 |
-
|
149 |
-
# إضافة إلى قائمة الجداول
|
150 |
-
tables.append(df)
|
151 |
-
|
152 |
-
return tables
|
153 |
-
|
154 |
-
except Exception as e:
|
155 |
-
error_msg = f"خطأ في استخراج الجداول من ملف PDF: {str(e)}"
|
156 |
-
print(error_msg)
|
157 |
-
traceback.print_exc()
|
158 |
-
raise Exception(error_msg)
|
159 |
-
|
160 |
-
|
161 |
-
def extract_images_from_pdf(file_path, output_dir=None, prefix='image'):
|
162 |
-
"""
|
163 |
-
استخراج الصور من ملف PDF
|
164 |
-
|
165 |
-
المعلمات:
|
166 |
-
file_path: مسار ملف PDF
|
167 |
-
output_dir: دليل الإخراج (افتراضي: None للإرجاع كقائمة من الصور)
|
168 |
-
prefix: بادئة أسماء ملفات الصور
|
169 |
-
|
170 |
-
الإرجاع:
|
171 |
-
قائمة من مسارات الصور المستخرجة إذا تم تحديد دليل الإخراج، وإلا قائمة من الصور
|
172 |
-
"""
|
173 |
-
try:
|
174 |
-
# التحقق من وجود الملف
|
175 |
-
if not os.path.exists(file_path):
|
176 |
-
raise FileNotFoundError(f"الملف غير موجود: {file_path}")
|
177 |
-
|
178 |
-
# إنشاء دليل الإخراج إذا تم تحديده
|
179 |
-
if output_dir:
|
180 |
-
create_directory_if_not_exists(output_dir)
|
181 |
-
|
182 |
-
# استخراج الصور باستخدام PyMuPDF
|
183 |
-
document = fitz.open(file_path)
|
184 |
-
images = []
|
185 |
-
image_paths = []
|
186 |
-
|
187 |
-
for page_idx in range(len(document)):
|
188 |
-
page = document.load_page(page_idx)
|
189 |
-
|
190 |
-
# استخراج الصور من الصفحة
|
191 |
-
image_list = page.get_images(full=True)
|
192 |
-
|
193 |
-
for img_idx, img_info in enumerate(image_list):
|
194 |
-
xref = img_info[0]
|
195 |
-
base_image = document.extract_image(xref)
|
196 |
-
image_bytes = base_image["image"]
|
197 |
-
|
198 |
-
# إنشاء كائن الصورة
|
199 |
-
image = Image.open(io.BytesIO(image_bytes))
|
200 |
-
|
201 |
-
if output_dir:
|
202 |
-
# حفظ الصورة في الدليل المحدد
|
203 |
-
image_filename = f"{prefix}_{page_idx+1}_{img_idx+1}.{base_image['ext']}"
|
204 |
-
image_path = os.path.join(output_dir, image_filename)
|
205 |
-
image.save(image_path)
|
206 |
-
image_paths.append(image_path)
|
207 |
-
else:
|
208 |
-
# إضافة الصورة إلى القائمة
|
209 |
-
images.append(image)
|
210 |
-
|
211 |
-
document.close()
|
212 |
-
|
213 |
-
return image_paths if output_dir else images
|
214 |
-
|
215 |
-
except Exception as e:
|
216 |
-
error_msg = f"خطأ في استخراج الصور من ملف PDF: {str(e)}"
|
217 |
-
print(error_msg)
|
218 |
-
traceback.print_exc()
|
219 |
-
raise Exception(error_msg)
|
220 |
-
|
221 |
-
|
222 |
-
def extract_text_from_image(image, lang='ara+eng'):
|
223 |
-
"""
|
224 |
-
استخراج النص من صورة باستخدام OCR
|
225 |
-
|
226 |
-
المعلمات:
|
227 |
-
image: كائن الصورة أو مسار الصورة
|
228 |
-
lang: لغة النص (افتراضي: 'ara+eng' للعربية والإنجليزية)
|
229 |
-
|
230 |
-
الإرجاع:
|
231 |
-
النص المستخرج من الصورة
|
232 |
-
"""
|
233 |
-
try:
|
234 |
-
# إذا كان مسار صورة، قم بفتحها
|
235 |
-
if isinstance(image, str):
|
236 |
-
image = Image.open(image)
|
237 |
-
|
238 |
-
# استخراج النص باستخدام pytesseract
|
239 |
-
text = pytesseract.image_to_string(image, lang=lang)
|
240 |
-
|
241 |
-
return text
|
242 |
-
|
243 |
-
except Exception as e:
|
244 |
-
error_msg = f"خطأ في استخراج النص من الصورة: {str(e)}"
|
245 |
-
print(error_msg)
|
246 |
-
traceback.print_exc()
|
247 |
-
return ""
|
248 |
-
|
249 |
-
|
250 |
-
def ocr_pdf(file_path, lang='ara+eng'):
|
251 |
-
"""
|
252 |
-
تنفيذ OCR على ملف PDF
|
253 |
-
|
254 |
-
المعلمات:
|
255 |
-
file_path: مسار ملف PDF
|
256 |
-
lang: لغة النص (افتراضي: 'ara+eng' للعربية والإنجليزية)
|
257 |
-
|
258 |
-
الإرجاع:
|
259 |
-
النص المستخرج من ملف PDF
|
260 |
-
"""
|
261 |
-
try:
|
262 |
-
# التحقق من وجود الملف
|
263 |
-
if not os.path.exists(file_path):
|
264 |
-
raise FileNotFoundError(f"الملف غير موجود: {file_path}")
|
265 |
-
|
266 |
-
# فتح ملف PDF
|
267 |
-
document = fitz.open(file_path)
|
268 |
-
text = ""
|
269 |
-
|
270 |
-
for page_idx in range(len(document)):
|
271 |
-
page = document.load_page(page_idx)
|
272 |
-
|
273 |
-
# تحويل الصفحة إلى صورة
|
274 |
-
pix = page.get_pixmap(matrix=fitz.Matrix(300/72, 300/72))
|
275 |
-
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
276 |
-
|
277 |
-
# استخراج النص من الصورة
|
278 |
-
page_text = extract_text_from_image(img, lang=lang)
|
279 |
-
text += page_text + "\n\n"
|
280 |
-
|
281 |
-
document.close()
|
282 |
-
|
283 |
-
return text
|
284 |
-
|
285 |
-
except Exception as e:
|
286 |
-
error_msg = f"خطأ في تنفيذ OCR على ملف PDF: {str(e)}"
|
287 |
-
print(error_msg)
|
288 |
-
traceback.print_exc()
|
289 |
-
raise Exception(error_msg)
|
290 |
-
|
291 |
-
|
292 |
-
def search_in_pdf(file_path, search_text):
|
293 |
-
"""
|
294 |
-
البحث عن نص في ملف PDF
|
295 |
-
|
296 |
-
المعلمات:
|
297 |
-
file_path: مسار ملف PDF
|
298 |
-
search_text: النص المراد البحث عنه
|
299 |
-
|
300 |
-
الإرجاع:
|
301 |
-
قائمة من النتائج {page_number, text_snippet, matched_text}
|
302 |
-
"""
|
303 |
-
try:
|
304 |
-
# التحقق من وجود الملف
|
305 |
-
if not os.path.exists(file_path):
|
306 |
-
raise FileNotFoundError(f"الملف غير موجود: {file_path}")
|
307 |
-
|
308 |
-
# استخراج النص من ملف PDF
|
309 |
-
document = fitz.open(file_path)
|
310 |
-
results = []
|
311 |
-
|
312 |
-
for page_idx in range(len(document)):
|
313 |
-
page = document.load_page(page_idx)
|
314 |
-
page_text = page.get_text("text")
|
315 |
-
|
316 |
-
# البحث عن النص
|
317 |
-
if search_text.lower() in page_text.lower():
|
318 |
-
# استخراج المقتطفات التي تحتوي على النص المطلوب
|
319 |
-
lines = page_text.split('\n')
|
320 |
-
for line in lines:
|
321 |
-
if search_text.lower() in line.lower():
|
322 |
-
results.append({
|
323 |
-
'page_number': page_idx + 1,
|
324 |
-
'text_snippet': line,
|
325 |
-
'matched_text': search_text
|
326 |
-
})
|
327 |
-
|
328 |
-
document.close()
|
329 |
-
|
330 |
-
return results
|
331 |
-
|
332 |
-
except Exception as e:
|
333 |
-
error_msg = f"خطأ في البحث في ملف PDF: {str(e)}"
|
334 |
-
print(error_msg)
|
335 |
-
traceback.print_exc()
|
336 |
-
raise Exception(error_msg)
|
337 |
-
|
338 |
-
|
339 |
-
def extract_quantities_from_pdf(file_path):
|
340 |
-
"""
|
341 |
-
استخراج الكميات من ملف PDF
|
342 |
-
|
343 |
-
المعلمات:
|
344 |
-
file_path: مسار ملف PDF
|
345 |
-
|
346 |
-
الإرجاع:
|
347 |
-
DataFrame يحتوي على البنود والكميات المستخرجة
|
348 |
-
"""
|
349 |
-
try:
|
350 |
-
# استخراج النص والجداول
|
351 |
-
text = extract_text_from_pdf(file_path)
|
352 |
-
tables = extract_tables_from_pdf(file_path)
|
353 |
-
|
354 |
-
quantities = []
|
355 |
-
|
356 |
-
# استخراج الكميات من الجداول
|
357 |
-
for table in tables:
|
358 |
-
# البحث عن أعمدة تحتوي على "الكمية" أو "الوحدة" أو "البند"
|
359 |
-
quantity_cols = [col for col in table.columns if any(term in col.lower() for term in ['كمية', 'عدد', 'الكمية'])]
|
360 |
-
unit_cols = [col for col in table.columns if any(term in col.lower() for term in ['وحدة', 'الوحدة'])]
|
361 |
-
item_cols = [col for col in table.columns if any(term in col.lower() for term in ['بند', 'وصف', 'البند', 'العمل'])]
|
362 |
-
|
363 |
-
if quantity_cols and (unit_cols or item_cols):
|
364 |
-
quantity_col = quantity_cols[0]
|
365 |
-
unit_col = unit_cols[0] if unit_cols else None
|
366 |
-
item_col = item_cols[0] if item_cols else None
|
367 |
-
|
368 |
-
# استخراج الكميات
|
369 |
-
for _, row in table.iterrows():
|
370 |
-
if pd.notna(row[quantity_col]) and (item_col is None or pd.notna(row[item_col])):
|
371 |
-
quantity_value = extract_numbers_from_text(row[quantity_col])
|
372 |
-
quantity = quantity_value[0] if quantity_value else None
|
373 |
-
|
374 |
-
quantities.append({
|
375 |
-
'البند': row[item_col] if item_col else "غير محدد",
|
376 |
-
'الوحدة': row[unit_col] if unit_col else "غير محدد",
|
377 |
-
'الكمية': quantity
|
378 |
-
})
|
379 |
-
|
380 |
-
# استخراج الكميات من النص
|
381 |
-
lines = text.split('\n')
|
382 |
-
for line in lines:
|
383 |
-
# البحث عن الخطوط التي تحتوي على أرقام ووحدات قياس
|
384 |
-
if re.search(r'\d+(?:,\d+)*(?:\.\d+)?', line) and any(unit in line for unit in ['م2', 'م3', 'متر', 'طن', 'كجم', 'عدد']):
|
385 |
-
numbers = extract_numbers_from_text(line)
|
386 |
-
if numbers:
|
387 |
-
# استخراج وحدة القياس
|
388 |
-
unit_match = re.search(r'\b(م2|م3|متر مربع|متر مكعب|م\.ط|طن|كجم|عدد|قطعة)\b', line)
|
389 |
-
unit = unit_match.group(1) if unit_match else "غير محدد"
|
390 |
-
|
391 |
-
quantities.append({
|
392 |
-
'البند': line,
|
393 |
-
'الوحدة': unit,
|
394 |
-
'الكمية': numbers[0]
|
395 |
-
})
|
396 |
-
|
397 |
-
# إنشاء DataFrame
|
398 |
-
if quantities:
|
399 |
-
quantities_df = pd.DataFrame(quantities)
|
400 |
-
return quantities_df
|
401 |
-
else:
|
402 |
-
return pd.DataFrame(columns=['البند', 'الوحدة', 'الكمية'])
|
403 |
-
|
404 |
-
except Exception as e:
|
405 |
-
error_msg = f"خطأ في استخراج الكميات من ملف PDF: {str(e)}"
|
406 |
-
print(error_msg)
|
407 |
-
traceback.print_exc()
|
408 |
-
raise Exception(error_msg)
|
409 |
-
|
410 |
-
|
411 |
-
def merge_pdfs(input_paths, output_path):
|
412 |
-
"""
|
413 |
-
دمج ملفات PDF متعددة في ملف واحد
|
414 |
-
|
415 |
-
المعلمات:
|
416 |
-
input_paths: قائمة من مسارات ملفات PDF المراد دمجها
|
417 |
-
output_path: مسار ملف PDF الناتج
|
418 |
-
|
419 |
-
الإرجاع:
|
420 |
-
True في حالة النجاح
|
421 |
-
"""
|
422 |
-
try:
|
423 |
-
# التحقق من وجود الملفات
|
424 |
-
for file_path in input_paths:
|
425 |
-
if not os.path.exists(file_path):
|
426 |
-
raise FileNotFoundError(f"الملف غير موجود: {file_path}")
|
427 |
-
|
428 |
-
# إنشاء مجلد الإخراج إذا لم يكن موجودًا
|
429 |
-
output_dir = os.path.dirname(output_path)
|
430 |
-
create_directory_if_not_exists(output_dir)
|
431 |
-
|
432 |
-
# دمج ملفات PDF
|
433 |
-
merger = PyPDF2.PdfMerger()
|
434 |
-
|
435 |
-
for file_path in input_paths:
|
436 |
-
merger.append(file_path)
|
437 |
-
|
438 |
-
merger.write(output_path)
|
439 |
-
merger.close()
|
440 |
-
|
441 |
-
return True
|
442 |
-
|
443 |
-
except Exception as e:
|
444 |
-
error_msg = f"خطأ في دمج ملفات PDF: {str(e)}"
|
445 |
-
print(error_msg)
|
446 |
-
traceback.print_exc()
|
447 |
-
raise Exception(error_msg)
|
448 |
-
|
449 |
-
|
450 |
-
def split_pdf(input_path, output_dir, prefix='page'):
|
451 |
-
"""
|
452 |
-
تقسيم ملف PDF إلى ملفات منفصلة لكل صفحة
|
453 |
-
|
454 |
-
المعلمات:
|
455 |
-
input_path: مسار ملف PDF المراد تقسيمه
|
456 |
-
output_dir: دليل الإخراج
|
457 |
-
prefix: بادئة أسماء ملفات الإخراج
|
458 |
-
|
459 |
-
الإرجاع:
|
460 |
-
قائمة من مسارات ملفات PDF الناتجة
|
461 |
-
"""
|
462 |
-
try:
|
463 |
-
# التحقق من وجود الملف
|
464 |
-
if not os.path.exists(input_path):
|
465 |
-
raise FileNotFoundError(f"الملف غير موجود: {input_path}")
|
466 |
-
|
467 |
-
# إنشاء دليل الإخراج
|
468 |
-
create_directory_if_not_exists(output_dir)
|
469 |
-
|
470 |
-
# قراءة ملف PDF
|
471 |
-
with open(input_path, 'rb') as file:
|
472 |
-
reader = PyPDF2.PdfReader(file)
|
473 |
-
output_files = []
|
474 |
-
|
475 |
-
# تقسيم كل صفحة إلى ملف منفصل
|
476 |
-
for page_idx in range(len(reader.pages)):
|
477 |
-
writer = PyPDF2.PdfWriter()
|
478 |
-
writer.add_page(reader.pages[page_idx])
|
479 |
-
|
480 |
-
output_filename = f"{prefix}_{page_idx+1}.pdf"
|
481 |
-
output_path = os.path.join(output_dir, output_filename)
|
482 |
-
|
483 |
-
with open(output_path, 'wb') as output_file:
|
484 |
-
writer.write(output_file)
|
485 |
-
|
486 |
-
output_files.append(output_path)
|
487 |
-
|
488 |
-
return output_files
|
489 |
-
|
490 |
-
except Exception as e:
|
491 |
-
error_msg = f"خطأ في تقسيم ملف PDF: {str(e)}"
|
492 |
-
print(error_msg)
|
493 |
-
traceback.print_exc()
|
494 |
-
raise Exception(error_msg)
|
495 |
-
|
496 |
-
|
497 |
-
def export_pricing_to_pdf(data, output_path, title="تحليل الأسعار", description=""):
|
498 |
-
"""
|
499 |
-
تصدير بيانات التسعير إلى ملف PDF
|
500 |
-
|
501 |
-
المعلمات:
|
502 |
-
data: DataFrame يحتوي على بيانات التسعير
|
503 |
-
output_path: مسار ملف PDF الناتج
|
504 |
-
title: عنوان التقرير
|
505 |
-
description: وصف إضافي للتقرير
|
506 |
-
|
507 |
-
الإرجاع:
|
508 |
-
مسار ملف PDF الناتج
|
509 |
-
"""
|
510 |
-
try:
|
511 |
-
# التحقق من أن البيانات ليست فارغة
|
512 |
-
if data is None or len(data) == 0:
|
513 |
-
raise ValueError("البيانات فارغة")
|
514 |
-
|
515 |
-
# إنشاء دليل الإخراج إذا لم يكن موجودًا
|
516 |
-
output_dir = os.path.dirname(output_path)
|
517 |
-
create_directory_if_not_exists(output_dir)
|
518 |
-
|
519 |
-
# إعداد أنماط النصوص
|
520 |
-
styles = getSampleStyleSheet()
|
521 |
-
|
522 |
-
# محاولة استخدام الخطوط العربية إذا كانت متوفرة
|
523 |
-
try:
|
524 |
-
# تسجيل الخطوط العربية إذا لم تكن مسجلة
|
525 |
-
if 'Arabic' not in pdfmetrics.getRegisteredFontNames():
|
526 |
-
pdfmetrics.registerFont(TTFont('Arabic', 'utils/fonts/Amiri-Regular.ttf'))
|
527 |
-
|
528 |
-
if 'ArabicBold' not in pdfmetrics.getRegisteredFontNames():
|
529 |
-
pdfmetrics.registerFont(TTFont('ArabicBold', 'utils/fonts/Amiri-Bold.ttf'))
|
530 |
-
|
531 |
-
# إنشاء أنماط للنصوص العربية
|
532 |
-
title_style = ParagraphStyle(
|
533 |
-
name='ArabicTitleStyle',
|
534 |
-
fontName='ArabicBold',
|
535 |
-
fontSize=16,
|
536 |
-
alignment=1, # وسط
|
537 |
-
leading=18
|
538 |
-
)
|
539 |
-
|
540 |
-
normal_style = ParagraphStyle(
|
541 |
-
name='ArabicStyle',
|
542 |
-
fontName='Arabic',
|
543 |
-
fontSize=12,
|
544 |
-
alignment=1, # وسط
|
545 |
-
leading=14
|
546 |
-
)
|
547 |
-
|
548 |
-
except:
|
549 |
-
# استخدام الخطوط الافتراضية إذا تعذر استخدام الخطوط العربية
|
550 |
-
title_style = styles['Title']
|
551 |
-
normal_style = styles['Normal']
|
552 |
-
|
553 |
-
# إنشاء وثيقة PDF
|
554 |
-
doc = SimpleDocTemplate(
|
555 |
-
output_path,
|
556 |
-
pagesize=landscape(A4),
|
557 |
-
rightMargin=30,
|
558 |
-
leftMargin=30,
|
559 |
-
topMargin=30,
|
560 |
-
bottomMargin=30
|
561 |
-
)
|
562 |
-
|
563 |
-
# قائمة العناصر في الوثيقة
|
564 |
-
elements = []
|
565 |
-
|
566 |
-
# إضافة العنوان
|
567 |
-
elements.append(Paragraph(title, title_style))
|
568 |
-
elements.append(Spacer(1, 20))
|
569 |
-
|
570 |
-
# إضافة الوصف إذا كان موجودًا
|
571 |
-
if description:
|
572 |
-
elements.append(Paragraph(description, normal_style))
|
573 |
-
elements.append(Spacer(1, 20))
|
574 |
-
|
575 |
-
# تحويل DataFrame إلى قائمة لاستخدامها في الجدول
|
576 |
-
data_list = [data.columns.tolist()] + data.values.tolist()
|
577 |
-
|
578 |
-
# إنشاء الجدول
|
579 |
-
table = Table(data_list, repeatRows=1)
|
580 |
-
|
581 |
-
# إضافة تنسيق للجدول
|
582 |
-
table_style = TableStyle([
|
583 |
-
('BACKGROUND', (0, 0), (-1, 0), colors.lightblue),
|
584 |
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
|
585 |
-
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
586 |
-
('FONTNAME', (0, 0), (-1, 0), 'ArabicBold' if 'ArabicBold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'),
|
587 |
-
('FONTSIZE', (0, 0), (-1, 0), 12),
|
588 |
-
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
589 |
-
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
590 |
-
('GRID', (0, 0), (-1, -1), 1, colors.black),
|
591 |
-
('FONTNAME', (0, 1), (-1, -1), 'Arabic' if 'Arabic' in pdfmetrics.getRegisteredFontNames() else 'Helvetica'),
|
592 |
-
('FONTSIZE', (0, 1), (-1, -1), 10),
|
593 |
-
])
|
594 |
-
|
595 |
-
# إضافة تلوين للصفوف المتناوبة
|
596 |
-
for i in range(1, len(data_list)):
|
597 |
-
if i % 2 == 0:
|
598 |
-
table_style.add('BACKGROUND', (0, i), (-1, i), colors.lightgrey)
|
599 |
-
|
600 |
-
table.setStyle(table_style)
|
601 |
-
elements.append(table)
|
602 |
-
|
603 |
-
# إضافة ملخص إحصائي إذا كانت البيانات تحتوي على أعمدة رقمية
|
604 |
-
numeric_columns = data.select_dtypes(include=['number']).columns
|
605 |
-
if len(numeric_columns) > 0:
|
606 |
-
elements.append(Spacer(1, 30))
|
607 |
-
elements.append(Paragraph("ملخص إحصائي", title_style))
|
608 |
-
elements.append(Spacer(1, 10))
|
609 |
-
|
610 |
-
# إنشاء ملخص إحصائي
|
611 |
-
summary = data[numeric_columns].describe().reset_index()
|
612 |
-
summary_list = [['الإحصاءات'] + list(numeric_columns)] + summary.values.tolist()
|
613 |
-
|
614 |
-
# إنشاء جدول الملخص
|
615 |
-
summary_table = Table(summary_list, repeatRows=1)
|
616 |
-
summary_table.setStyle(TableStyle([
|
617 |
-
('BACKGROUND', (0, 0), (-1, 0), colors.lightblue),
|
618 |
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
|
619 |
-
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
620 |
-
('FONTNAME', (0, 0), (-1, 0), 'ArabicBold' if 'ArabicBold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'),
|
621 |
-
('FONTSIZE', (0, 0), (-1, 0), 12),
|
622 |
-
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
623 |
-
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
624 |
-
('GRID', (0, 0), (-1, -1), 1, colors.black),
|
625 |
-
('FONTNAME', (0, 1), (-1, -1), 'Arabic' if 'Arabic' in pdfmetrics.getRegisteredFontNames() else 'Helvetica'),
|
626 |
-
('FONTSIZE', (0, 1), (-1, -1), 10),
|
627 |
-
]))
|
628 |
-
elements.append(summary_table)
|
629 |
-
|
630 |
-
# إنشاء مخططات للأعمدة الرقمية الرئيسية
|
631 |
-
if len(numeric_columns) > 0 and len(data) > 1:
|
632 |
-
for col in numeric_columns[:2]: # أخذ أول عمودين رقميين فقط
|
633 |
-
plt.figure(figsize=(8, 4))
|
634 |
-
plt.barh(range(len(data)), data[col])
|
635 |
-
plt.yticks(range(len(data)), data.iloc[:, 0] if len(data.columns) > 0 else range(len(data)))
|
636 |
-
plt.xlabel(col)
|
637 |
-
plt.title(f"مخطط {col}")
|
638 |
-
plt.tight_layout()
|
639 |
-
|
640 |
-
# حفظ المخطط كصورة في الذاكرة
|
641 |
-
img_data = io.BytesIO()
|
642 |
-
plt.savefig(img_data, format='png')
|
643 |
-
img_data.seek(0)
|
644 |
-
plt.close()
|
645 |
-
|
646 |
-
# إضافة المخطط إلى الوثيقة
|
647 |
-
elements.append(Spacer(1, 20))
|
648 |
-
img = ReportLabImage(img_data, width=500, height=250)
|
649 |
-
elements.append(img)
|
650 |
-
|
651 |
-
# بناء الوثيقة
|
652 |
-
doc.build(elements)
|
653 |
-
|
654 |
-
return output_path
|
655 |
-
|
656 |
-
except Exception as e:
|
657 |
-
error_msg = f"خطأ في تصدير بيانات التسعير إلى PDF: {str(e)}"
|
658 |
-
print(error_msg)
|
659 |
-
traceback.print_exc()
|
660 |
-
raise Exception(error_msg)
|
661 |
-
|
662 |
-
|
663 |
-
def export_pricing_with_analysis_to_pdf(items_data, analysis_data, output_path, title="تحليل الأسعار مع تحليل المكونات", project_info=None):
|
664 |
-
"""
|
665 |
-
تصدير بيانات التسعير مع تحليل المكونات إلى ملف PDF
|
666 |
-
|
667 |
-
المعلمات:
|
668 |
-
items_data: DataFrame يحتوي على بيانات البنود
|
669 |
-
analysis_data: قاموس يحتوي على تحليل أسعار البنود {item_id: DataFrame}
|
670 |
-
output_path: مسار ملف PDF الناتج
|
671 |
-
title: عنوان التقرير
|
672 |
-
project_info: معلومات المشروع (قاموس)
|
673 |
-
|
674 |
-
الإرجاع:
|
675 |
-
مسار ملف PDF الناتج
|
676 |
-
"""
|
677 |
-
try:
|
678 |
-
# التحقق من أن البيانات ليست فارغة
|
679 |
-
if items_data is None or len(items_data) == 0:
|
680 |
-
raise ValueError("بيانات البنود فارغة")
|
681 |
-
|
682 |
-
# إنشاء دليل الإخراج إذا لم يكن موجودًا
|
683 |
-
output_dir = os.path.dirname(output_path)
|
684 |
-
create_directory_if_not_exists(output_dir)
|
685 |
-
|
686 |
-
# إعداد أنماط النصوص
|
687 |
-
styles = getSampleStyleSheet()
|
688 |
-
|
689 |
-
# محاولة استخدام الخطوط العربية إذا كانت متوفرة
|
690 |
-
try:
|
691 |
-
# تسجيل الخطوط العربية إذا لم تكن مسجلة
|
692 |
-
if 'Arabic' not in pdfmetrics.getRegisteredFontNames():
|
693 |
-
pdfmetrics.registerFont(TTFont('Arabic', 'utils/fonts/Amiri-Regular.ttf'))
|
694 |
-
|
695 |
-
if 'ArabicBold' not in pdfmetrics.getRegisteredFontNames():
|
696 |
-
pdfmetrics.registerFont(TTFont('ArabicBold', 'utils/fonts/Amiri-Bold.ttf'))
|
697 |
-
|
698 |
-
# إنشاء أنماط للنصوص العربية
|
699 |
-
title_style = ParagraphStyle(
|
700 |
-
name='ArabicTitleStyle',
|
701 |
-
fontName='ArabicBold',
|
702 |
-
fontSize=16,
|
703 |
-
alignment=1, # وسط
|
704 |
-
leading=18
|
705 |
-
)
|
706 |
-
|
707 |
-
normal_style = ParagraphStyle(
|
708 |
-
name='ArabicStyle',
|
709 |
-
fontName='Arabic',
|
710 |
-
fontSize=12,
|
711 |
-
alignment=1, # وسط
|
712 |
-
leading=14
|
713 |
-
)
|
714 |
-
|
715 |
-
subtitle_style = ParagraphStyle(
|
716 |
-
name='ArabicSubtitleStyle',
|
717 |
-
fontName='ArabicBold',
|
718 |
-
fontSize=14,
|
719 |
-
alignment=1, # وسط
|
720 |
-
leading=16
|
721 |
-
)
|
722 |
-
|
723 |
-
except:
|
724 |
-
# استخدام الخطوط الافتراضية إذا تعذر استخدام الخطوط العربية
|
725 |
-
title_style = styles['Title']
|
726 |
-
normal_style = styles['Normal']
|
727 |
-
subtitle_style = styles['Heading2']
|
728 |
-
|
729 |
-
# إنشاء وثيقة PDF
|
730 |
-
doc = SimpleDocTemplate(
|
731 |
-
output_path,
|
732 |
-
pagesize=landscape(A4),
|
733 |
-
rightMargin=30,
|
734 |
-
leftMargin=30,
|
735 |
-
topMargin=30,
|
736 |
-
bottomMargin=30
|
737 |
-
)
|
738 |
-
|
739 |
-
# قائمة العناصر في الوثيقة
|
740 |
-
elements = []
|
741 |
-
|
742 |
-
# إضافة العنوان
|
743 |
-
elements.append(Paragraph(title, title_style))
|
744 |
-
elements.append(Spacer(1, 20))
|
745 |
-
|
746 |
-
# إضافة معلومات المشروع إذا كانت موجودة
|
747 |
-
if project_info:
|
748 |
-
project_info_text = f"اسم المشروع: {project_info.get('اسم_المشروع', '')} | "
|
749 |
-
project_info_text += f"وصف المشروع: {project_info.get('وصف_المشروع', '')}"
|
750 |
-
elements.append(Paragraph(project_info_text, normal_style))
|
751 |
-
elements.append(Spacer(1, 20))
|
752 |
-
|
753 |
-
# إضافة جدول البنود
|
754 |
-
elements.append(Paragraph("جدول البنود", subtitle_style))
|
755 |
-
elements.append(Spacer(1, 10))
|
756 |
-
|
757 |
-
# تحويل DataFrame إلى قائمة لاستخدامها في الجدول
|
758 |
-
items_list = [items_data.columns.tolist()] + items_data.values.tolist()
|
759 |
-
|
760 |
-
# إنشاء الجدول
|
761 |
-
items_table = Table(items_list, repeatRows=1)
|
762 |
-
|
763 |
-
# إضافة تنسيق للجدول
|
764 |
-
items_table_style = TableStyle([
|
765 |
-
('BACKGROUND', (0, 0), (-1, 0), colors.lightblue),
|
766 |
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
|
767 |
-
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
768 |
-
('FONTNAME', (0, 0), (-1, 0), 'ArabicBold' if 'ArabicBold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'),
|
769 |
-
('FONTSIZE', (0, 0), (-1, 0), 12),
|
770 |
-
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
771 |
-
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
772 |
-
('GRID', (0, 0), (-1, -1), 1, colors.black),
|
773 |
-
('FONTNAME', (0, 1), (-1, -1), 'Arabic' if 'Arabic' in pdfmetrics.getRegisteredFontNames() else 'Helvetica'),
|
774 |
-
('FONTSIZE', (0, 1), (-1, -1), 10),
|
775 |
-
])
|
776 |
-
|
777 |
-
# إضافة تلوين للصفوف المتناوبة
|
778 |
-
for i in range(1, len(items_list)):
|
779 |
-
if i % 2 == 0:
|
780 |
-
items_table_style.add('BACKGROUND', (0, i), (-1, i), colors.lightgrey)
|
781 |
-
|
782 |
-
items_table.setStyle(items_table_style)
|
783 |
-
elements.append(items_table)
|
784 |
-
|
785 |
-
# إضافة تحليل الأسعار لكل بند
|
786 |
-
if analysis_data:
|
787 |
-
elements.append(Spacer(1, 30))
|
788 |
-
elements.append(Paragraph("تحليل مكونات الأسعار", subtitle_style))
|
789 |
-
|
790 |
-
for item_id, analysis_df in analysis_data.items():
|
791 |
-
# الحصول على معلومات البند
|
792 |
-
item_info = items_data[items_data['رقم البند'] == item_id].iloc[0] if 'رقم البند' in items_data.columns and len(items_data[items_data['رقم البند'] == item_id]) > 0 else None
|
793 |
-
|
794 |
-
if item_info is not None:
|
795 |
-
item_title = f"تحليل مكونات البند: {item_id} - {item_info.get('وصف البند', '')}"
|
796 |
-
else:
|
797 |
-
item_title = f"تحليل مكونات البند: {item_id}"
|
798 |
-
|
799 |
-
elements.append(Spacer(1, 20))
|
800 |
-
elements.append(Paragraph(item_title, subtitle_style))
|
801 |
-
elements.append(Spacer(1, 10))
|
802 |
-
|
803 |
-
# تحويل DataFrame إلى قائمة لاستخدامها في الجدول
|
804 |
-
analysis_list = [analysis_df.columns.tolist()] + analysis_df.values.tolist()
|
805 |
-
|
806 |
-
# إنشاء الجدول
|
807 |
-
analysis_table = Table(analysis_list, repeatRows=1)
|
808 |
-
|
809 |
-
# إضافة تنسيق للجدول
|
810 |
-
analysis_table_style = TableStyle([
|
811 |
-
('BACKGROUND', (0, 0), (-1, 0), colors.lightblue),
|
812 |
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
|
813 |
-
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
814 |
-
('FONTNAME', (0, 0), (-1, 0), 'ArabicBold' if 'ArabicBold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'),
|
815 |
-
('FONTSIZE', (0, 0), (-1, 0), 12),
|
816 |
-
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
817 |
-
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
818 |
-
('GRID', (0, 0), (-1, -1), 1, colors.black),
|
819 |
-
('FONTNAME', (0, 1), (-1, -1), 'Arabic' if 'Arabic' in pdfmetrics.getRegisteredFontNames() else 'Helvetica'),
|
820 |
-
('FONTSIZE', (0, 1), (-1, -1), 10),
|
821 |
-
])
|
822 |
-
|
823 |
-
# إضافة تلوين للصفوف المتناوبة
|
824 |
-
for i in range(1, len(analysis_list)):
|
825 |
-
if i % 2 == 0:
|
826 |
-
analysis_table_style.add('BACKGROUND', (0, i), (-1, i), colors.lightgrey)
|
827 |
-
|
828 |
-
analysis_table.setStyle(analysis_table_style)
|
829 |
-
elements.append(analysis_table)
|
830 |
-
|
831 |
-
# حساب وعرض إجمالي تحليل السعر
|
832 |
-
if 'الإجمالي' in analysis_df.columns and len(analysis_df) > 0:
|
833 |
-
total_analysis_price = analysis_df['الإجمالي'].sum()
|
834 |
-
total_text = f"إجمالي تكلفة البند من التحليل: {total_analysis_price:,.2f} ريال"
|
835 |
-
elements.append(Spacer(1, 10))
|
836 |
-
elements.append(Paragraph(total_text, normal_style))
|
837 |
-
|
838 |
-
# بناء الوثيقة
|
839 |
-
doc.build(elements)
|
840 |
-
|
841 |
-
return output_path
|
842 |
-
|
843 |
-
except Exception as e:
|
844 |
-
error_msg = f"خطأ في تصدير تحليل الأسعار إلى PDF: {str(e)}"
|
845 |
-
print(error_msg)
|
846 |
-
traceback.print_exc()
|
847 |
-
raise Exception(error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/pdf_handler.py.bak
DELETED
@@ -1,476 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
معالج ملفات PDF
|
3 |
-
"""
|
4 |
-
|
5 |
-
import os
|
6 |
-
import io
|
7 |
-
import re
|
8 |
-
import PyPDF2
|
9 |
-
import fitz # PyMuPDF
|
10 |
-
import pdfplumber
|
11 |
-
import numpy as np
|
12 |
-
from PIL import Image
|
13 |
-
import pytesseract
|
14 |
-
import pandas as pd
|
15 |
-
import traceback
|
16 |
-
|
17 |
-
from utils.helpers import create_directory_if_not_exists, extract_numbers_from_text
|
18 |
-
|
19 |
-
|
20 |
-
def extract_text_from_pdf(file_path, method='pymupdf'):
|
21 |
-
"""
|
22 |
-
استخراج النص من ملف PDF
|
23 |
-
|
24 |
-
المعلمات:
|
25 |
-
file_path: مسار ملف PDF
|
26 |
-
method: طريقة الاستخراج ('pymupdf', 'pypdf2', 'pdfplumber')
|
27 |
-
|
28 |
-
الإرجاع:
|
29 |
-
نص مستخرج من ملف PDF
|
30 |
-
"""
|
31 |
-
try:
|
32 |
-
# التحقق من وجود الملف
|
33 |
-
if not os.path.exists(file_path):
|
34 |
-
raise FileNotFoundError(f"الملف غير موجود: {file_path}")
|
35 |
-
|
36 |
-
# استخراج النص حسب الطريقة المطلوبة
|
37 |
-
if method.lower() == 'pymupdf':
|
38 |
-
return _extract_text_with_pymupdf(file_path)
|
39 |
-
elif method.lower() == 'pypdf2':
|
40 |
-
return _extract_text_with_pypdf2(file_path)
|
41 |
-
elif method.lower() == 'pdfplumber':
|
42 |
-
return _extract_text_with_pdfplumber(file_path)
|
43 |
-
else:
|
44 |
-
# استخدام PyMuPDF كطريقة افتراضية
|
45 |
-
return _extract_text_with_pymupdf(file_path)
|
46 |
-
|
47 |
-
except Exception as e:
|
48 |
-
error_msg = f"خطأ في استخراج النص من ملف PDF: {str(e)}"
|
49 |
-
print(error_msg)
|
50 |
-
traceback.print_exc()
|
51 |
-
raise Exception(error_msg)
|
52 |
-
|
53 |
-
|
54 |
-
def _extract_text_with_pymupdf(file_path):
|
55 |
-
"""استخراج النص باستخدام PyMuPDF"""
|
56 |
-
document = fitz.open(file_path)
|
57 |
-
text = ""
|
58 |
-
|
59 |
-
for page_number in range(len(document)):
|
60 |
-
page = document.load_page(page_number)
|
61 |
-
text += page.get_text("text") + "\n\n"
|
62 |
-
|
63 |
-
document.close()
|
64 |
-
return text
|
65 |
-
|
66 |
-
|
67 |
-
def _extract_text_with_pypdf2(file_path):
|
68 |
-
"""استخراج النص باستخدام PyPDF2"""
|
69 |
-
with open(file_path, 'rb') as file:
|
70 |
-
reader = PyPDF2.PdfReader(file)
|
71 |
-
text = ""
|
72 |
-
|
73 |
-
for page_number in range(len(reader.pages)):
|
74 |
-
page = reader.pages[page_number]
|
75 |
-
text += page.extract_text() + "\n\n"
|
76 |
-
|
77 |
-
return text
|
78 |
-
|
79 |
-
|
80 |
-
def _extract_text_with_pdfplumber(file_path):
|
81 |
-
"""استخراج النص باستخدام pdfplumber"""
|
82 |
-
with pdfplumber.open(file_path) as pdf:
|
83 |
-
text = ""
|
84 |
-
|
85 |
-
for page in pdf.pages:
|
86 |
-
text += page.extract_text() + "\n\n"
|
87 |
-
|
88 |
-
return text
|
89 |
-
|
90 |
-
|
91 |
-
def extract_tables_from_pdf(file_path, page_numbers=None):
|
92 |
-
"""
|
93 |
-
استخراج الجداول من ملف PDF
|
94 |
-
|
95 |
-
المعلمات:
|
96 |
-
file_path: مسار ملف PDF
|
97 |
-
page_numbers: قائمة بأرقام الصفحات للاستخراج منها (افتراضي: None لجميع الصفحات)
|
98 |
-
|
99 |
-
الإرجاع:
|
100 |
-
قائمة من DataFrames تمثل الجداول المستخرجة
|
101 |
-
"""
|
102 |
-
try:
|
103 |
-
# التحقق من وجود الملف
|
104 |
-
if not os.path.exists(file_path):
|
105 |
-
raise FileNotFoundError(f"الملف غير موجود: {file_path}")
|
106 |
-
|
107 |
-
# استخراج الجداول باستخدام pdfplumber
|
108 |
-
tables = []
|
109 |
-
|
110 |
-
with pdfplumber.open(file_path) as pdf:
|
111 |
-
# تحديد الصفحات المراد استخراج الجداول منها
|
112 |
-
if page_numbers is None:
|
113 |
-
pages_to_extract = range(len(pdf.pages))
|
114 |
-
else:
|
115 |
-
pages_to_extract = [p-1 for p in page_numbers if 1 <= p <= len(pdf.pages)]
|
116 |
-
|
117 |
-
# استخراج الجداول من كل صفحة
|
118 |
-
for page_idx in pages_to_extract:
|
119 |
-
page = pdf.pages[page_idx]
|
120 |
-
page_tables = page.extract_tables()
|
121 |
-
|
122 |
-
if page_tables:
|
123 |
-
for table in page_tables:
|
124 |
-
if table: # التحقق من أن الجدول ليس فارغًا
|
125 |
-
# تحويل الجدول إلى DataFrame
|
126 |
-
df = pd.DataFrame(table[1:], columns=table[0])
|
127 |
-
|
128 |
-
# تنظيف البيانات
|
129 |
-
df = df.applymap(lambda x: x.strip() if isinstance(x, str) else x)
|
130 |
-
|
131 |
-
# إضافة إلى قائمة الجداول
|
132 |
-
tables.append(df)
|
133 |
-
|
134 |
-
return tables
|
135 |
-
|
136 |
-
except Exception as e:
|
137 |
-
error_msg = f"خطأ في استخراج الجداول من ملف PDF: {str(e)}"
|
138 |
-
print(error_msg)
|
139 |
-
traceback.print_exc()
|
140 |
-
raise Exception(error_msg)
|
141 |
-
|
142 |
-
|
143 |
-
def extract_images_from_pdf(file_path, output_dir=None, prefix='image'):
|
144 |
-
"""
|
145 |
-
استخراج الصور من ملف PDF
|
146 |
-
|
147 |
-
المعلمات:
|
148 |
-
file_path: مسار ملف PDF
|
149 |
-
output_dir: دليل الإخراج (افتراضي: None للإرجاع كقائمة من الصور)
|
150 |
-
prefix: بادئة أسماء ملفات الصور
|
151 |
-
|
152 |
-
الإرجاع:
|
153 |
-
قائمة من مسارات الصور المستخرجة إذا تم تحديد دليل الإخراج، وإلا قائمة من الصور
|
154 |
-
"""
|
155 |
-
try:
|
156 |
-
# التحقق من وجود الملف
|
157 |
-
if not os.path.exists(file_path):
|
158 |
-
raise FileNotFoundError(f"الملف غير موجود: {file_path}")
|
159 |
-
|
160 |
-
# إنشاء دليل الإخراج إذا تم تحديده
|
161 |
-
if output_dir:
|
162 |
-
create_directory_if_not_exists(output_dir)
|
163 |
-
|
164 |
-
# استخراج الصور باستخدام PyMuPDF
|
165 |
-
document = fitz.open(file_path)
|
166 |
-
images = []
|
167 |
-
image_paths = []
|
168 |
-
|
169 |
-
for page_idx in range(len(document)):
|
170 |
-
page = document.load_page(page_idx)
|
171 |
-
|
172 |
-
# استخراج الصور من الصفحة
|
173 |
-
image_list = page.get_images(full=True)
|
174 |
-
|
175 |
-
for img_idx, img_info in enumerate(image_list):
|
176 |
-
xref = img_info[0]
|
177 |
-
base_image = document.extract_image(xref)
|
178 |
-
image_bytes = base_image["image"]
|
179 |
-
|
180 |
-
# إنشاء كائن الصورة
|
181 |
-
image = Image.open(io.BytesIO(image_bytes))
|
182 |
-
|
183 |
-
if output_dir:
|
184 |
-
# حفظ الصورة في الدليل المحدد
|
185 |
-
image_filename = f"{prefix}_{page_idx+1}_{img_idx+1}.{base_image['ext']}"
|
186 |
-
image_path = os.path.join(output_dir, image_filename)
|
187 |
-
image.save(image_path)
|
188 |
-
image_paths.append(image_path)
|
189 |
-
else:
|
190 |
-
# إضافة الصورة إلى القائمة
|
191 |
-
images.append(image)
|
192 |
-
|
193 |
-
document.close()
|
194 |
-
|
195 |
-
return image_paths if output_dir else images
|
196 |
-
|
197 |
-
except Exception as e:
|
198 |
-
error_msg = f"خطأ في استخراج الصور من ملف PDF: {str(e)}"
|
199 |
-
print(error_msg)
|
200 |
-
traceback.print_exc()
|
201 |
-
raise Exception(error_msg)
|
202 |
-
|
203 |
-
|
204 |
-
def extract_text_from_image(image, lang='ara+eng'):
|
205 |
-
"""
|
206 |
-
استخراج النص من صورة باستخدام OCR
|
207 |
-
|
208 |
-
المعلمات:
|
209 |
-
image: كائن الصورة أو مسار الصورة
|
210 |
-
lang: لغة النص (افتراضي: 'ara+eng' للعربية والإنجليزية)
|
211 |
-
|
212 |
-
الإرجاع:
|
213 |
-
النص المستخرج من الصورة
|
214 |
-
"""
|
215 |
-
try:
|
216 |
-
# إذا كان مسار صورة، قم بفتحها
|
217 |
-
if isinstance(image, str):
|
218 |
-
image = Image.open(image)
|
219 |
-
|
220 |
-
# استخراج النص باستخدام pytesseract
|
221 |
-
text = pytesseract.image_to_string(image, lang=lang)
|
222 |
-
|
223 |
-
return text
|
224 |
-
|
225 |
-
except Exception as e:
|
226 |
-
error_msg = f"خطأ في استخراج النص من الصورة: {str(e)}"
|
227 |
-
print(error_msg)
|
228 |
-
traceback.print_exc()
|
229 |
-
return ""
|
230 |
-
|
231 |
-
|
232 |
-
def ocr_pdf(file_path, lang='ara+eng'):
|
233 |
-
"""
|
234 |
-
تنفيذ OCR على ملف PDF
|
235 |
-
|
236 |
-
المعلمات:
|
237 |
-
file_path: مسار ملف PDF
|
238 |
-
lang: لغة النص (افتراضي: 'ara+eng' للعربية والإنجليزية)
|
239 |
-
|
240 |
-
الإرجاع:
|
241 |
-
النص المستخرج من ملف PDF
|
242 |
-
"""
|
243 |
-
try:
|
244 |
-
# التحقق من وجود الملف
|
245 |
-
if not os.path.exists(file_path):
|
246 |
-
raise FileNotFoundError(f"الملف غير موجود: {file_path}")
|
247 |
-
|
248 |
-
# فتح ملف PDF
|
249 |
-
document = fitz.open(file_path)
|
250 |
-
text = ""
|
251 |
-
|
252 |
-
for page_idx in range(len(document)):
|
253 |
-
page = document.load_page(page_idx)
|
254 |
-
|
255 |
-
# تحويل الصفحة إلى صورة
|
256 |
-
pix = page.get_pixmap(matrix=fitz.Matrix(300/72, 300/72))
|
257 |
-
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
258 |
-
|
259 |
-
# استخراج النص من الصورة
|
260 |
-
page_text = extract_text_from_image(img, lang=lang)
|
261 |
-
text += page_text + "\n\n"
|
262 |
-
|
263 |
-
document.close()
|
264 |
-
|
265 |
-
return text
|
266 |
-
|
267 |
-
except Exception as e:
|
268 |
-
error_msg = f"خطأ في تنفيذ OCR على ملف PDF: {str(e)}"
|
269 |
-
print(error_msg)
|
270 |
-
traceback.print_exc()
|
271 |
-
raise Exception(error_msg)
|
272 |
-
|
273 |
-
|
274 |
-
def search_in_pdf(file_path, search_text):
|
275 |
-
"""
|
276 |
-
البحث عن نص في ملف PDF
|
277 |
-
|
278 |
-
المعلمات:
|
279 |
-
file_path: مسار ملف PDF
|
280 |
-
search_text: النص المراد البحث عنه
|
281 |
-
|
282 |
-
الإرجاع:
|
283 |
-
قائمة من النتائج {page_number, text_snippet, matched_text}
|
284 |
-
"""
|
285 |
-
try:
|
286 |
-
# التحقق من وجود الملف
|
287 |
-
if not os.path.exists(file_path):
|
288 |
-
raise FileNotFoundError(f"الملف غير موجود: {file_path}")
|
289 |
-
|
290 |
-
# استخراج النص من ملف PDF
|
291 |
-
document = fitz.open(file_path)
|
292 |
-
results = []
|
293 |
-
|
294 |
-
for page_idx in range(len(document)):
|
295 |
-
page = document.load_page(page_idx)
|
296 |
-
page_text = page.get_text("text")
|
297 |
-
|
298 |
-
# البحث عن النص
|
299 |
-
if search_text.lower() in page_text.lower():
|
300 |
-
# استخراج المقتطفات التي تحتوي على النص المطلوب
|
301 |
-
lines = page_text.split('\n')
|
302 |
-
for line in lines:
|
303 |
-
if search_text.lower() in line.lower():
|
304 |
-
results.append({
|
305 |
-
'page_number': page_idx + 1,
|
306 |
-
'text_snippet': line,
|
307 |
-
'matched_text': search_text
|
308 |
-
})
|
309 |
-
|
310 |
-
document.close()
|
311 |
-
|
312 |
-
return results
|
313 |
-
|
314 |
-
except Exception as e:
|
315 |
-
error_msg = f"خطأ في البحث في ملف PDF: {str(e)}"
|
316 |
-
print(error_msg)
|
317 |
-
traceback.print_exc()
|
318 |
-
raise Exception(error_msg)
|
319 |
-
|
320 |
-
|
321 |
-
def extract_quantities_from_pdf(file_path):
|
322 |
-
"""
|
323 |
-
استخراج الكميات من ملف PDF
|
324 |
-
|
325 |
-
المعلمات:
|
326 |
-
file_path: مسار ملف PDF
|
327 |
-
|
328 |
-
الإرجاع:
|
329 |
-
DataFrame يحتوي على البنود والكميات المستخرجة
|
330 |
-
"""
|
331 |
-
try:
|
332 |
-
# استخراج النص والجداول
|
333 |
-
text = extract_text_from_pdf(file_path)
|
334 |
-
tables = extract_tables_from_pdf(file_path)
|
335 |
-
|
336 |
-
quantities = []
|
337 |
-
|
338 |
-
# استخراج الكميات من الجداول
|
339 |
-
for table in tables:
|
340 |
-
# البحث عن أعمدة تحتوي على "الكمية" أو "الوحدة" أو "البند"
|
341 |
-
quantity_cols = [col for col in table.columns if any(term in col.lower() for term in ['كمية', 'عدد', 'الكمية'])]
|
342 |
-
unit_cols = [col for col in table.columns if any(term in col.lower() for term in ['وحدة', 'الوحدة'])]
|
343 |
-
item_cols = [col for col in table.columns if any(term in col.lower() for term in ['بند', 'وصف', 'البند', 'العمل'])]
|
344 |
-
|
345 |
-
if quantity_cols and (unit_cols or item_cols):
|
346 |
-
quantity_col = quantity_cols[0]
|
347 |
-
unit_col = unit_cols[0] if unit_cols else None
|
348 |
-
item_col = item_cols[0] if item_cols else None
|
349 |
-
|
350 |
-
# استخراج الكميات
|
351 |
-
for _, row in table.iterrows():
|
352 |
-
if pd.notna(row[quantity_col]) and (item_col is None or pd.notna(row[item_col])):
|
353 |
-
quantity_value = extract_numbers_from_text(row[quantity_col])
|
354 |
-
quantity = quantity_value[0] if quantity_value else None
|
355 |
-
|
356 |
-
quantities.append({
|
357 |
-
'البند': row[item_col] if item_col else "غير محدد",
|
358 |
-
'الوحدة': row[unit_col] if unit_col else "غير محدد",
|
359 |
-
'الكمية': quantity
|
360 |
-
})
|
361 |
-
|
362 |
-
# استخراج الكميات من النص
|
363 |
-
lines = text.split('\n')
|
364 |
-
for line in lines:
|
365 |
-
# البحث عن الخطوط التي تحتوي على أرقام ووحدات قياس
|
366 |
-
if re.search(r'\d+(?:,\d+)*(?:\.\d+)?', line) and any(unit in line for unit in ['م2', 'م3', 'متر', 'طن', 'كجم', 'عدد']):
|
367 |
-
numbers = extract_numbers_from_text(line)
|
368 |
-
if numbers:
|
369 |
-
# استخراج وحدة القياس
|
370 |
-
unit_match = re.search(r'\b(م2|م3|متر مربع|متر مكعب|م\.ط|طن|كجم|عدد|قطعة)\b', line)
|
371 |
-
unit = unit_match.group(1) if unit_match else "غير محدد"
|
372 |
-
|
373 |
-
quantities.append({
|
374 |
-
'البند': line,
|
375 |
-
'الوحدة': unit,
|
376 |
-
'الكمية': numbers[0]
|
377 |
-
})
|
378 |
-
|
379 |
-
# إنشاء DataFrame
|
380 |
-
if quantities:
|
381 |
-
quantities_df = pd.DataFrame(quantities)
|
382 |
-
return quantities_df
|
383 |
-
else:
|
384 |
-
return pd.DataFrame(columns=['البند', 'الوحدة', 'الكمية'])
|
385 |
-
|
386 |
-
except Exception as e:
|
387 |
-
error_msg = f"خطأ في استخراج الكميات من ملف PDF: {str(e)}"
|
388 |
-
print(error_msg)
|
389 |
-
traceback.print_exc()
|
390 |
-
raise Exception(error_msg)
|
391 |
-
|
392 |
-
|
393 |
-
def merge_pdfs(input_paths, output_path):
|
394 |
-
"""
|
395 |
-
دمج ملفات PDF متعددة في ملف واحد
|
396 |
-
|
397 |
-
المعلمات:
|
398 |
-
input_paths: قائمة من مسارات ملفات PDF المراد دمجها
|
399 |
-
output_path: مسار ملف PDF الناتج
|
400 |
-
|
401 |
-
الإرجاع:
|
402 |
-
True في حالة النجاح
|
403 |
-
"""
|
404 |
-
try:
|
405 |
-
# التحقق من وجود الملفات
|
406 |
-
for file_path in input_paths:
|
407 |
-
if not os.path.exists(file_path):
|
408 |
-
raise FileNotFoundError(f"الملف غير موجود: {file_path}")
|
409 |
-
|
410 |
-
# إنشاء مجلد الإخراج إذا لم يكن موجودًا
|
411 |
-
output_dir = os.path.dirname(output_path)
|
412 |
-
create_directory_if_not_exists(output_dir)
|
413 |
-
|
414 |
-
# دمج ملفات PDF
|
415 |
-
merger = PyPDF2.PdfMerger()
|
416 |
-
|
417 |
-
for file_path in input_paths:
|
418 |
-
merger.append(file_path)
|
419 |
-
|
420 |
-
merger.write(output_path)
|
421 |
-
merger.close()
|
422 |
-
|
423 |
-
return True
|
424 |
-
|
425 |
-
except Exception as e:
|
426 |
-
error_msg = f"خطأ في دمج ملفات PDF: {str(e)}"
|
427 |
-
print(error_msg)
|
428 |
-
traceback.print_exc()
|
429 |
-
raise Exception(error_msg)
|
430 |
-
|
431 |
-
|
432 |
-
def split_pdf(input_path, output_dir, prefix='page'):
|
433 |
-
"""
|
434 |
-
تقسيم ملف PDF إلى ملفات منفصلة لكل صفحة
|
435 |
-
|
436 |
-
المعلمات:
|
437 |
-
input_path: مسار ملف PDF المراد تقسيمه
|
438 |
-
output_dir: دليل الإخراج
|
439 |
-
prefix: بادئة أسماء ملفات الإخراج
|
440 |
-
|
441 |
-
الإرجاع:
|
442 |
-
قائمة من مسارات ملفات PDF الناتجة
|
443 |
-
"""
|
444 |
-
try:
|
445 |
-
# التحقق من وجود الملف
|
446 |
-
if not os.path.exists(input_path):
|
447 |
-
raise FileNotFoundError(f"الملف غير موجود: {input_path}")
|
448 |
-
|
449 |
-
# إنشاء دليل الإخراج
|
450 |
-
create_directory_if_not_exists(output_dir)
|
451 |
-
|
452 |
-
# قراءة ملف PDF
|
453 |
-
with open(input_path, 'rb') as file:
|
454 |
-
reader = PyPDF2.PdfReader(file)
|
455 |
-
output_files = []
|
456 |
-
|
457 |
-
# تقسيم كل صفحة إلى ملف منفصل
|
458 |
-
for page_idx in range(len(reader.pages)):
|
459 |
-
writer = PyPDF2.PdfWriter()
|
460 |
-
writer.add_page(reader.pages[page_idx])
|
461 |
-
|
462 |
-
output_filename = f"{prefix}_{page_idx+1}.pdf"
|
463 |
-
output_path = os.path.join(output_dir, output_filename)
|
464 |
-
|
465 |
-
with open(output_path, 'wb') as output_file:
|
466 |
-
writer.write(output_file)
|
467 |
-
|
468 |
-
output_files.append(output_path)
|
469 |
-
|
470 |
-
return output_files
|
471 |
-
|
472 |
-
except Exception as e:
|
473 |
-
error_msg = f"خطأ في تقسيم ملف PDF: {str(e)}"
|
474 |
-
print(error_msg)
|
475 |
-
traceback.print_exc()
|
476 |
-
raise Exception(error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/session_state.py
DELETED
@@ -1,247 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
وحدة إدارة حالة الجلسة
|
3 |
-
"""
|
4 |
-
|
5 |
-
import streamlit as st
|
6 |
-
from datetime import datetime
|
7 |
-
import pandas as pd
|
8 |
-
import config
|
9 |
-
|
10 |
-
|
11 |
-
def initialize_session_state():
|
12 |
-
"""
|
13 |
-
تهيئة متغيرات حالة الجلسة
|
14 |
-
"""
|
15 |
-
# المتغيرات الرئيسية لحالة المستخدم
|
16 |
-
if 'is_authenticated' not in st.session_state:
|
17 |
-
st.session_state.is_authenticated = False
|
18 |
-
|
19 |
-
if 'user_info' not in st.session_state:
|
20 |
-
st.session_state.user_info = None
|
21 |
-
|
22 |
-
# المتغيرات المتعلقة بالمناقصات والمشاريع
|
23 |
-
if 'current_project' not in st.session_state:
|
24 |
-
st.session_state.current_project = None
|
25 |
-
|
26 |
-
if 'current_pricing' not in st.session_state:
|
27 |
-
st.session_state.current_pricing = None
|
28 |
-
|
29 |
-
if 'pricing_history' not in st.session_state:
|
30 |
-
st.session_state.pricing_history = []
|
31 |
-
|
32 |
-
# المتغيرات المتعلقة بالمحتوى المحلي
|
33 |
-
if 'local_content_products' not in st.session_state:
|
34 |
-
st.session_state.local_content_products = pd.DataFrame({
|
35 |
-
'المنتج': [],
|
36 |
-
'الكمية': [],
|
37 |
-
'سعر_الوحدة': [],
|
38 |
-
'التكلفة_الإجمالية': [],
|
39 |
-
'نسبة_المحتوى_المحلي': []
|
40 |
-
})
|
41 |
-
|
42 |
-
if 'local_content_services' not in st.session_state:
|
43 |
-
st.session_state.local_content_services = pd.DataFrame({
|
44 |
-
'الخدمة': [],
|
45 |
-
'التكلفة': [],
|
46 |
-
'نسبة_المحتوى_المحلي': []
|
47 |
-
})
|
48 |
-
|
49 |
-
if 'local_content_labor' not in st.session_state:
|
50 |
-
st.session_state.local_content_labor = pd.DataFrame({
|
51 |
-
'فئة_العمالة': [],
|
52 |
-
'العدد': [],
|
53 |
-
'الراتب_الشهري': [],
|
54 |
-
'المدة_بالأشهر': [],
|
55 |
-
'نسبة_المحتوى_المحلي': []
|
56 |
-
})
|
57 |
-
|
58 |
-
# المتغيرات المتعلقة بتحليل المستندات
|
59 |
-
if 'current_document' not in st.session_state:
|
60 |
-
st.session_state.current_document = None
|
61 |
-
|
62 |
-
if 'analyzed_documents' not in st.session_state:
|
63 |
-
st.session_state.analyzed_documents = []
|
64 |
-
|
65 |
-
# المتغيرات المتعلقة بالتسعير
|
66 |
-
if 'manual_items' not in st.session_state:
|
67 |
-
st.session_state.manual_items = pd.DataFrame({
|
68 |
-
'رقم البند': [],
|
69 |
-
'وصف البند': [],
|
70 |
-
'الوحدة': [],
|
71 |
-
'الكمية': [],
|
72 |
-
'سعر الوحدة': [],
|
73 |
-
'الإجمالي': []
|
74 |
-
})
|
75 |
-
|
76 |
-
# المتغيرات المتعلقة بالمصادر
|
77 |
-
if 'resources' not in st.session_state:
|
78 |
-
st.session_state.resources = []
|
79 |
-
|
80 |
-
# المتغيرات المتعلقة بالإعدادات
|
81 |
-
if 'settings' not in st.session_state:
|
82 |
-
st.session_state.settings = {
|
83 |
-
'ui_theme': config.UI_THEME,
|
84 |
-
'locale': config.LOCALE,
|
85 |
-
'enable_animations': config.ENABLE_ANIMATIONS
|
86 |
-
}
|
87 |
-
|
88 |
-
|
89 |
-
def save_current_pricing():
|
90 |
-
"""
|
91 |
-
حفظ التسعير الحالي في سجل التسعير
|
92 |
-
|
93 |
-
الإرجاع:
|
94 |
-
True في حالة النجاح، False في حالة الفشل
|
95 |
-
"""
|
96 |
-
try:
|
97 |
-
if st.session_state.current_pricing:
|
98 |
-
# إضافة معلومات إضافية
|
99 |
-
pricing_entry = st.session_state.current_pricing.copy()
|
100 |
-
pricing_entry['timestamp'] = datetime.now()
|
101 |
-
|
102 |
-
# إضافة إلى سجل التسعير
|
103 |
-
st.session_state.pricing_history.append(pricing_entry)
|
104 |
-
|
105 |
-
return True
|
106 |
-
return False
|
107 |
-
except Exception as e:
|
108 |
-
print(f"خطأ في حفظ التسعير الحالي: {str(e)}")
|
109 |
-
return False
|
110 |
-
|
111 |
-
|
112 |
-
def set_current_project(project_data):
|
113 |
-
"""
|
114 |
-
تعيين المشروع الحالي
|
115 |
-
|
116 |
-
المعلمات:
|
117 |
-
project_data: بيانات المشروع
|
118 |
-
"""
|
119 |
-
st.session_state.current_project = project_data
|
120 |
-
|
121 |
-
|
122 |
-
def set_current_pricing(pricing_data):
|
123 |
-
"""
|
124 |
-
تعيين التسعير الحالي
|
125 |
-
|
126 |
-
المعلمات:
|
127 |
-
pricing_data: بيانات التسعير
|
128 |
-
"""
|
129 |
-
st.session_state.current_pricing = pricing_data
|
130 |
-
|
131 |
-
|
132 |
-
def set_current_document(document_data):
|
133 |
-
"""
|
134 |
-
تعيين المستند الحالي
|
135 |
-
|
136 |
-
المعلمات:
|
137 |
-
document_data: بيانات المستند
|
138 |
-
"""
|
139 |
-
st.session_state.current_document = document_data
|
140 |
-
|
141 |
-
|
142 |
-
def clear_session():
|
143 |
-
"""
|
144 |
-
مسح بيانات الجلسة الحالية
|
145 |
-
"""
|
146 |
-
# الاحتفاظ بحالة المصادقة والمستخدم
|
147 |
-
is_authenticated = st.session_state.is_authenticated
|
148 |
-
user_info = st.session_state.user_info
|
149 |
-
settings = st.session_state.settings
|
150 |
-
|
151 |
-
# م��ح المتغيرات
|
152 |
-
st.session_state.clear()
|
153 |
-
|
154 |
-
# إعادة تعيين حالة المصادقة والمستخدم
|
155 |
-
st.session_state.is_authenticated = is_authenticated
|
156 |
-
st.session_state.user_info = user_info
|
157 |
-
st.session_state.settings = settings
|
158 |
-
|
159 |
-
# إعادة تهيئة متغيرات الجلسة
|
160 |
-
initialize_session_state()
|
161 |
-
|
162 |
-
|
163 |
-
def export_session_state():
|
164 |
-
"""
|
165 |
-
تصدير حالة الجلسة الحالية
|
166 |
-
|
167 |
-
الإرجاع:
|
168 |
-
قاموس يحتوي على حالة الجلسة
|
169 |
-
"""
|
170 |
-
# إنشاء نسخة من حالة الجلسة
|
171 |
-
session_data = {}
|
172 |
-
|
173 |
-
# تخزين البيانات الرئيسية
|
174 |
-
if st.session_state.current_project:
|
175 |
-
session_data['current_project'] = st.session_state.current_project
|
176 |
-
|
177 |
-
if st.session_state.current_pricing:
|
178 |
-
session_data['current_pricing'] = st.session_state.current_pricing
|
179 |
-
|
180 |
-
if st.session_state.pricing_history:
|
181 |
-
session_data['pricing_history'] = st.session_state.pricing_history
|
182 |
-
|
183 |
-
# تحويل DataFrames إلى قوائم من القواميس
|
184 |
-
if 'local_content_products' in st.session_state and not st.session_state.local_content_products.empty:
|
185 |
-
session_data['local_content_products'] = st.session_state.local_content_products.to_dict('records')
|
186 |
-
|
187 |
-
if 'local_content_services' in st.session_state and not st.session_state.local_content_services.empty:
|
188 |
-
session_data['local_content_services'] = st.session_state.local_content_services.to_dict('records')
|
189 |
-
|
190 |
-
if 'local_content_labor' in st.session_state and not st.session_state.local_content_labor.empty:
|
191 |
-
session_data['local_content_labor'] = st.session_state.local_content_labor.to_dict('records')
|
192 |
-
|
193 |
-
# تخزين البيانات الأخرى
|
194 |
-
if 'manual_items' in st.session_state and not st.session_state.manual_items.empty:
|
195 |
-
session_data['manual_items'] = st.session_state.manual_items.to_dict('records')
|
196 |
-
|
197 |
-
if st.session_state.resources:
|
198 |
-
session_data['resources'] = st.session_state.resources
|
199 |
-
|
200 |
-
# إضافة بيانات الوقت
|
201 |
-
session_data['exported_at'] = datetime.now().isoformat()
|
202 |
-
|
203 |
-
return session_data
|
204 |
-
|
205 |
-
|
206 |
-
def import_session_state(session_data):
|
207 |
-
"""
|
208 |
-
استيراد حالة الجلسة
|
209 |
-
|
210 |
-
المعلمات:
|
211 |
-
session_data: قاموس يحتوي على حالة الجلسة
|
212 |
-
|
213 |
-
الإرجاع:
|
214 |
-
True في حالة النجاح، False في حالة الفشل
|
215 |
-
"""
|
216 |
-
try:
|
217 |
-
# استيراد البيانات الرئيسية
|
218 |
-
if 'current_project' in session_data:
|
219 |
-
st.session_state.current_project = session_data['current_project']
|
220 |
-
|
221 |
-
if 'current_pricing' in session_data:
|
222 |
-
st.session_state.current_pricing = session_data['current_pricing']
|
223 |
-
|
224 |
-
if 'pricing_history' in session_data:
|
225 |
-
st.session_state.pricing_history = session_data['pricing_history']
|
226 |
-
|
227 |
-
# استيراد DataFrames
|
228 |
-
if 'local_content_products' in session_data:
|
229 |
-
st.session_state.local_content_products = pd.DataFrame(session_data['local_content_products'])
|
230 |
-
|
231 |
-
if 'local_content_services' in session_data:
|
232 |
-
st.session_state.local_content_services = pd.DataFrame(session_data['local_content_services'])
|
233 |
-
|
234 |
-
if 'local_content_labor' in session_data:
|
235 |
-
st.session_state.local_content_labor = pd.DataFrame(session_data['local_content_labor'])
|
236 |
-
|
237 |
-
# استيراد البيانات الأخرى
|
238 |
-
if 'manual_items' in session_data:
|
239 |
-
st.session_state.manual_items = pd.DataFrame(session_data['manual_items'])
|
240 |
-
|
241 |
-
if 'resources' in session_data:
|
242 |
-
st.session_state.resources = session_data['resources']
|
243 |
-
|
244 |
-
return True
|
245 |
-
except Exception as e:
|
246 |
-
print(f"خطأ في استيراد حالة الجلسة: {str(e)}")
|
247 |
-
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|