Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- __pycache__/search_engine.cpython-311.pyc +0 -0
- app/__pycache__/ai_features.cpython-311.pyc +0 -0
- app/__pycache__/api.cpython-311.pyc +0 -0
- app/__pycache__/config.cpython-311.pyc +0 -0
- app/__pycache__/email_utils.cpython-311.pyc +0 -0
- app/__pycache__/page_features.cpython-311.pyc +0 -0
- app/__pycache__/xero_routes.cpython-311.pyc +0 -0
- app/__pycache__/xero_utils.cpython-311.pyc +0 -0
- app/ai_features.py +24 -1
- app/api.py +425 -391
- app/email_utils.py +103 -0
- app/page_features.py +523 -4
- app/templates/base.html +89 -9
- app/templates/edit_inventory.html +86 -0
- app/xero_routes.py +52 -5
- app/xero_utils.py +4 -1
- env_test.py +197 -0
- flask_session/aa71dde20eaf768ca7e5f90a25563ea6 +0 -0
- search_engine.py +2 -2
__pycache__/search_engine.cpython-311.pyc
CHANGED
Binary files a/__pycache__/search_engine.cpython-311.pyc and b/__pycache__/search_engine.cpython-311.pyc differ
|
|
app/__pycache__/ai_features.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/ai_features.cpython-311.pyc and b/app/__pycache__/ai_features.cpython-311.pyc differ
|
|
app/__pycache__/api.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/api.cpython-311.pyc and b/app/__pycache__/api.cpython-311.pyc differ
|
|
app/__pycache__/config.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/config.cpython-311.pyc and b/app/__pycache__/config.cpython-311.pyc differ
|
|
app/__pycache__/email_utils.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/email_utils.cpython-311.pyc and b/app/__pycache__/email_utils.cpython-311.pyc differ
|
|
app/__pycache__/page_features.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/page_features.cpython-311.pyc and b/app/__pycache__/page_features.cpython-311.pyc differ
|
|
app/__pycache__/xero_routes.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/xero_routes.cpython-311.pyc and b/app/__pycache__/xero_routes.cpython-311.pyc differ
|
|
app/__pycache__/xero_utils.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/xero_utils.cpython-311.pyc and b/app/__pycache__/xero_utils.cpython-311.pyc differ
|
|
app/ai_features.py
CHANGED
@@ -15,6 +15,7 @@ import os
|
|
15 |
import json
|
16 |
import traceback
|
17 |
from .xero_utils import trigger_po_creation,trigger_contact_creation
|
|
|
18 |
# +++ START: WHATSAPP FEATURE IMPORTS +++
|
19 |
import requests
|
20 |
from twilio.twiml.messaging_response import MessagingResponse
|
@@ -598,6 +599,7 @@ def whatsapp_reply():
|
|
598 |
final_response_text = error_msg
|
599 |
else: # User was found successfully
|
600 |
user_email = user['email']
|
|
|
601 |
user_name = user.get('contactPerson', 'the customer')
|
602 |
|
603 |
if function_call.name == 'create_direct_order':
|
@@ -620,6 +622,15 @@ def whatsapp_reply():
|
|
620 |
"order_id": str(order_id), "user_email": user_email, "items": validated_items,
|
621 |
"delivery_address": user.get('businessAddress'), "mobile_number": user.get('phoneNumber'),"deliverydate": args.get('delivery_date')
|
622 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
623 |
trigger_po_creation(order_details_for_xero)
|
624 |
final_response_text = f"Thank you! I have directly placed order #{next_serial} for *{user_name}* for delivery on {args.get('delivery_date')}. They can view details at https://matax-express.vercel.app/."
|
625 |
|
@@ -629,7 +640,10 @@ def whatsapp_reply():
|
|
629 |
p_doc = mongo.db.products.find_one({'name': {'$regex': f'^{item.get("product_name")}$', '$options': 'i'}})
|
630 |
if p_doc:
|
631 |
db_items.append({"productId": str(p_doc['_id']), "quantity": item.get('quantity'), "mode": item.get('unit')})
|
632 |
-
|
|
|
|
|
|
|
633 |
else: added_messages.append(f"could not find '{item.get('product_name')}'")
|
634 |
if db_items: mongo.db.carts.update_one({'user_email': user_email}, {'$push': {'items': {'$each': db_items}}, '$set': {'updated_at': datetime.utcnow()}}, upsert=True)
|
635 |
final_response_text = f"OK, I've updated the cart for *{user_name}*: I added {', '.join(added_messages)}."
|
@@ -648,6 +662,15 @@ def whatsapp_reply():
|
|
648 |
"delivery_address": user.get('businessAddress'), "mobile_number": user.get('phoneNumber'),"deliverydate": args.get('delivery_date')
|
649 |
}
|
650 |
trigger_po_creation(order_details_for_xero)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
651 |
mongo.db.carts.delete_one({'user_email': user_email}) # Clear cart
|
652 |
final_response_text = f"Thank you! Order `#{next_serial}` has been placed for *{user_name}* for delivery on {args.get('delivery_date')}. They can view details at https://matax-express.vercel.app/."
|
653 |
|
|
|
15 |
import json
|
16 |
import traceback
|
17 |
from .xero_utils import trigger_po_creation,trigger_contact_creation
|
18 |
+
from .email_utils import send_order_confirmation_email
|
19 |
# +++ START: WHATSAPP FEATURE IMPORTS +++
|
20 |
import requests
|
21 |
from twilio.twiml.messaging_response import MessagingResponse
|
|
|
599 |
final_response_text = error_msg
|
600 |
else: # User was found successfully
|
601 |
user_email = user['email']
|
602 |
+
user_id = mongo.db.users.find_one({'email': user_email})
|
603 |
user_name = user.get('contactPerson', 'the customer')
|
604 |
|
605 |
if function_call.name == 'create_direct_order':
|
|
|
622 |
"order_id": str(order_id), "user_email": user_email, "items": validated_items,
|
623 |
"delivery_address": user.get('businessAddress'), "mobile_number": user.get('phoneNumber'),"deliverydate": args.get('delivery_date')
|
624 |
}
|
625 |
+
order_doc['_id'] = order_id
|
626 |
+
product_ids = [ObjectId(item['productId']) for item in validated_items]
|
627 |
+
products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
|
628 |
+
order_doc['populated_items'] = [{
|
629 |
+
"name": products_map.get(item['productId'], {}).get('name', 'N/A'),
|
630 |
+
"quantity": item['quantity'],
|
631 |
+
"mode": item.get('mode', 'pieces')
|
632 |
+
} for item in validated_items]
|
633 |
+
send_order_confirmation_email(order_doc, user_id)
|
634 |
trigger_po_creation(order_details_for_xero)
|
635 |
final_response_text = f"Thank you! I have directly placed order #{next_serial} for *{user_name}* for delivery on {args.get('delivery_date')}. They can view details at https://matax-express.vercel.app/."
|
636 |
|
|
|
640 |
p_doc = mongo.db.products.find_one({'name': {'$regex': f'^{item.get("product_name")}$', '$options': 'i'}})
|
641 |
if p_doc:
|
642 |
db_items.append({"productId": str(p_doc['_id']), "quantity": item.get('quantity'), "mode": item.get('unit')})
|
643 |
+
modes=item.get('unit')
|
644 |
+
if modes == 'weight':
|
645 |
+
modes='lb'
|
646 |
+
added_messages.append(f"{item.get('quantity')} {modes} of {p_doc['name']}")
|
647 |
else: added_messages.append(f"could not find '{item.get('product_name')}'")
|
648 |
if db_items: mongo.db.carts.update_one({'user_email': user_email}, {'$push': {'items': {'$each': db_items}}, '$set': {'updated_at': datetime.utcnow()}}, upsert=True)
|
649 |
final_response_text = f"OK, I've updated the cart for *{user_name}*: I added {', '.join(added_messages)}."
|
|
|
662 |
"delivery_address": user.get('businessAddress'), "mobile_number": user.get('phoneNumber'),"deliverydate": args.get('delivery_date')
|
663 |
}
|
664 |
trigger_po_creation(order_details_for_xero)
|
665 |
+
order_doc['_id'] = order_id
|
666 |
+
product_ids = [ObjectId(item['productId']) for item in cart['items']]
|
667 |
+
products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
|
668 |
+
order_doc['populated_items'] = [{
|
669 |
+
"name": products_map.get(item['productId'], {}).get('name', 'N/A'),
|
670 |
+
"quantity": item['quantity'],
|
671 |
+
"mode": item.get('mode', 'pieces')
|
672 |
+
} for item in cart['items']]
|
673 |
+
send_order_confirmation_email(order_doc, user_id)
|
674 |
mongo.db.carts.delete_one({'user_email': user_email}) # Clear cart
|
675 |
final_response_text = f"Thank you! Order `#{next_serial}` has been placed for *{user_name}* for delivery on {args.get('delivery_date')}. They can view details at https://matax-express.vercel.app/."
|
676 |
|
app/api.py
CHANGED
@@ -1,392 +1,426 @@
|
|
1 |
-
# your_app/api.py
|
2 |
-
|
3 |
-
from flask import Blueprint, request, jsonify, current_app, redirect
|
4 |
-
from bson.objectid import ObjectId
|
5 |
-
from datetime import datetime
|
6 |
-
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
|
7 |
-
from .extensions import bcrypt, mongo
|
8 |
-
from .xero_utils import trigger_po_creation, trigger_contact_creation,sync_user_approval_from_xero
|
9 |
-
from .email_utils import send_order_confirmation_email, send_registration_email, send_login_notification_email, send_cart_reminder_email
|
10 |
-
from .general_utils import get_next_order_serial
|
11 |
-
|
12 |
-
|
13 |
-
api_bp = Blueprint('api', __name__)
|
14 |
-
@api_bp.route('/sync_xero_users', methods=['GET'])
|
15 |
-
def sync_xero_users():
|
16 |
-
sync_user_approval_from_xero()
|
17 |
-
return "✅"
|
18 |
-
|
19 |
-
@api_bp.route('/clear')
|
20 |
-
def clear_all():
|
21 |
-
mongo.db.users.delete_many({})
|
22 |
-
mongo.db.orders.delete_many({})
|
23 |
-
return "✅"
|
24 |
-
|
25 |
-
@api_bp.route('/register', methods=['POST'])
|
26 |
-
def register():
|
27 |
-
data = request.get_json()
|
28 |
-
email = data.get('email')
|
29 |
-
password = data.get('password')
|
30 |
-
company_name = data.get('businessName')
|
31 |
-
|
32 |
-
if not all([email, password, company_name]):
|
33 |
-
return jsonify({"msg": "Missing required fields: Email, Password, and Business Name"}), 400
|
34 |
-
if mongo.db.users.find_one({'email': email}):
|
35 |
-
return jsonify({"msg": "A user with this email already exists"}), 409
|
36 |
-
|
37 |
-
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
|
38 |
-
|
39 |
-
user_document = data.copy()
|
40 |
-
user_document['password'] = hashed_password
|
41 |
-
user_document['company_name'] = company_name
|
42 |
-
user_document['is_approved'] = False
|
43 |
-
user_document['is_admin'] = False
|
44 |
-
|
45 |
-
mongo.db.users.insert_one(user_document)
|
46 |
-
trigger_contact_creation(data)
|
47 |
-
|
48 |
-
try:
|
49 |
-
send_registration_email(data)
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
@
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
'
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
'
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
@
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
for
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
'
|
121 |
-
'
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
'
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
'
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
]
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
return jsonify(
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
)
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
392 |
return jsonify({"msg": "An internal server error occurred."}), 500
|
|
|
1 |
+
# your_app/api.py
|
2 |
+
|
3 |
+
from flask import Blueprint, request, jsonify, current_app, redirect
|
4 |
+
from bson.objectid import ObjectId
|
5 |
+
from datetime import datetime
|
6 |
+
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
|
7 |
+
from .extensions import bcrypt, mongo
|
8 |
+
from .xero_utils import trigger_po_creation, trigger_contact_creation,sync_user_approval_from_xero
|
9 |
+
from .email_utils import send_order_confirmation_email, send_registration_email, send_registration_admin_notification, send_login_notification_email, send_cart_reminder_email
|
10 |
+
from .general_utils import get_next_order_serial
|
11 |
+
|
12 |
+
|
13 |
+
api_bp = Blueprint('api', __name__)
|
14 |
+
@api_bp.route('/sync_xero_users', methods=['GET'])
|
15 |
+
def sync_xero_users():
|
16 |
+
sync_user_approval_from_xero()
|
17 |
+
return "✅"
|
18 |
+
|
19 |
+
@api_bp.route('/clear')
|
20 |
+
def clear_all():
|
21 |
+
mongo.db.users.delete_many({})
|
22 |
+
mongo.db.orders.delete_many({})
|
23 |
+
return "✅"
|
24 |
+
|
25 |
+
@api_bp.route('/register', methods=['POST'])
|
26 |
+
def register():
|
27 |
+
data = request.get_json()
|
28 |
+
email = data.get('email')
|
29 |
+
password = data.get('password')
|
30 |
+
company_name = data.get('businessName')
|
31 |
+
|
32 |
+
if not all([email, password, company_name]):
|
33 |
+
return jsonify({"msg": "Missing required fields: Email, Password, and Business Name"}), 400
|
34 |
+
if mongo.db.users.find_one({'email': email}):
|
35 |
+
return jsonify({"msg": "A user with this email already exists"}), 409
|
36 |
+
|
37 |
+
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
|
38 |
+
|
39 |
+
user_document = data.copy()
|
40 |
+
user_document['password'] = hashed_password
|
41 |
+
user_document['company_name'] = company_name
|
42 |
+
user_document['is_approved'] = False
|
43 |
+
user_document['is_admin'] = False
|
44 |
+
|
45 |
+
mongo.db.users.insert_one(user_document)
|
46 |
+
trigger_contact_creation(data)
|
47 |
+
|
48 |
+
try:
|
49 |
+
send_registration_email(data)
|
50 |
+
send_registration_admin_notification(data) # Send notification to admin
|
51 |
+
except Exception as e:
|
52 |
+
current_app.logger.error(f"Failed to send registration emails for {email}: {e}")
|
53 |
+
|
54 |
+
return jsonify({"msg": "Registration successful! Your application is being processed."}), 201
|
55 |
+
|
56 |
+
# ... (the rest of your api.py file remains unchanged)
|
57 |
+
@api_bp.route('/login', methods=['POST'])
|
58 |
+
def login():
|
59 |
+
data = request.get_json()
|
60 |
+
email, password = data.get('email'), data.get('password')
|
61 |
+
user = mongo.db.users.find_one({'email': email})
|
62 |
+
|
63 |
+
if user and user.get('password') and bcrypt.check_password_hash(user['password'], password):
|
64 |
+
if not user.get('is_approved', False): return jsonify({"msg": "Account pending approval"}), 403
|
65 |
+
|
66 |
+
try:
|
67 |
+
send_login_notification_email(user)
|
68 |
+
except Exception as e:
|
69 |
+
current_app.logger.error(f"Failed to send login notification email to {email}: {e}")
|
70 |
+
|
71 |
+
access_token = create_access_token(identity=email)
|
72 |
+
return jsonify(access_token=access_token, email=user['email'], companyName=user['businessName'],contactPerson=user.get('contactPerson', '')) , 200
|
73 |
+
|
74 |
+
return jsonify({"msg": "Bad email or password"}), 401
|
75 |
+
|
76 |
+
@api_bp.route('/profile', methods=['GET'])
|
77 |
+
@jwt_required()
|
78 |
+
def get_user_profile():
|
79 |
+
user_email = get_jwt_identity()
|
80 |
+
user = mongo.db.users.find_one({'email': user_email})
|
81 |
+
|
82 |
+
if not user:
|
83 |
+
return jsonify({"msg": "User not found"}), 404
|
84 |
+
|
85 |
+
profile_data = {
|
86 |
+
'deliveryAddress': user.get('businessAddress', ''),
|
87 |
+
'mobileNumber': user.get('phoneNumber', '')
|
88 |
+
}
|
89 |
+
|
90 |
+
return jsonify(profile_data), 200
|
91 |
+
|
92 |
+
@api_bp.route('/products', methods=['GET'])
|
93 |
+
def get_products():
|
94 |
+
products = [{
|
95 |
+
'id': str(p['_id']), 'name': p.get('name'), 'category': p.get('category'),
|
96 |
+
'unit': p.get('unit'), 'image_url': p.get('image_url', ''), 'price': p.get('price', '')
|
97 |
+
} for p in mongo.db.products.find()]
|
98 |
+
return jsonify(products)
|
99 |
+
|
100 |
+
|
101 |
+
@api_bp.route('/cart', methods=['GET', 'POST'])
|
102 |
+
@jwt_required()
|
103 |
+
def handle_cart():
|
104 |
+
user_email = get_jwt_identity()
|
105 |
+
|
106 |
+
if request.method == 'GET':
|
107 |
+
cart = mongo.db.carts.find_one({'user_email': user_email})
|
108 |
+
if not cart:
|
109 |
+
return jsonify({'items': [], 'deliveryDate': None})
|
110 |
+
|
111 |
+
populated_items = []
|
112 |
+
if cart.get('items'):
|
113 |
+
product_ids = [ObjectId(item['productId']) for item in cart['items']]
|
114 |
+
if product_ids:
|
115 |
+
products = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
|
116 |
+
for item in cart['items']:
|
117 |
+
details = products.get(item['productId'])
|
118 |
+
if details:
|
119 |
+
populated_items.append({
|
120 |
+
'product': {'id': str(details['_id']), 'name': details.get('name'), 'unit': details.get('unit'), 'image_url': details.get('image_url'), 'price': details.get('price')},
|
121 |
+
'quantity': item['quantity'],
|
122 |
+
'mode': item.get('mode', 'pieces')
|
123 |
+
})
|
124 |
+
|
125 |
+
return jsonify({
|
126 |
+
'items': populated_items,
|
127 |
+
'deliveryDate': cart.get('deliveryDate')
|
128 |
+
})
|
129 |
+
|
130 |
+
if request.method == 'POST':
|
131 |
+
data = request.get_json()
|
132 |
+
|
133 |
+
update_doc = {
|
134 |
+
'user_email': user_email,
|
135 |
+
'updated_at': datetime.utcnow()
|
136 |
+
}
|
137 |
+
|
138 |
+
# --- MODIFICATION START ---
|
139 |
+
if 'items' in data:
|
140 |
+
sanitized_items = []
|
141 |
+
for item in data['items']:
|
142 |
+
try:
|
143 |
+
# Ensure required fields exist and are not None
|
144 |
+
if not all(k in item for k in ['productId', 'quantity', 'mode']) or item['quantity'] is None:
|
145 |
+
continue # Skip malformed items
|
146 |
+
|
147 |
+
mode = item.get('mode')
|
148 |
+
quantity = item.get('quantity')
|
149 |
+
|
150 |
+
if mode == 'weight':
|
151 |
+
# For weight, convert to float. Handles integer and float strings.
|
152 |
+
numeric_quantity = float(quantity)
|
153 |
+
else:
|
154 |
+
# For all other modes, convert to integer.
|
155 |
+
numeric_quantity = int(float(quantity)) # Use float first to handle "1.0"
|
156 |
+
|
157 |
+
# Ensure quantity is not negative
|
158 |
+
if numeric_quantity < 0:
|
159 |
+
continue
|
160 |
+
|
161 |
+
sanitized_items.append({
|
162 |
+
'productId': item['productId'],
|
163 |
+
'quantity': numeric_quantity,
|
164 |
+
'mode': mode
|
165 |
+
})
|
166 |
+
|
167 |
+
except (ValueError, TypeError):
|
168 |
+
# If quantity is not a valid number for its mode, return an error
|
169 |
+
return jsonify({"msg": f"Invalid quantity format for item."}), 400
|
170 |
+
|
171 |
+
update_doc['items'] = sanitized_items
|
172 |
+
# --- MODIFICATION END ---
|
173 |
+
|
174 |
+
if 'deliveryDate' in data:
|
175 |
+
update_doc['deliveryDate'] = data['deliveryDate']
|
176 |
+
|
177 |
+
mongo.db.carts.update_one(
|
178 |
+
{'user_email': user_email},
|
179 |
+
{'$set': update_doc},
|
180 |
+
upsert=True
|
181 |
+
)
|
182 |
+
return jsonify({"msg": "Cart updated successfully"})
|
183 |
+
|
184 |
+
@api_bp.route('/orders', methods=['GET', 'POST'])
|
185 |
+
@jwt_required()
|
186 |
+
def handle_orders():
|
187 |
+
user_email = get_jwt_identity()
|
188 |
+
|
189 |
+
if request.method == 'POST':
|
190 |
+
cart = mongo.db.carts.find_one({'user_email': user_email})
|
191 |
+
if not cart or not cart.get('items'): return jsonify({"msg": "Your cart is empty"}), 400
|
192 |
+
|
193 |
+
data = request.get_json()
|
194 |
+
if not all([data.get('deliveryDate'), data.get('deliveryAddress'), data.get('mobileNumber')]): return jsonify({"msg": "Missing delivery information"}), 400
|
195 |
+
|
196 |
+
user = mongo.db.users.find_one({'email': user_email})
|
197 |
+
if not user:
|
198 |
+
return jsonify({"msg": "User not found"}), 404
|
199 |
+
|
200 |
+
order_doc = {
|
201 |
+
'user_email': user_email, 'items': cart['items'], 'delivery_date': data['deliveryDate'],
|
202 |
+
'delivery_address': data['deliveryAddress'], 'mobile_number': data['mobileNumber'],
|
203 |
+
'additional_info': data.get('additionalInfo'), 'total_amount': data.get('totalAmount'),
|
204 |
+
'status': 'pending', 'created_at': datetime.utcnow()
|
205 |
+
}
|
206 |
+
order_doc['serial_no'] = get_next_order_serial()
|
207 |
+
order_id = mongo.db.orders.insert_one(order_doc).inserted_id
|
208 |
+
order_doc['_id'] = order_id
|
209 |
+
|
210 |
+
order_details_for_xero = {
|
211 |
+
"order_id": str(order_id), "user_email": user_email, "items": cart['items'],
|
212 |
+
"delivery_address": data['deliveryAddress'], "mobile_number": data['mobileNumber'],"deliverydate":data["deliveryDate"]
|
213 |
+
}
|
214 |
+
trigger_po_creation(order_details_for_xero)
|
215 |
+
|
216 |
+
try:
|
217 |
+
product_ids = [ObjectId(item['productId']) for item in cart['items']]
|
218 |
+
products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
|
219 |
+
|
220 |
+
order_doc['populated_items'] = [{
|
221 |
+
"name": products_map.get(item['productId'], {}).get('name', 'N/A'),
|
222 |
+
"quantity": item['quantity'],
|
223 |
+
"mode": item.get('mode', 'pieces')
|
224 |
+
} for item in cart['items']]
|
225 |
+
|
226 |
+
send_order_confirmation_email(order_doc, user)
|
227 |
+
|
228 |
+
except Exception as e:
|
229 |
+
current_app.logger.error(f"Failed to send confirmation email for order {order_id}: {e}")
|
230 |
+
|
231 |
+
mongo.db.carts.delete_one({'user_email': user_email})
|
232 |
+
return jsonify({"msg": "Order placed successfully! You will be redirected shortly to the Orders Page!", "orderId": str(order_id)}), 201
|
233 |
+
|
234 |
+
if request.method == 'GET':
|
235 |
+
user_orders = list(mongo.db.orders.find({'user_email': user_email}).sort('created_at', -1))
|
236 |
+
if not user_orders: return jsonify([])
|
237 |
+
|
238 |
+
all_product_ids = {ObjectId(item['productId']) for order in user_orders for item in order.get('items', [])}
|
239 |
+
products = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': list(all_product_ids)}})}
|
240 |
+
|
241 |
+
for order in user_orders:
|
242 |
+
order['items'] = [
|
243 |
+
{
|
244 |
+
'quantity': item['quantity'],
|
245 |
+
'mode': item.get('mode', 'pieces'),
|
246 |
+
'product': {
|
247 |
+
'id': str(p['_id']),
|
248 |
+
'name': p.get('name'),
|
249 |
+
'unit': p.get('unit'),
|
250 |
+
'image_url': p.get('image_url')
|
251 |
+
}
|
252 |
+
}
|
253 |
+
for item in order.get('items', []) if (p := products.get(item['productId']))
|
254 |
+
]
|
255 |
+
order['_id'] = str(order['_id'])
|
256 |
+
order['created_at'] = order['created_at'].isoformat()
|
257 |
+
order['delivery_date'] = order['delivery_date'] if isinstance(order['delivery_date'], str) else order['delivery_date'].isoformat()
|
258 |
+
return jsonify(user_orders)
|
259 |
+
|
260 |
+
@api_bp.route('/orders/<order_id>', methods=['GET'])
|
261 |
+
@jwt_required()
|
262 |
+
def get_order(order_id):
|
263 |
+
user_email = get_jwt_identity()
|
264 |
+
try:
|
265 |
+
order = mongo.db.orders.find_one({'_id': ObjectId(order_id), 'user_email': user_email})
|
266 |
+
if not order:
|
267 |
+
return jsonify({"msg": "Order not found or access denied"}), 404
|
268 |
+
|
269 |
+
order['_id'] = str(order['_id'])
|
270 |
+
return jsonify(order), 200
|
271 |
+
except Exception as e:
|
272 |
+
return jsonify({"msg": f"Invalid Order ID format: {e}"}), 400
|
273 |
+
|
274 |
+
@api_bp.route('/orders/<order_id>', methods=['PUT'])
|
275 |
+
@jwt_required()
|
276 |
+
def update_order(order_id):
|
277 |
+
user_email = get_jwt_identity()
|
278 |
+
|
279 |
+
order = mongo.db.orders.find_one({'_id': ObjectId(order_id), 'user_email': user_email})
|
280 |
+
if not order:
|
281 |
+
return jsonify({"msg": "Order not found or access denied"}), 404
|
282 |
+
|
283 |
+
if order.get('status') not in ['pending', 'confirmed']:
|
284 |
+
return jsonify({"msg": f"Order with status '{order.get('status')}' cannot be modified."}), 400
|
285 |
+
|
286 |
+
cart = mongo.db.carts.find_one({'user_email': user_email})
|
287 |
+
if not cart or not cart.get('items'):
|
288 |
+
return jsonify({"msg": "Cannot update with an empty cart. Please add items."}), 400
|
289 |
+
|
290 |
+
data = request.get_json()
|
291 |
+
update_doc = {
|
292 |
+
'items': cart['items'],
|
293 |
+
'delivery_date': data['deliveryDate'],
|
294 |
+
'delivery_address': data['deliveryAddress'],
|
295 |
+
'mobile_number': data['mobileNumber'],
|
296 |
+
'additional_info': data.get('additionalInfo'),
|
297 |
+
'total_amount': data.get('totalAmount'),
|
298 |
+
'updated_at': datetime.utcnow()
|
299 |
+
}
|
300 |
+
|
301 |
+
mongo.db.orders.update_one({'_id': ObjectId(order_id)}, {'$set': update_doc})
|
302 |
+
mongo.db.carts.delete_one({'user_email': user_email})
|
303 |
+
|
304 |
+
return jsonify({"msg": "Order updated successfully!", "orderId": order_id}), 200
|
305 |
+
|
306 |
+
@api_bp.route('/orders/<order_id>/cancel', methods=['POST'])
|
307 |
+
@jwt_required()
|
308 |
+
def cancel_order(order_id):
|
309 |
+
user_email = get_jwt_identity()
|
310 |
+
order = mongo.db.orders.find_one({'_id': ObjectId(order_id), 'user_email': user_email})
|
311 |
+
|
312 |
+
if not order:
|
313 |
+
return jsonify({"msg": "Order not found or access denied"}), 404
|
314 |
+
|
315 |
+
if order.get('status') in ['delivered', 'cancelled']:
|
316 |
+
return jsonify({"msg": "This order can no longer be cancelled."}), 400
|
317 |
+
|
318 |
+
mongo.db.orders.update_one(
|
319 |
+
{'_id': ObjectId(order_id)},
|
320 |
+
{'$set': {'status': 'cancelled', 'updated_at': datetime.utcnow()}}
|
321 |
+
)
|
322 |
+
|
323 |
+
return jsonify({"msg": "Order has been cancelled."}), 200
|
324 |
+
|
325 |
+
@api_bp.route('/sendmail', methods=['GET'])
|
326 |
+
def send_cart_reminders():
|
327 |
+
try:
|
328 |
+
carts_with_items = list(mongo.db.carts.find({'items': {'$exists': True, '$ne': []}}))
|
329 |
+
|
330 |
+
if not carts_with_items:
|
331 |
+
return jsonify({"msg": "No users with pending items in cart."}), 200
|
332 |
+
|
333 |
+
user_emails = [cart['user_email'] for cart in carts_with_items]
|
334 |
+
all_product_ids = {
|
335 |
+
ObjectId(item['productId'])
|
336 |
+
for cart in carts_with_items
|
337 |
+
for item in cart.get('items', [])
|
338 |
+
}
|
339 |
+
|
340 |
+
users_cursor = mongo.db.users.find({'email': {'$in': user_emails}})
|
341 |
+
products_cursor = mongo.db.products.find({'_id': {'$in': list(all_product_ids)}})
|
342 |
+
|
343 |
+
users_map = {user['email']: user for user in users_cursor}
|
344 |
+
products_map = {str(prod['_id']): prod for prod in products_cursor}
|
345 |
+
|
346 |
+
emails_sent_count = 0
|
347 |
+
|
348 |
+
for cart in carts_with_items:
|
349 |
+
user = users_map.get(cart['user_email'])
|
350 |
+
if not user:
|
351 |
+
current_app.logger.warning(f"Cart found for non-existent user: {cart['user_email']}")
|
352 |
+
continue
|
353 |
+
|
354 |
+
populated_items = []
|
355 |
+
for item in cart.get('items', []):
|
356 |
+
product_details = products_map.get(item['productId'])
|
357 |
+
if product_details:
|
358 |
+
populated_items.append({
|
359 |
+
'product': {
|
360 |
+
'id': str(product_details['_id']),
|
361 |
+
'name': product_details.get('name'),
|
362 |
+
},
|
363 |
+
'quantity': item['quantity']
|
364 |
+
})
|
365 |
+
|
366 |
+
if populated_items:
|
367 |
+
try:
|
368 |
+
send_cart_reminder_email(user, populated_items)
|
369 |
+
emails_sent_count += 1
|
370 |
+
except Exception as e:
|
371 |
+
current_app.logger.error(f"Failed to send cart reminder to {user['email']}: {e}")
|
372 |
+
|
373 |
+
return jsonify({"msg": f"Cart reminder process finished. Emails sent to {emails_sent_count} users."}), 200
|
374 |
+
|
375 |
+
except Exception as e:
|
376 |
+
current_app.logger.error(f"Error in /sendmail endpoint: {e}")
|
377 |
+
return jsonify({"msg": "An internal error occurred while sending reminders."}), 500
|
378 |
+
|
379 |
+
@api_bp.route('/admin/users/approve/<user_id>', methods=['POST'])
|
380 |
+
@jwt_required()
|
381 |
+
def approve_user(user_id):
|
382 |
+
mongo.db.users.update_one({'_id': ObjectId(user_id)}, {'$set': {'is_approved': True}})
|
383 |
+
return jsonify({"msg": f"User {user_id} approved"})
|
384 |
+
|
385 |
+
# +++ START: NEW ENDPOINT FOR ITEM REQUESTS +++
|
386 |
+
@api_bp.route('/request-item', methods=['POST'])
|
387 |
+
@jwt_required()
|
388 |
+
def request_item():
|
389 |
+
"""
|
390 |
+
Allows a logged-in user to request an item that is not in the catalog.
|
391 |
+
The request is saved to the database for admin review.
|
392 |
+
"""
|
393 |
+
user_email = get_jwt_identity()
|
394 |
+
data = request.get_json()
|
395 |
+
|
396 |
+
if not data or not data.get('details'):
|
397 |
+
return jsonify({"msg": "Item details are required."}), 400
|
398 |
+
|
399 |
+
details = data.get('details').strip()
|
400 |
+
if not details:
|
401 |
+
return jsonify({"msg": "Item details cannot be empty."}), 400
|
402 |
+
|
403 |
+
try:
|
404 |
+
# Fetch user info for more context in the request
|
405 |
+
user = mongo.db.users.find_one({'email': user_email}, {'company_name': 1})
|
406 |
+
company_name = user.get('company_name', 'N/A') if user else 'N/A'
|
407 |
+
|
408 |
+
request_doc = {
|
409 |
+
'user_email': user_email,
|
410 |
+
'company_name': company_name,
|
411 |
+
'details': details,
|
412 |
+
'status': 'new', # Possible statuses: 'new', 'reviewed', 'sourced', 'rejected'
|
413 |
+
'requested_at': datetime.utcnow()
|
414 |
+
}
|
415 |
+
|
416 |
+
# The collection 'item_requests' will be created if it doesn't exist
|
417 |
+
mongo.db.item_requests.insert_one(request_doc)
|
418 |
+
|
419 |
+
# Optional: Here you could add a call to an email utility to notify admins
|
420 |
+
# For example: send_item_request_notification(user_email, company_name, details)
|
421 |
+
|
422 |
+
return jsonify({"msg": "Your item request has been submitted. We will look into it!"}), 201
|
423 |
+
|
424 |
+
except Exception as e:
|
425 |
+
current_app.logger.error(f"Error processing item request for {user_email}: {e}")
|
426 |
return jsonify({"msg": "An internal server error occurred."}), 500
|
app/email_utils.py
CHANGED
@@ -28,6 +28,15 @@ def send_order_confirmation_email(order, user):
|
|
28 |
contact_person = user['contactPerson']
|
29 |
order_id_str = str(order['serial_no'])
|
30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
# --- Generate HTML table of ordered items ---
|
32 |
items_html = "<table border='1' cellpadding='5' cellspacing='0' style='border-collapse: collapse; width: 100%;'><tr><th style='text-align: left;'>Product</th><th style='text-align: center;'>Quantity</th></tr>"
|
33 |
for item in order['populated_items']:
|
@@ -50,6 +59,7 @@ def send_order_confirmation_email(order, user):
|
|
50 |
<p><strong>Contact Number:</strong> {order['mobile_number']}</p>
|
51 |
<p><strong>Additional Info:</strong> {order.get('additional_info') or 'N/A'}</p>
|
52 |
<p>Thank you for your business!</p>
|
|
|
53 |
</body></html>
|
54 |
"""
|
55 |
|
@@ -107,6 +117,15 @@ def send_registration_email(user_data):
|
|
107 |
recipient_email = user_data['email']
|
108 |
recipient_name = user_data.get('contactPerson', user_data.get('businessName', 'New User'))
|
109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
subject = "your application is under process"
|
111 |
html_content = f"""
|
112 |
<html><body>
|
@@ -115,6 +134,7 @@ def send_registration_email(user_data):
|
|
115 |
<p>We will notify you via email as soon as your account is approved.</p>
|
116 |
<p>Thank you for your patience.</p>
|
117 |
<p>Best regards,<br>The Matax Express Team</p>
|
|
|
118 |
</body></html>
|
119 |
"""
|
120 |
|
@@ -131,6 +151,69 @@ def send_registration_email(user_data):
|
|
131 |
except ApiException as e:
|
132 |
current_app.logger.error(f"Failed to send registration email to {recipient_email}: {e}")
|
133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
134 |
def send_login_notification_email(user):
|
135 |
"""Sends a notification email upon successful login."""
|
136 |
configuration = sib_api_v3_sdk.Configuration()
|
@@ -142,6 +225,15 @@ def send_login_notification_email(user):
|
|
142 |
recipient_email = user['email']
|
143 |
recipient_name = user.get('contactPerson', user.get('company_name', 'Valued Customer'))
|
144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
145 |
subject = "New login.."
|
146 |
html_content = f"""
|
147 |
<html><body>
|
@@ -151,6 +243,7 @@ def send_login_notification_email(user):
|
|
151 |
<p>If this was you, you can safely disregard this email. If you do not recognize this activity, please change your password immediately and contact our support team.</p>
|
152 |
<p>Thank you for helping us keep your account secure.</p>
|
153 |
<p>Best regards,<br>The Matax Express Team</p>
|
|
|
154 |
</body></html>
|
155 |
"""
|
156 |
|
@@ -178,6 +271,15 @@ def send_cart_reminder_email(user, populated_items):
|
|
178 |
recipient_email = user['email']
|
179 |
recipient_name = user.get('contactPerson', user.get('company_name', 'Valued Customer'))
|
180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
181 |
subject = "Your items are waiting for you!"
|
182 |
|
183 |
items_html = "<table border='1' cellpadding='5' cellspacing='0' style='border-collapse: collapse; width: 100%;'><tr><th style='text-align: left;'>Product</th><th style='text-align: center;'>Quantity</th></tr>"
|
@@ -195,6 +297,7 @@ def send_cart_reminder_email(user, populated_items):
|
|
195 |
<p>Ready to complete your order? Please log in to your account to view your cart.</p>
|
196 |
<p>These items are popular and might not be available for long!</p>
|
197 |
<p>Best regards,<br>The Matax Express Team</p>
|
|
|
198 |
</body></html>
|
199 |
"""
|
200 |
|
|
|
28 |
contact_person = user['contactPerson']
|
29 |
order_id_str = str(order['serial_no'])
|
30 |
|
31 |
+
# --- Common portal footer ---
|
32 |
+
portal_footer = """
|
33 |
+
<p style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #cccccc; font-size: 14px;">
|
34 |
+
Remember, you can place new orders, view your order history, manage your cart, and even use our AI-powered quick order feature at our customer portal:
|
35 |
+
<br>
|
36 |
+
<a href="https://matax-express.vercel.app/" style="color: #007bff;">https://matax-express.vercel.app/</a>
|
37 |
+
</p>
|
38 |
+
"""
|
39 |
+
|
40 |
# --- Generate HTML table of ordered items ---
|
41 |
items_html = "<table border='1' cellpadding='5' cellspacing='0' style='border-collapse: collapse; width: 100%;'><tr><th style='text-align: left;'>Product</th><th style='text-align: center;'>Quantity</th></tr>"
|
42 |
for item in order['populated_items']:
|
|
|
59 |
<p><strong>Contact Number:</strong> {order['mobile_number']}</p>
|
60 |
<p><strong>Additional Info:</strong> {order.get('additional_info') or 'N/A'}</p>
|
61 |
<p>Thank you for your business!</p>
|
62 |
+
{portal_footer}
|
63 |
</body></html>
|
64 |
"""
|
65 |
|
|
|
117 |
recipient_email = user_data['email']
|
118 |
recipient_name = user_data.get('contactPerson', user_data.get('businessName', 'New User'))
|
119 |
|
120 |
+
# --- Common portal footer ---
|
121 |
+
portal_footer = f"""
|
122 |
+
<p style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #cccccc; font-size: 14px;">
|
123 |
+
Once your account is approved, you can place orders, view your order history, manage your cart, and use our AI-powered quick order feature at our customer portal:
|
124 |
+
<br>
|
125 |
+
<a href="https://matax-express.vercel.app/" style="color: #007bff;">https://matax-express.vercel.app/</a>
|
126 |
+
</p>
|
127 |
+
"""
|
128 |
+
|
129 |
subject = "your application is under process"
|
130 |
html_content = f"""
|
131 |
<html><body>
|
|
|
134 |
<p>We will notify you via email as soon as your account is approved.</p>
|
135 |
<p>Thank you for your patience.</p>
|
136 |
<p>Best regards,<br>The Matax Express Team</p>
|
137 |
+
{portal_footer}
|
138 |
</body></html>
|
139 |
"""
|
140 |
|
|
|
151 |
except ApiException as e:
|
152 |
current_app.logger.error(f"Failed to send registration email to {recipient_email}: {e}")
|
153 |
|
154 |
+
def send_registration_admin_notification(registration_data):
|
155 |
+
"""Sends a notification to the admin with the new client's details."""
|
156 |
+
configuration = sib_api_v3_sdk.Configuration()
|
157 |
+
configuration.api_key['api-key'] = current_app.config['BREVO_API_KEY']
|
158 |
+
api_instance = sib_api_v3_sdk.TransactionalEmailsApi(sib_api_v3_sdk.ApiClient(configuration))
|
159 |
+
|
160 |
+
sender_email = current_app.config['SENDER_EMAIL']
|
161 |
+
sender_name = "Matax Express System"
|
162 |
+
admin_email = current_app.config['CLIENT_ADMIN_EMAIL']
|
163 |
+
|
164 |
+
# Format the details as requested
|
165 |
+
history_details = (
|
166 |
+
f"--- Client Application Details ---\n"
|
167 |
+
f"Business Name: {registration_data.get('businessName')}\n"
|
168 |
+
f"Contact Person: {registration_data.get('contactPerson')}\n"
|
169 |
+
f"Email: {registration_data.get('email')}\n"
|
170 |
+
f"Phone: {registration_data.get('phoneNumber')}\n"
|
171 |
+
f"Company Website: {registration_data.get('companyWebsite')}\n"
|
172 |
+
f"Business Address: {registration_data.get('businessAddress')}\n"
|
173 |
+
f"Business Type: {registration_data.get('businessType')}\n"
|
174 |
+
f"Years Operating: {registration_data.get('yearsOperating')}\n"
|
175 |
+
f"Number of Locations: {registration_data.get('numLocations')}\n"
|
176 |
+
f"Estimated Weekly Volume: {registration_data.get('estimatedVolume')}\n\n"
|
177 |
+
|
178 |
+
f"--- Logistics Information ---\n"
|
179 |
+
f"Preferred Delivery Times: {registration_data.get('preferredDeliveryTimes')}\n"
|
180 |
+
f"Has Loading Dock: {registration_data.get('hasLoadingDock')}\n"
|
181 |
+
f"Special Delivery Instructions: {registration_data.get('specialDeliveryInstructions')}\n"
|
182 |
+
f"Minimum Quantity Requirements: {registration_data.get('minQuantityRequirements')}\n\n"
|
183 |
+
|
184 |
+
f"--- Service & Billing ---\n"
|
185 |
+
f"Reason for Switching: {registration_data.get('reasonForSwitch')}\n"
|
186 |
+
f"Preferred Payment Method: {registration_data.get('paymentMethod')}\n\n"
|
187 |
+
|
188 |
+
f"--- Additional Notes ---\n"
|
189 |
+
f"{registration_data.get('additionalNotes')}"
|
190 |
+
)
|
191 |
+
|
192 |
+
subject = f"New Client Application: {registration_data.get('businessName')}"
|
193 |
+
# Use <pre> tag to preserve formatting in HTML email
|
194 |
+
html_content = f"""
|
195 |
+
<html><body>
|
196 |
+
<h1>New Client Application Received</h1>
|
197 |
+
<p>A new client has submitted an application for an account. Please review the details below and approve them in the admin panel if applicable.</p>
|
198 |
+
<hr>
|
199 |
+
<pre style="font-family: monospace; font-size: 14px;">{history_details}</pre>
|
200 |
+
</body></html>
|
201 |
+
"""
|
202 |
+
|
203 |
+
send_smtp_email = sib_api_v3_sdk.SendSmtpEmail(
|
204 |
+
to=[{ "email": admin_email }],
|
205 |
+
sender={ "email": sender_email, "name": sender_name },
|
206 |
+
subject=subject,
|
207 |
+
html_content=html_content
|
208 |
+
)
|
209 |
+
|
210 |
+
try:
|
211 |
+
api_instance.send_transac_email(send_smtp_email)
|
212 |
+
current_app.logger.info(f"Admin notification for new registration '{registration_data.get('email')}' sent to {admin_email}")
|
213 |
+
except ApiException as e:
|
214 |
+
current_app.logger.error(f"Failed to send admin notification email for registration '{registration_data.get('email')}': {e}")
|
215 |
+
|
216 |
+
|
217 |
def send_login_notification_email(user):
|
218 |
"""Sends a notification email upon successful login."""
|
219 |
configuration = sib_api_v3_sdk.Configuration()
|
|
|
225 |
recipient_email = user['email']
|
226 |
recipient_name = user.get('contactPerson', user.get('company_name', 'Valued Customer'))
|
227 |
|
228 |
+
# --- Common portal footer ---
|
229 |
+
portal_footer = """
|
230 |
+
<p style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #cccccc; font-size: 14px;">
|
231 |
+
You can place new orders, view your order history, manage your cart, and even use our AI-powered quick order feature at our customer portal:
|
232 |
+
<br>
|
233 |
+
<a href="https://matax-express.vercel.app/" style="color: #007bff;">https://matax-express.vercel.app/</a>
|
234 |
+
</p>
|
235 |
+
"""
|
236 |
+
|
237 |
subject = "New login.."
|
238 |
html_content = f"""
|
239 |
<html><body>
|
|
|
243 |
<p>If this was you, you can safely disregard this email. If you do not recognize this activity, please change your password immediately and contact our support team.</p>
|
244 |
<p>Thank you for helping us keep your account secure.</p>
|
245 |
<p>Best regards,<br>The Matax Express Team</p>
|
246 |
+
{portal_footer}
|
247 |
</body></html>
|
248 |
"""
|
249 |
|
|
|
271 |
recipient_email = user['email']
|
272 |
recipient_name = user.get('contactPerson', user.get('company_name', 'Valued Customer'))
|
273 |
|
274 |
+
# --- Common portal footer ---
|
275 |
+
portal_footer = """
|
276 |
+
<p style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #cccccc; font-size: 14px;">
|
277 |
+
To complete your order, or to place new ones, please visit our customer portal:
|
278 |
+
<br>
|
279 |
+
<a href="https://matax-express.vercel.app/" style="color: #007bff;">https://matax-express.vercel.app/</a>
|
280 |
+
</p>
|
281 |
+
"""
|
282 |
+
|
283 |
subject = "Your items are waiting for you!"
|
284 |
|
285 |
items_html = "<table border='1' cellpadding='5' cellspacing='0' style='border-collapse: collapse; width: 100%;'><tr><th style='text-align: left;'>Product</th><th style='text-align: center;'>Quantity</th></tr>"
|
|
|
297 |
<p>Ready to complete your order? Please log in to your account to view your cart.</p>
|
298 |
<p>These items are popular and might not be available for long!</p>
|
299 |
<p>Best regards,<br>The Matax Express Team</p>
|
300 |
+
{portal_footer}
|
301 |
</body></html>
|
302 |
"""
|
303 |
|
app/page_features.py
CHANGED
@@ -1,10 +1,530 @@
|
|
1 |
# your_app/page_features.py
|
2 |
|
3 |
-
|
|
|
|
|
|
|
|
|
|
|
4 |
from .extensions import mongo
|
|
|
|
|
5 |
|
6 |
page_bp = Blueprint('pages', __name__)
|
7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
# Default data for initialization if pages don't exist in the database
|
9 |
DEFAULT_ABOUT_DATA = {
|
10 |
"_id": "about",
|
@@ -75,19 +595,18 @@ def update_page_content():
|
|
75 |
]
|
76 |
}
|
77 |
else:
|
78 |
-
return redirect('/api/update')
|
79 |
|
80 |
mongo.db.pages.update_one(
|
81 |
{'_id': page_name},
|
82 |
{'$set': update_data},
|
83 |
upsert=True
|
84 |
)
|
85 |
-
return redirect('/api/
|
86 |
|
87 |
@page_bp.route('/update_ui', methods=['GET'])
|
88 |
def update_ui():
|
89 |
"""Serves the simple HTML UI for editing page content."""
|
90 |
-
# Note: Route changed to /update_ui to avoid conflict with /pages/update
|
91 |
about_data = mongo.db.pages.find_one({'_id': 'about'}) or DEFAULT_ABOUT_DATA
|
92 |
contact_data = mongo.db.pages.find_one({'_id': 'contact'}) or DEFAULT_CONTACT_DATA
|
93 |
|
|
|
1 |
# your_app/page_features.py
|
2 |
|
3 |
+
import os
|
4 |
+
import json
|
5 |
+
import requests
|
6 |
+
from flask import Blueprint, request, jsonify, redirect, render_template_string, flash
|
7 |
+
|
8 |
+
# Assuming extensions.py exists and is configured correctly
|
9 |
from .extensions import mongo
|
10 |
+
# For standalone running, we'll create a mock mongo object
|
11 |
+
|
12 |
|
13 |
page_bp = Blueprint('pages', __name__)
|
14 |
|
15 |
+
# --- Vercel Configuration (as provided) ---
|
16 |
+
VERCEL_TOKEN = "azm0JC5lyPwah6qPGhbN5DL5"
|
17 |
+
PROJECT_ID = "prj_4RemrVT2H255cTcdXxkLqFJUD6Dq"
|
18 |
+
# Vercel Team ID is optional. Only include it if your project is under a team.
|
19 |
+
TEAM_ID = None # Set to your Team ID string if applicable, otherwise None or False
|
20 |
+
PROJECT_NAME = "matax-express"
|
21 |
+
|
22 |
+
# --- Default Data Structures (as provided) ---
|
23 |
+
# These are used as fallbacks if fetching from Vercel fails.
|
24 |
+
DEFAULT_WHY_US_FEATURES_DATA = [
|
25 |
+
{ "icon": "EnergySavingsLeafIcon", "title": "Peak Freshness", "description": "Guaranteed farm-to-door freshness in every order." },
|
26 |
+
{ "icon": "YardIcon", "title": "Local Sourcing", "description": "Partnering with local farms to support the community." },
|
27 |
+
{ "icon": "CategoryIcon", "title": "Wide Selection", "description": "A diverse range of produce, dairy, and pantry staples." },
|
28 |
+
{ "icon": "LocalShippingIcon", "title": "Reliable Delivery", "description": "On-time, refrigerated delivery you can count on." },
|
29 |
+
]
|
30 |
+
|
31 |
+
DEFAULT_FAQS_DATA = [
|
32 |
+
{ "question": "What regions do you deliver to?", "answer": "We currently deliver to all major metropolitan areas within the state. We are actively expanding our delivery network, so please check back for updates on new regions." },
|
33 |
+
{ "question": "How do I place an order?", "answer": "Once you register for an account and are approved, you can log in to our customer portal. From there, you can browse our product catalog, select quantities, and schedule your delivery." },
|
34 |
+
{ "question": "What are your quality standards?", "answer": "We pride ourselves on sourcing only the freshest, Grade A produce from trusted local and national farms. Every item is inspected for quality and freshness before it leaves our facility." },
|
35 |
+
{ "question": "Is there a minimum order requirement?", "answer": "Yes, there is a minimum order value for delivery. This amount varies by region. You can find the specific minimum for your area in your customer portal after logging in." },
|
36 |
+
]
|
37 |
+
|
38 |
+
# --- Vercel API Helper Functions (Integrated into Flask App) ---
|
39 |
+
# Note: These functions have been slightly modified to append log messages to a list
|
40 |
+
# for display in the UI, rather than printing to the console.
|
41 |
+
|
42 |
+
def get_existing_env_var(key, headers, params, logs):
|
43 |
+
"""Fetches a specific environment variable by key."""
|
44 |
+
api_url = f"https://api.vercel.com/v9/projects/{PROJECT_ID}/env"
|
45 |
+
try:
|
46 |
+
response = requests.get(api_url, headers=headers, params=params, timeout=10)
|
47 |
+
response.raise_for_status()
|
48 |
+
all_vars = response.json().get('envs', [])
|
49 |
+
for var in all_vars:
|
50 |
+
if var['key'] == key:
|
51 |
+
logs.append(f"INFO: Found existing variable '{key}' with ID: {var['id']}")
|
52 |
+
return var
|
53 |
+
logs.append(f"INFO: No existing environment variable found with key '{key}'.")
|
54 |
+
return None
|
55 |
+
except requests.exceptions.RequestException as err:
|
56 |
+
logs.append(f"❌ ERROR: Error fetching environment variables: {err}")
|
57 |
+
return None
|
58 |
+
|
59 |
+
def delete_env_var(var_id, headers, params, logs):
|
60 |
+
"""Deletes an environment variable by its ID."""
|
61 |
+
logs.append(f"ATTEMPT: Deleting variable with ID: {var_id}")
|
62 |
+
api_url = f"https://api.vercel.com/v9/projects/{PROJECT_ID}/env/{var_id}"
|
63 |
+
try:
|
64 |
+
response = requests.delete(api_url, headers=headers, params=params, timeout=10)
|
65 |
+
response.raise_for_status()
|
66 |
+
logs.append(f"✅ SUCCESS: Successfully deleted variable.")
|
67 |
+
return True
|
68 |
+
except requests.exceptions.RequestException as err:
|
69 |
+
logs.append(f"❌ ERROR: Error deleting environment variable: {err}")
|
70 |
+
return False
|
71 |
+
|
72 |
+
def create_env_var(key, value_obj, target_environments, headers, params, logs):
|
73 |
+
"""Creates a new environment variable."""
|
74 |
+
logs.append(f"ATTEMPT: Creating new environment variable '{key}'...")
|
75 |
+
api_url = f"https://api.vercel.com/v9/projects/{PROJECT_ID}/env"
|
76 |
+
payload = {
|
77 |
+
"key": key,
|
78 |
+
"value": json.dumps(value_obj),
|
79 |
+
"type": "encrypted",
|
80 |
+
"target": target_environments,
|
81 |
+
}
|
82 |
+
try:
|
83 |
+
response = requests.post(api_url, headers=headers, json=payload, params=params, timeout=10)
|
84 |
+
response.raise_for_status()
|
85 |
+
logs.append(f"✅ SUCCESS: Successfully created environment variable '{key}'.")
|
86 |
+
return True
|
87 |
+
except requests.exceptions.RequestException as err:
|
88 |
+
logs.append(f"❌ ERROR: HTTP Error creating '{key}': {err}")
|
89 |
+
if err.response:
|
90 |
+
logs.append(f" Response Body: {err.response.text}")
|
91 |
+
return False
|
92 |
+
|
93 |
+
def set_vercel_env_var(key, value_obj, target_environments, logs):
|
94 |
+
"""Orchestrates deleting and recreating a Vercel environment variable."""
|
95 |
+
logs.append(f"--- Processing environment variable: {key} ---")
|
96 |
+
headers = {"Authorization": f"Bearer {VERCEL_TOKEN}"}
|
97 |
+
params = {"teamId": TEAM_ID} if TEAM_ID else {}
|
98 |
+
|
99 |
+
existing_var = get_existing_env_var(key, headers, params, logs)
|
100 |
+
|
101 |
+
if existing_var:
|
102 |
+
if not delete_env_var(existing_var['id'], headers, params, logs):
|
103 |
+
logs.append(f"CRITICAL: Aborting update for '{key}' due to deletion failure.")
|
104 |
+
return False
|
105 |
+
|
106 |
+
return create_env_var(key, value_obj, target_environments, headers, params, logs)
|
107 |
+
|
108 |
+
def get_project_git_info(headers, params, logs):
|
109 |
+
"""Fetches project Git info required for deployment."""
|
110 |
+
logs.append("INFO: Fetching project details to find Git repo ID...")
|
111 |
+
api_url = f"https://api.vercel.com/v9/projects/{PROJECT_ID}"
|
112 |
+
try:
|
113 |
+
response = requests.get(api_url, headers=headers, params=params, timeout=10)
|
114 |
+
response.raise_for_status()
|
115 |
+
project_data = response.json()
|
116 |
+
link_info = project_data.get('link')
|
117 |
+
if not link_info or 'repoId' not in link_info or 'type' not in link_info:
|
118 |
+
logs.append("❌ ERROR: Could not find linked Git repository information (repoId).")
|
119 |
+
logs.append(" Ensure your Vercel project is connected to a Git repository.")
|
120 |
+
return None, None
|
121 |
+
|
122 |
+
repo_id = link_info['repoId']
|
123 |
+
git_type = link_info['type']
|
124 |
+
logs.append(f"✅ SUCCESS: Found Git repo ID: {repo_id} (type: {git_type})")
|
125 |
+
return repo_id, git_type
|
126 |
+
except requests.exceptions.RequestException as err:
|
127 |
+
logs.append(f"❌ ERROR: Error fetching project details: {err}")
|
128 |
+
return None, None
|
129 |
+
|
130 |
+
def trigger_vercel_deployment(logs):
|
131 |
+
"""Triggers a new Vercel deployment."""
|
132 |
+
logs.append("\n--- Triggering new Vercel deployment ---")
|
133 |
+
headers = {"Authorization": f"Bearer {VERCEL_TOKEN}", "Content-Type": "application/json"}
|
134 |
+
params = {"teamId": TEAM_ID} if TEAM_ID else {}
|
135 |
+
|
136 |
+
repo_id, git_type = get_project_git_info(headers, params, logs)
|
137 |
+
if not repo_id:
|
138 |
+
logs.append("CRITICAL: Aborting deployment trigger due to missing Git info.")
|
139 |
+
return False
|
140 |
+
|
141 |
+
api_url = "https://api.vercel.com/v13/deployments"
|
142 |
+
payload = {
|
143 |
+
"name": PROJECT_NAME,
|
144 |
+
"target": "production",
|
145 |
+
"gitSource": {
|
146 |
+
"type": git_type,
|
147 |
+
"repoId": repo_id,
|
148 |
+
"ref": "main" # Change 'main' to your default branch if it's different
|
149 |
+
}
|
150 |
+
}
|
151 |
+
|
152 |
+
try:
|
153 |
+
response = requests.post(api_url, headers=headers, json=payload, params=params, timeout=15)
|
154 |
+
response.raise_for_status()
|
155 |
+
deployment_url = response.json().get('url')
|
156 |
+
logs.append(f"✅✅✅ SUCCESS: Successfully triggered new deployment!")
|
157 |
+
logs.append(f" Inspect deployment status at: https://{deployment_url}")
|
158 |
+
return True
|
159 |
+
except requests.exceptions.HTTPError as err:
|
160 |
+
logs.append(f"❌ ERROR: HTTP Error triggering deployment: {err}")
|
161 |
+
logs.append(f" Response Body: {err.response.text}")
|
162 |
+
return False
|
163 |
+
except Exception as err:
|
164 |
+
logs.append(f"❌ ERROR: An unexpected error occurred while triggering deployment: {err}")
|
165 |
+
return False
|
166 |
+
|
167 |
+
|
168 |
+
# --- NEW ENDPOINT AND UI FOR HOMEPAGE CONTENT ---
|
169 |
+
|
170 |
+
@page_bp.route('/edit_homepage', methods=['GET', 'POST'])
|
171 |
+
def edit_homepage():
|
172 |
+
"""
|
173 |
+
Provides a UI to edit homepage content (FAQs, Why Us) and deploy changes to Vercel.
|
174 |
+
"""
|
175 |
+
logs = []
|
176 |
+
|
177 |
+
# --- POST Request: Handle form submission ---
|
178 |
+
if request.method == 'POST':
|
179 |
+
# The form data for 'why_us_features' and 'faqs' is now assembled by client-side JavaScript
|
180 |
+
# into JSON strings, so the backend logic can remain the same.
|
181 |
+
why_us_json_str = request.form.get('why_us_features')
|
182 |
+
faqs_json_str = request.form.get('faqs')
|
183 |
+
|
184 |
+
try:
|
185 |
+
# Validate and parse the submitted JSON
|
186 |
+
why_us_data = json.loads(why_us_json_str)
|
187 |
+
faqs_data = json.loads(faqs_json_str)
|
188 |
+
except json.JSONDecodeError as e:
|
189 |
+
# If JSON is invalid, render the result page with an error
|
190 |
+
logs.append(f"❌ CRITICAL ERROR: Invalid JSON format submitted. Please correct and try again.")
|
191 |
+
logs.append(f" Details: {e}")
|
192 |
+
return render_template_string(RESULT_PAGE_TEMPLATE, logs="\n".join(logs), status_class="error")
|
193 |
+
|
194 |
+
target_environments = ["production", "preview", "development"]
|
195 |
+
|
196 |
+
# Update the environment variables on Vercel
|
197 |
+
success1 = set_vercel_env_var("REACT_APP_WHY_US_FEATURES", why_us_data, target_environments, logs)
|
198 |
+
logs.append("\n")
|
199 |
+
success2 = set_vercel_env_var("REACT_APP_FAQS", faqs_data, target_environments, logs)
|
200 |
+
|
201 |
+
# If variable updates were successful, trigger deployment
|
202 |
+
if success1 and success2:
|
203 |
+
trigger_vercel_deployment(logs)
|
204 |
+
else:
|
205 |
+
logs.append("\nSkipping deployment due to errors in updating environment variables.")
|
206 |
+
|
207 |
+
status_class = "success" if "✅✅✅ SUCCESS" in "".join(logs) else "error"
|
208 |
+
return render_template_string(RESULT_PAGE_TEMPLATE, logs="\n".join(logs), status_class=status_class)
|
209 |
+
|
210 |
+
# --- GET Request: Display the editor UI ---
|
211 |
+
else:
|
212 |
+
# Fetch current data from Vercel to populate the editor
|
213 |
+
headers = {"Authorization": f"Bearer {VERCEL_TOKEN}"}
|
214 |
+
params = {"teamId": TEAM_ID} if TEAM_ID else {}
|
215 |
+
|
216 |
+
# Fetch "Why Us" data
|
217 |
+
why_us_var = get_existing_env_var("REACT_APP_WHY_US_FEATURES", headers, params, [])
|
218 |
+
if why_us_var and 'value' in why_us_var:
|
219 |
+
try:
|
220 |
+
why_us_data = json.loads(why_us_var['value'])
|
221 |
+
except json.JSONDecodeError:
|
222 |
+
why_us_data = DEFAULT_WHY_US_FEATURES_DATA
|
223 |
+
else:
|
224 |
+
why_us_data = DEFAULT_WHY_US_FEATURES_DATA
|
225 |
+
|
226 |
+
# Fetch "FAQs" data
|
227 |
+
faqs_var = get_existing_env_var("REACT_APP_FAQS", headers, params, [])
|
228 |
+
if faqs_var and 'value' in faqs_var:
|
229 |
+
try:
|
230 |
+
faqs_data = json.loads(faqs_var['value'])
|
231 |
+
except json.JSONDecodeError:
|
232 |
+
faqs_data = DEFAULT_FAQS_DATA
|
233 |
+
else:
|
234 |
+
faqs_data = DEFAULT_FAQS_DATA
|
235 |
+
|
236 |
+
# Pass the Python objects directly to the template for the interactive UI
|
237 |
+
return render_template_string(
|
238 |
+
EDITOR_UI_TEMPLATE,
|
239 |
+
why_us_data=why_us_data,
|
240 |
+
faqs_data=faqs_data
|
241 |
+
)
|
242 |
+
|
243 |
+
# --- HTML Templates for the new endpoint ---
|
244 |
+
|
245 |
+
EDITOR_UI_TEMPLATE = """
|
246 |
+
<!DOCTYPE html>
|
247 |
+
<html lang="en">
|
248 |
+
<head>
|
249 |
+
<meta charset="UTF-8">
|
250 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
251 |
+
<title>Edit Homepage Content & Deploy</title>
|
252 |
+
<style>
|
253 |
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; background-color: #f8f9fa; color: #212529; }
|
254 |
+
.container { max-width: 960px; margin: 2em auto; padding: 0 1em; }
|
255 |
+
h1, h2 { color: #343a40; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5em; margin-top: 1.5em;}
|
256 |
+
form { background: white; padding: 2em; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
257 |
+
|
258 |
+
/* New Interactive Editor Styles */
|
259 |
+
.editor-section { margin-bottom: 2.5em; }
|
260 |
+
.editor-item {
|
261 |
+
background-color: #f8f9fa;
|
262 |
+
border: 1px solid #dee2e6;
|
263 |
+
border-radius: 6px;
|
264 |
+
padding: 1.5em;
|
265 |
+
margin-bottom: 1em;
|
266 |
+
position: relative;
|
267 |
+
}
|
268 |
+
.input-group { margin-bottom: 1em; }
|
269 |
+
.input-group:last-child { margin-bottom: 0; }
|
270 |
+
.input-group label {
|
271 |
+
display: block;
|
272 |
+
margin-bottom: 0.5em;
|
273 |
+
font-weight: bold;
|
274 |
+
color: #495057;
|
275 |
+
font-size: 0.9em;
|
276 |
+
}
|
277 |
+
.input-group input[type="text"], .input-group textarea {
|
278 |
+
width: 100%;
|
279 |
+
padding: 0.8em;
|
280 |
+
border: 1px solid #ced4da;
|
281 |
+
border-radius: 4px;
|
282 |
+
box-sizing: border-box;
|
283 |
+
font-size: 0.95rem;
|
284 |
+
font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", monospace;
|
285 |
+
}
|
286 |
+
.input-group textarea {
|
287 |
+
min-height: 100px;
|
288 |
+
resize: vertical;
|
289 |
+
}
|
290 |
+
.remove-btn {
|
291 |
+
position: absolute;
|
292 |
+
top: 10px;
|
293 |
+
right: 10px;
|
294 |
+
background-color: #dc3545;
|
295 |
+
color: white;
|
296 |
+
border: none;
|
297 |
+
border-radius: 50%;
|
298 |
+
width: 24px;
|
299 |
+
height: 24px;
|
300 |
+
font-size: 16px;
|
301 |
+
line-height: 24px;
|
302 |
+
text-align: center;
|
303 |
+
cursor: pointer;
|
304 |
+
font-weight: bold;
|
305 |
+
}
|
306 |
+
.remove-btn:hover { background-color: #c82333; }
|
307 |
+
.add-btn {
|
308 |
+
background-color: #007bff;
|
309 |
+
color: white;
|
310 |
+
padding: 0.6em 1.2em;
|
311 |
+
border: none;
|
312 |
+
border-radius: 4px;
|
313 |
+
cursor: pointer;
|
314 |
+
font-size: 1em;
|
315 |
+
font-weight: bold;
|
316 |
+
display: inline-block;
|
317 |
+
margin-top: 0.5em;
|
318 |
+
}
|
319 |
+
.add-btn:hover { background-color: #0056b3; }
|
320 |
+
|
321 |
+
.deploy-button { background-color: #28a745; color: white; padding: 0.8em 1.5em; border: none; border-radius: 4px; cursor: pointer; font-size: 1.2em; font-weight: bold; display: block; width: 100%; margin-top: 2em; transition: background-color 0.2s; }
|
322 |
+
.deploy-button:hover { background-color: #218838; }
|
323 |
+
</style>
|
324 |
+
</head>
|
325 |
+
<body>
|
326 |
+
<div class="container">
|
327 |
+
<h1>Edit Homepage Content & Deploy</h1>
|
328 |
+
<form id="main-form" action="/api/pages/edit_homepage" method="post">
|
329 |
+
|
330 |
+
<div class="editor-section">
|
331 |
+
<h2>'Why Us' Features</h2>
|
332 |
+
<div id="why-us-editor">
|
333 |
+
{% for feature in why_us_data %}
|
334 |
+
<div class="editor-item why-us-item">
|
335 |
+
<button type="button" class="remove-btn" title="Remove Feature">×</button>
|
336 |
+
<div class="input-group">
|
337 |
+
<label>Icon Name (e.g., EnergySavingsLeafIcon)</label>
|
338 |
+
<input type="text" class="why-us-icon" value="{{ feature.icon | e }}">
|
339 |
+
</div>
|
340 |
+
<div class="input-group">
|
341 |
+
<label>Title</label>
|
342 |
+
<input type="text" class="why-us-title" value="{{ feature.title | e }}">
|
343 |
+
</div>
|
344 |
+
<div class="input-group">
|
345 |
+
<label>Description</label>
|
346 |
+
<input type="text" class="why-us-desc" value="{{ feature.description | e }}">
|
347 |
+
</div>
|
348 |
+
</div>
|
349 |
+
{% endfor %}
|
350 |
+
</div>
|
351 |
+
<button type="button" id="add-why-us-btn" class="add-btn">+ Add Feature</button>
|
352 |
+
<textarea id="why_us_features" name="why_us_features" style="display: none;"></textarea>
|
353 |
+
</div>
|
354 |
+
|
355 |
+
<div class="editor-section">
|
356 |
+
<h2>FAQs</h2>
|
357 |
+
<div id="faq-editor">
|
358 |
+
{% for faq in faqs_data %}
|
359 |
+
<div class="editor-item faq-item">
|
360 |
+
<button type="button" class="remove-btn" title="Remove FAQ">×</button>
|
361 |
+
<div class="input-group">
|
362 |
+
<label>Question</label>
|
363 |
+
<input type="text" class="faq-question" value="{{ faq.question | e }}">
|
364 |
+
</div>
|
365 |
+
<div class="input-group">
|
366 |
+
<label>Answer</label>
|
367 |
+
<textarea class="faq-answer">{{ faq.answer }}</textarea>
|
368 |
+
</div>
|
369 |
+
</div>
|
370 |
+
{% endfor %}
|
371 |
+
</div>
|
372 |
+
<button type="button" id="add-faq-btn" class="add-btn">+ Add FAQ</button>
|
373 |
+
<textarea id="faqs" name="faqs" style="display: none;"></textarea>
|
374 |
+
</div>
|
375 |
+
|
376 |
+
<button type="submit" class="deploy-button">Save and Deploy to Vercel</button>
|
377 |
+
</form>
|
378 |
+
</div>
|
379 |
+
|
380 |
+
<script>
|
381 |
+
document.addEventListener('DOMContentLoaded', function() {
|
382 |
+
|
383 |
+
// --- 'Why Us' Section Logic ---
|
384 |
+
|
385 |
+
const whyUsEditor = document.getElementById('why-us-editor');
|
386 |
+
const addWhyUsBtn = document.getElementById('add-why-us-btn');
|
387 |
+
|
388 |
+
// Add new 'Why Us' feature
|
389 |
+
addWhyUsBtn.addEventListener('click', () => {
|
390 |
+
const newItem = document.createElement('div');
|
391 |
+
newItem.className = 'editor-item why-us-item';
|
392 |
+
newItem.innerHTML = `
|
393 |
+
<button type="button" class="remove-btn" title="Remove Feature">×</button>
|
394 |
+
<div class="input-group">
|
395 |
+
<label>Icon Name (e.g., EnergySavingsLeafIcon)</label>
|
396 |
+
<input type="text" class="why-us-icon" value="">
|
397 |
+
</div>
|
398 |
+
<div class="input-group">
|
399 |
+
<label>Title</label>
|
400 |
+
<input type="text" class="why-us-title" value="">
|
401 |
+
</div>
|
402 |
+
<div class="input-group">
|
403 |
+
<label>Description</label>
|
404 |
+
<input type="text" class="why-us-desc" value="">
|
405 |
+
</div>
|
406 |
+
`;
|
407 |
+
whyUsEditor.appendChild(newItem);
|
408 |
+
});
|
409 |
+
|
410 |
+
// Remove 'Why Us' feature (using event delegation)
|
411 |
+
whyUsEditor.addEventListener('click', (e) => {
|
412 |
+
if (e.target && e.target.classList.contains('remove-btn')) {
|
413 |
+
e.target.closest('.editor-item').remove();
|
414 |
+
}
|
415 |
+
});
|
416 |
+
|
417 |
+
|
418 |
+
// --- FAQ Section Logic ---
|
419 |
+
|
420 |
+
const faqEditor = document.getElementById('faq-editor');
|
421 |
+
const addFaqBtn = document.getElementById('add-faq-btn');
|
422 |
+
|
423 |
+
// Add new FAQ
|
424 |
+
addFaqBtn.addEventListener('click', () => {
|
425 |
+
const newItem = document.createElement('div');
|
426 |
+
newItem.className = 'editor-item faq-item';
|
427 |
+
newItem.innerHTML = `
|
428 |
+
<button type="button" class="remove-btn" title="Remove FAQ">×</button>
|
429 |
+
<div class="input-group">
|
430 |
+
<label>Question</label>
|
431 |
+
<input type="text" class="faq-question" value="">
|
432 |
+
</div>
|
433 |
+
<div class="input-group">
|
434 |
+
<label>Answer</label>
|
435 |
+
<textarea class="faq-answer"></textarea>
|
436 |
+
</div>
|
437 |
+
`;
|
438 |
+
faqEditor.appendChild(newItem);
|
439 |
+
});
|
440 |
+
|
441 |
+
// Remove FAQ (using event delegation)
|
442 |
+
faqEditor.addEventListener('click', (e) => {
|
443 |
+
if (e.target && e.target.classList.contains('remove-btn')) {
|
444 |
+
e.target.closest('.editor-item').remove();
|
445 |
+
}
|
446 |
+
});
|
447 |
+
|
448 |
+
|
449 |
+
// --- Form Submission Logic ---
|
450 |
+
|
451 |
+
const mainForm = document.getElementById('main-form');
|
452 |
+
mainForm.addEventListener('submit', (e) => {
|
453 |
+
// Serialize 'Why Us' data into hidden textarea
|
454 |
+
const whyUsData = [];
|
455 |
+
document.querySelectorAll('.why-us-item').forEach(item => {
|
456 |
+
const icon = item.querySelector('.why-us-icon').value.trim();
|
457 |
+
const title = item.querySelector('.why-us-title').value.trim();
|
458 |
+
const description = item.querySelector('.why-us-desc').value.trim();
|
459 |
+
if (icon && title && description) {
|
460 |
+
whyUsData.push({ icon, title, description });
|
461 |
+
}
|
462 |
+
});
|
463 |
+
document.getElementById('why_us_features').value = JSON.stringify(whyUsData, null, 2);
|
464 |
+
|
465 |
+
// Serialize FAQ data into hidden textarea
|
466 |
+
const faqsData = [];
|
467 |
+
document.querySelectorAll('.faq-item').forEach(item => {
|
468 |
+
const question = item.querySelector('.faq-question').value.trim();
|
469 |
+
const answer = item.querySelector('.faq-answer').value.trim();
|
470 |
+
if (question && answer) {
|
471 |
+
faqsData.push({ question, answer });
|
472 |
+
}
|
473 |
+
});
|
474 |
+
document.getElementById('faqs').value = JSON.stringify(faqsData, null, 2);
|
475 |
+
|
476 |
+
// Quick validation to prevent submitting empty arrays
|
477 |
+
if (document.querySelectorAll('.why-us-item').length > 0 && whyUsData.length === 0) {
|
478 |
+
alert('A "Why Us" feature has empty fields. Please fill them out or remove the item.');
|
479 |
+
e.preventDefault(); // Stop form submission
|
480 |
+
return;
|
481 |
+
}
|
482 |
+
if (document.querySelectorAll('.faq-item').length > 0 && faqsData.length === 0) {
|
483 |
+
alert('An FAQ has empty fields. Please fill them out or remove the item.');
|
484 |
+
e.preventDefault(); // Stop form submission
|
485 |
+
return;
|
486 |
+
}
|
487 |
+
|
488 |
+
});
|
489 |
+
|
490 |
+
});
|
491 |
+
</script>
|
492 |
+
|
493 |
+
</body>
|
494 |
+
</html>
|
495 |
+
"""
|
496 |
+
|
497 |
+
RESULT_PAGE_TEMPLATE = """
|
498 |
+
<!DOCTYPE html>
|
499 |
+
<html lang="en">
|
500 |
+
<head>
|
501 |
+
<meta charset="UTF-8">
|
502 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
503 |
+
<title>Deployment Status</title>
|
504 |
+
<style>
|
505 |
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; background-color: #f8f9fa; color: #212529; }
|
506 |
+
.container { max-width: 960px; margin: 2em auto; padding: 2em; background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
507 |
+
h1 { color: #343a40; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5em; margin-bottom: 1em; }
|
508 |
+
h1.success { color: #28a745; }
|
509 |
+
h1.error { color: #dc3545; }
|
510 |
+
pre { background-color: #e9ecef; color: #495057; padding: 1.5em; border-radius: 4px; white-space: pre-wrap; word-wrap: break-word; font-size: 0.9rem; line-height: 1.6; }
|
511 |
+
.back-link { display: inline-block; margin-top: 1.5em; background-color: #007bff; color: white; padding: 0.6em 1.2em; border-radius: 4px; text-decoration: none; font-weight: bold; }
|
512 |
+
.back-link:hover { background-color: #0056b3; }
|
513 |
+
</style>
|
514 |
+
</head>
|
515 |
+
<body>
|
516 |
+
<div class="container">
|
517 |
+
<h1 class="{{ status_class }}">Deployment Process Log</h1>
|
518 |
+
<pre>{{ logs }}</pre>
|
519 |
+
<a href="/api/pages/edit_homepage" class="back-link">← Back to Editor</a>
|
520 |
+
</div>
|
521 |
+
</body>
|
522 |
+
</html>
|
523 |
+
"""
|
524 |
+
|
525 |
+
|
526 |
+
# --- PRE-EXISTING CODE (UNCHANGED) ---
|
527 |
+
|
528 |
# Default data for initialization if pages don't exist in the database
|
529 |
DEFAULT_ABOUT_DATA = {
|
530 |
"_id": "about",
|
|
|
595 |
]
|
596 |
}
|
597 |
else:
|
598 |
+
return redirect('/api/pages/update')
|
599 |
|
600 |
mongo.db.pages.update_one(
|
601 |
{'_id': page_name},
|
602 |
{'$set': update_data},
|
603 |
upsert=True
|
604 |
)
|
605 |
+
return redirect('/api/pages/update_ui') # Corrected redirect to the UI page
|
606 |
|
607 |
@page_bp.route('/update_ui', methods=['GET'])
|
608 |
def update_ui():
|
609 |
"""Serves the simple HTML UI for editing page content."""
|
|
|
610 |
about_data = mongo.db.pages.find_one({'_id': 'about'}) or DEFAULT_ABOUT_DATA
|
611 |
contact_data = mongo.db.pages.find_one({'_id': 'contact'}) or DEFAULT_CONTACT_DATA
|
612 |
|
app/templates/base.html
CHANGED
@@ -2,15 +2,95 @@
|
|
2 |
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
</head>
|
7 |
-
<body>
|
8 |
-
|
9 |
-
|
10 |
-
<
|
11 |
-
|
12 |
-
<
|
13 |
-
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
</body>
|
16 |
</html>
|
|
|
2 |
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>{% if title %}{{ title }}{% else %}Welcome to Xero Python OAuth Starter{% endif %}</title>
|
7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
+
<style>
|
9 |
+
body {
|
10 |
+
font-family: 'Inter', sans-serif;
|
11 |
+
}
|
12 |
+
.link-card {
|
13 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
14 |
+
}
|
15 |
+
.link-card:hover {
|
16 |
+
transform: translateY(-4px);
|
17 |
+
box-shadow: 0 8px 20px rgba(0,0,0,0.08);
|
18 |
+
}
|
19 |
+
.code-block {
|
20 |
+
background: #f8fafc;
|
21 |
+
border: 1px solid #e2e8f0;
|
22 |
+
border-radius: 0.75rem;
|
23 |
+
padding: 1rem;
|
24 |
+
font-family: 'Fira Code', monospace;
|
25 |
+
font-size: 0.9rem;
|
26 |
+
color: #334155;
|
27 |
+
overflow-x: auto;
|
28 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
29 |
+
}
|
30 |
+
.code-block pre {
|
31 |
+
margin: 0;
|
32 |
+
}
|
33 |
+
</style>
|
34 |
</head>
|
35 |
+
<body class="bg-white text-gray-800">
|
36 |
+
|
37 |
+
<!-- Navbar -->
|
38 |
+
<nav class="bg-white shadow-sm fixed top-0 left-0 w-full z-10">
|
39 |
+
<div class="max-w-6xl mx-auto px-6 py-4 flex justify-between items-center">
|
40 |
+
<h1 class="text-2xl font-bold text-gray-900">Matax Express Admin Panel</h1>
|
41 |
+
<div class="space-x-6 text-gray-600">
|
42 |
+
<a href="{{ url_for('xero.logs') }}" class="hover:text-blue-600 transition">Index</a>
|
43 |
+
<a href="{{ url_for('xero.login') }}" class="hover:text-blue-600 transition">Login</a>
|
44 |
+
<a href="{{ url_for('xero.logout') }}" class="hover:text-blue-600 transition">Logout</a>
|
45 |
+
</div>
|
46 |
+
</div>
|
47 |
+
</nav>
|
48 |
+
|
49 |
+
<!-- Content -->
|
50 |
+
<div class="max-w-5xl mx-auto px-6 pt-28 pb-10">
|
51 |
+
<h2 class="text-3xl font-bold mb-8 text-gray-900">Quick Actions</h2>
|
52 |
+
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
53 |
+
|
54 |
+
<a href="{{ url_for('xero.fetch_inventory') }}" class="link-card bg-white p-6 rounded-2xl shadow-sm border border-gray-100 hover:border-blue-200">
|
55 |
+
<h3 class="text-lg font-semibold text-gray-900 mb-2">Sync Xero Inventory</h3>
|
56 |
+
<p class="text-gray-500 text-sm">Update your website inventory directly from Xero in one click.</p>
|
57 |
+
</a>
|
58 |
+
|
59 |
+
<a href="/api/edit_inventory" class="link-card bg-white p-6 rounded-2xl shadow-sm border border-gray-100 hover:border-blue-200">
|
60 |
+
<h3 class="text-lg font-semibold text-gray-900 mb-2">Edit Product Images</h3>
|
61 |
+
<p class="text-gray-500 text-sm">Manage and update product catalog images effortlessly.</p>
|
62 |
+
</a>
|
63 |
+
|
64 |
+
<a href="/api/sync_xero_users" class="link-card bg-white p-6 rounded-2xl shadow-sm border border-gray-100 hover:border-blue-200">
|
65 |
+
<h3 class="text-lg font-semibold text-gray-900 mb-2">Sync Xero Users</h3>
|
66 |
+
<p class="text-gray-500 text-sm">Keep your approved Xero users up to date in the database.</p>
|
67 |
+
</a>
|
68 |
+
|
69 |
+
<a href="/api/sendmail" class="link-card bg-white p-6 rounded-2xl shadow-sm border border-gray-100 hover:border-blue-200">
|
70 |
+
<h3 class="text-lg font-semibold text-gray-900 mb-2">Send Reminder Emails</h3>
|
71 |
+
<p class="text-gray-500 text-sm">Notify users with pending items in their cart.</p>
|
72 |
+
</a>
|
73 |
+
|
74 |
+
<a href="/api/pages/update_ui" class="link-card bg-white p-6 rounded-2xl shadow-sm border border-gray-100 hover:border-blue-200">
|
75 |
+
<h3 class="text-lg font-semibold text-gray-900 mb-2">Update Pages UI</h3>
|
76 |
+
<p class="text-gray-500 text-sm">Revamp the look of the Contact and About Us pages.</p>
|
77 |
+
</a>
|
78 |
+
|
79 |
+
<a href="/api/pages/edit_homepage" class="link-card bg-white p-6 rounded-2xl shadow-sm border border-gray-100 hover:border-blue-200">
|
80 |
+
<h3 class="text-lg font-semibold text-gray-900 mb-2">Update Homepage UI</h3>
|
81 |
+
<p class="text-gray-500 text-sm">Edit the Faq and Why choose us page.This may take some time to take effect</p>
|
82 |
+
</a>
|
83 |
+
</div>
|
84 |
+
|
85 |
+
<!-- Beautiful Code Block for Block Content -->
|
86 |
+
<div class="mt-12">
|
87 |
+
<h3 class="text-xl font-bold text-gray-800 mb-4">Output</h3>
|
88 |
+
<div class="code-block">
|
89 |
+
<pre>{% block content %}{% endblock %}</pre>
|
90 |
+
</div>
|
91 |
+
</div>
|
92 |
+
|
93 |
+
</div>
|
94 |
+
|
95 |
</body>
|
96 |
</html>
|
app/templates/edit_inventory.html
ADDED
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<title>{{ title }}</title>
|
6 |
+
<style>
|
7 |
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 2em; background-color: #f8f9fa; color: #333; }
|
8 |
+
.container { max-width: 700px; margin: auto; background-color: #fff; padding: 2em; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
|
9 |
+
h1 { color: #005c9e; }
|
10 |
+
.form-group { margin-bottom: 1.5em; }
|
11 |
+
label { display: block; margin-bottom: 0.5em; font-weight: bold; }
|
12 |
+
input[type="url"], select { width: 100%; padding: 0.8em; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
|
13 |
+
.btn { display: inline-block; padding: 0.8em 1.5em; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; text-align: center; font-size: 1em; }
|
14 |
+
.btn:hover { background-color: #0056b3; }
|
15 |
+
.alert { padding: 1em; margin-bottom: 1em; border-radius: 5px; border: 1px solid transparent; }
|
16 |
+
.alert-success { background-color: #d4edda; color: #155724; border-color: #c3e6cb; }
|
17 |
+
.alert-error { background-color: #f8d7da; color: #721c24; border-color: #f5c6cb; }
|
18 |
+
#image_preview { max-width: 200px; max-height: 200px; margin-top: 1em; border-radius: 4px; border: 1px solid #ddd; }
|
19 |
+
</style>
|
20 |
+
</head>
|
21 |
+
<body>
|
22 |
+
<div class="container">
|
23 |
+
<h1>{{ title }}</h1>
|
24 |
+
|
25 |
+
<!-- Display flashed messages -->
|
26 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
27 |
+
{% if messages %}
|
28 |
+
{% for category, message in messages %}
|
29 |
+
<div class="alert alert-{{ category }}">{{ message }}</div>
|
30 |
+
{% endfor %}
|
31 |
+
{% endif %}
|
32 |
+
{% endwith %}
|
33 |
+
|
34 |
+
<form action="{{ url_for('xero.edit_inventory') }}" method="post">
|
35 |
+
<div class="form-group">
|
36 |
+
<label for="product_code">Select Product:</label>
|
37 |
+
<select name="product_code" id="product_code" required>
|
38 |
+
<option value="">-- Please choose a product --</option>
|
39 |
+
{% for product in products %}
|
40 |
+
<option value="{{ product.code }}">{{ product.name }} (Code: {{ product.code }})</option>
|
41 |
+
{% endfor %}
|
42 |
+
</select>
|
43 |
+
</div>
|
44 |
+
<div class="form-group">
|
45 |
+
<label for="image_url">New Image URL:</label>
|
46 |
+
<input type="url" name="image_url" id="image_url" placeholder="https://example.com/new_image.jpg" required>
|
47 |
+
</div>
|
48 |
+
<div class="form-group">
|
49 |
+
<p><strong>Current Image:</strong> <span id="current_url_display">N/A</span></p>
|
50 |
+
<img id="image_preview" src="" alt="Image Preview" style="display: none;">
|
51 |
+
</div>
|
52 |
+
<button type="submit" class="btn">Update Image</button>
|
53 |
+
</form>
|
54 |
+
</div>
|
55 |
+
|
56 |
+
<script>
|
57 |
+
// Safely pass product data from Flask to JavaScript
|
58 |
+
const products = {{ products|tojson }};
|
59 |
+
const productSelect = document.getElementById('product_code');
|
60 |
+
const currentUrlSpan = document.getElementById('current_url_display');
|
61 |
+
const imagePreview = document.getElementById('image_preview');
|
62 |
+
|
63 |
+
productSelect.addEventListener('change', (event) => {
|
64 |
+
const selectedCode = event.target.value;
|
65 |
+
// Reset if no product is selected
|
66 |
+
if (!selectedCode) {
|
67 |
+
currentUrlSpan.textContent = 'N/A';
|
68 |
+
imagePreview.style.display = 'none';
|
69 |
+
imagePreview.src = '';
|
70 |
+
return;
|
71 |
+
}
|
72 |
+
// Find the selected product from our data
|
73 |
+
const selectedProduct = products.find(p => p.code === selectedCode);
|
74 |
+
if (selectedProduct && selectedProduct.image_url) {
|
75 |
+
currentUrlSpan.textContent = selectedProduct.image_url;
|
76 |
+
imagePreview.src = selectedProduct.image_url;
|
77 |
+
imagePreview.style.display = 'block';
|
78 |
+
} else {
|
79 |
+
currentUrlSpan.textContent = 'No image URL set.';
|
80 |
+
imagePreview.style.display = 'none';
|
81 |
+
imagePreview.src = '';
|
82 |
+
}
|
83 |
+
});
|
84 |
+
</script>
|
85 |
+
</body>
|
86 |
+
</html>
|
app/xero_routes.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
import json
|
2 |
import logging
|
3 |
-
from flask import Blueprint, render_template, redirect, url_for, jsonify, request
|
4 |
from pymongo import UpdateOne
|
5 |
from xero_python.accounting import AccountingApi
|
6 |
from utils import jsonify as jsonify_xero
|
@@ -12,8 +12,8 @@ xero_bp = Blueprint('xero', __name__)
|
|
12 |
logger = logging.getLogger(__name__)
|
13 |
|
14 |
|
15 |
-
@xero_bp.route("/")
|
16 |
-
def
|
17 |
xero_access = dict(obtain_xero_oauth2_token() or {})
|
18 |
return render_template(
|
19 |
"code.html",
|
@@ -21,6 +21,15 @@ def index():
|
|
21 |
code=json.dumps(xero_access, sort_keys=True, indent=4),
|
22 |
)
|
23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
@xero_bp.route("/login")
|
25 |
def login():
|
26 |
redirect_url = url_for("xero.oauth_callback", _external=True)
|
@@ -51,13 +60,13 @@ def fetch_inventory():
|
|
51 |
try:
|
52 |
xero_tenant_id = get_xero_tenant_id()
|
53 |
accounting_api = AccountingApi(api_client)
|
54 |
-
|
55 |
xero_items = accounting_api.get_items(xero_tenant_id=xero_tenant_id).items
|
56 |
fetched_products = [{
|
57 |
"code": item.code, "name": item.name,
|
58 |
-
"price": float(item.sales_details.unit_price) if item.sales_details else 0.0,
|
59 |
"unit": item.description, "on_hand": item.quantity_on_hand if item.quantity_on_hand else 1,
|
60 |
} for item in xero_items]
|
|
|
61 |
|
62 |
# Use mongo.db directly
|
63 |
db_products = list(mongo.db.products.find({}))
|
@@ -102,3 +111,41 @@ def fetch_inventory():
|
|
102 |
|
103 |
return render_template("code.html", title="Inventory Sync", sub_title=sub_title, code=code_to_display)
|
104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import json
|
2 |
import logging
|
3 |
+
from flask import Blueprint, render_template, redirect, url_for, jsonify, request,flash
|
4 |
from pymongo import UpdateOne
|
5 |
from xero_python.accounting import AccountingApi
|
6 |
from utils import jsonify as jsonify_xero
|
|
|
12 |
logger = logging.getLogger(__name__)
|
13 |
|
14 |
|
15 |
+
@xero_bp.route("/logs")
|
16 |
+
def logs():
|
17 |
xero_access = dict(obtain_xero_oauth2_token() or {})
|
18 |
return render_template(
|
19 |
"code.html",
|
|
|
21 |
code=json.dumps(xero_access, sort_keys=True, indent=4),
|
22 |
)
|
23 |
|
24 |
+
@xero_bp.route("/")
|
25 |
+
def index():
|
26 |
+
xero_access = dict(obtain_xero_oauth2_token() or {})
|
27 |
+
return render_template(
|
28 |
+
"code.html",
|
29 |
+
title="Home",
|
30 |
+
code="",
|
31 |
+
)
|
32 |
+
|
33 |
@xero_bp.route("/login")
|
34 |
def login():
|
35 |
redirect_url = url_for("xero.oauth_callback", _external=True)
|
|
|
60 |
try:
|
61 |
xero_tenant_id = get_xero_tenant_id()
|
62 |
accounting_api = AccountingApi(api_client)
|
|
|
63 |
xero_items = accounting_api.get_items(xero_tenant_id=xero_tenant_id).items
|
64 |
fetched_products = [{
|
65 |
"code": item.code, "name": item.name,
|
66 |
+
"price": float(item.sales_details.unit_price) if item.sales_details.unit_price else 0.0,
|
67 |
"unit": item.description, "on_hand": item.quantity_on_hand if item.quantity_on_hand else 1,
|
68 |
} for item in xero_items]
|
69 |
+
print(fetched_products)
|
70 |
|
71 |
# Use mongo.db directly
|
72 |
db_products = list(mongo.db.products.find({}))
|
|
|
111 |
|
112 |
return render_template("code.html", title="Inventory Sync", sub_title=sub_title, code=code_to_display)
|
113 |
|
114 |
+
@xero_bp.route("/api/edit_inventory", methods=["GET", "POST"])
|
115 |
+
def edit_inventory():
|
116 |
+
"""
|
117 |
+
Provides a UI to edit the image_url for inventory items.
|
118 |
+
On GET, it displays a form with all products.
|
119 |
+
On POST, it updates the product's image URL in the database.
|
120 |
+
"""
|
121 |
+
# Note: For the flash() functionality to work, you must have a SECRET_KEY configured for your Flask app.
|
122 |
+
# For example: app.config['SECRET_KEY'] = 'a-secret-key'
|
123 |
+
|
124 |
+
if request.method == "POST":
|
125 |
+
product_code = request.form.get("product_code")
|
126 |
+
new_image_url = request.form.get("image_url")
|
127 |
+
|
128 |
+
if not product_code or not new_image_url:
|
129 |
+
flash("Product and a new image URL are required.", "error")
|
130 |
+
return redirect(url_for("xero.edit_inventory"))
|
131 |
+
|
132 |
+
# Update the document in MongoDB
|
133 |
+
# Use mongo.db directly
|
134 |
+
result = mongo.db.products.update_one(
|
135 |
+
{"code": product_code},
|
136 |
+
{"$set": {"image_url": new_image_url}}
|
137 |
+
)
|
138 |
+
|
139 |
+
if result.matched_count:
|
140 |
+
flash(f"Image for product '{product_code}' updated successfully!", "success")
|
141 |
+
else:
|
142 |
+
flash(f"Product with code '{product_code}' not found.", "error")
|
143 |
+
|
144 |
+
return redirect(url_for("xero.edit_inventory"))
|
145 |
+
|
146 |
+
# For a GET request, fetch all products and render the edit page
|
147 |
+
# Fetch only the necessary fields to populate the form and sort by name
|
148 |
+
products = list(mongo.db.products.find({}, {"_id": 0, "code": 1, "name": 1, "image_url": 1}).sort("name", 1))
|
149 |
+
return render_template("edit_inventory.html", title="Edit Inventory Image", products=products)
|
150 |
+
|
151 |
+
# --- NEW FEATURE CODE ENDS HERE ---
|
app/xero_utils.py
CHANGED
@@ -295,10 +295,13 @@ def create_xero_purchase_order_async(app_context, xero_tenant_id, order_details)
|
|
295 |
if product:
|
296 |
product_name = product.get('name', 'N/A')
|
297 |
item_code = xero_item_map.get(product_name, "")
|
|
|
|
|
|
|
298 |
line_items.append(
|
299 |
LineItem(
|
300 |
item_code=item_code,
|
301 |
-
description=
|
302 |
quantity=item['quantity'],
|
303 |
unit_amount=float(product.get('price', 0))
|
304 |
)
|
|
|
295 |
if product:
|
296 |
product_name = product.get('name', 'N/A')
|
297 |
item_code = xero_item_map.get(product_name, "")
|
298 |
+
unitss=item["mode"]
|
299 |
+
if unitss == "weight":
|
300 |
+
unitss = "lb"
|
301 |
line_items.append(
|
302 |
LineItem(
|
303 |
item_code=item_code,
|
304 |
+
description=str(item['quantity']) + " " + unitss + " of " + product_name,
|
305 |
quantity=item['quantity'],
|
306 |
unit_amount=float(product.get('price', 0))
|
307 |
)
|
env_test.py
ADDED
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
VERCEL_TOKEN = "azm0JC5lyPwah6qPGhbN5DL5"
|
2 |
+
PROJECT_ID = "prj_4RemrVT2H255cTcdXxkLqFJUD6Dq"
|
3 |
+
# Vercel Team ID is optional. Only include it if your project is under a team.
|
4 |
+
TEAM_ID = False
|
5 |
+
PROJECT_NAME = "matax-express"
|
6 |
+
# update_vercel_env.py
|
7 |
+
import os
|
8 |
+
import json
|
9 |
+
import requests
|
10 |
+
import sys
|
11 |
+
|
12 |
+
# --- Configuration ---
|
13 |
+
|
14 |
+
# --- Data to be Set ---
|
15 |
+
# This data will be converted to a JSON string and sent to Vercel.
|
16 |
+
# You can modify this data directly in the script before running it.
|
17 |
+
|
18 |
+
WHY_US_FEATURES_DATA = [
|
19 |
+
{ "icon": "EnergySavingsLeafIcon", "title": "Peak Freshness", "description": "Guaranteed farm-to-door freshness in every order." },
|
20 |
+
{ "icon": "YardIcon", "title": "Local Sourcing", "description": "Partnering with local farms to support the community." },
|
21 |
+
{ "icon": "CategoryIcon", "title": "Wide Selection", "description": "A diverse range of produce, dairy, and pantry staples." },
|
22 |
+
{ "icon": "LocalShippingIcon", "title": "Reliable Delivery", "description": "On-time, refrigerated delivery you can count on." },
|
23 |
+
]
|
24 |
+
|
25 |
+
FAQS_DATA = [
|
26 |
+
{ "question": "What regions do you deliver to?", "answer": "We currently deliver to all major metropolitan areas within the state. We are actively expanding our delivery network, so please check back for updates on new regions." },
|
27 |
+
{ "question": "How do I place an order?", "answer": "Once you register for an account and are approved, you can log in to our customer portal. From there, you can browse our product catalog, select quantities, and schedule your delivery." },
|
28 |
+
{ "question": "What are your quality standards?", "answer": "We pride ourselves on sourcing only the freshest, Grade A produce from trusted local and national farms. Every item is inspected for quality and freshness before it leaves our facility." },
|
29 |
+
{ "question": "Is there a minimum order requirement?", "answer": "Yes, there is a minimum order value for delivery. This amount varies by region. You can find the specific minimum for your area in your customer portal after logging in." },
|
30 |
+
]
|
31 |
+
|
32 |
+
def get_existing_env_var(key, headers, params):
|
33 |
+
"""Fetches all environment variables and returns the one matching the key."""
|
34 |
+
api_url = f"https://api.vercel.com/v9/projects/{PROJECT_ID}/env"
|
35 |
+
try:
|
36 |
+
response = requests.get(api_url, headers=headers, params=params)
|
37 |
+
response.raise_for_status()
|
38 |
+
all_vars = response.json().get('envs', [])
|
39 |
+
for var in all_vars:
|
40 |
+
if var['key'] == key:
|
41 |
+
print(f"Found existing variable '{key}' with ID: {var['id']}")
|
42 |
+
return var
|
43 |
+
return None
|
44 |
+
except requests.exceptions.RequestException as err:
|
45 |
+
print(f"❌ Error fetching environment variables: {err}")
|
46 |
+
return None
|
47 |
+
|
48 |
+
def delete_env_var(var_id, headers, params):
|
49 |
+
"""Deletes an environment variable by its ID."""
|
50 |
+
print(f"Attempting to delete variable with ID: {var_id}")
|
51 |
+
api_url = f"https://api.vercel.com/v9/projects/{PROJECT_ID}/env/{var_id}"
|
52 |
+
try:
|
53 |
+
response = requests.delete(api_url, headers=headers, params=params)
|
54 |
+
response.raise_for_status()
|
55 |
+
print(f"✅ Successfully deleted variable.")
|
56 |
+
return True
|
57 |
+
except requests.exceptions.RequestException as err:
|
58 |
+
print(f"❌ Error deleting environment variable: {err}")
|
59 |
+
return False
|
60 |
+
|
61 |
+
def create_env_var(key, value_obj, target_environments, headers, params):
|
62 |
+
"""Creates a new environment variable."""
|
63 |
+
print(f"Creating new environment variable '{key}'...")
|
64 |
+
api_url = f"https://api.vercel.com/v9/projects/{PROJECT_ID}/env"
|
65 |
+
payload = {
|
66 |
+
"key": key,
|
67 |
+
"value": json.dumps(value_obj),
|
68 |
+
"type": "encrypted",
|
69 |
+
"target": target_environments,
|
70 |
+
}
|
71 |
+
try:
|
72 |
+
response = requests.post(api_url, headers=headers, json=payload, params=params)
|
73 |
+
response.raise_for_status()
|
74 |
+
print(f"✅ Successfully created environment variable '{key}'.")
|
75 |
+
return True
|
76 |
+
except requests.exceptions.RequestException as err:
|
77 |
+
print(f"❌ HTTP Error creating '{key}': {err}")
|
78 |
+
if err.response:
|
79 |
+
print(f" Response Body: {err.response.text}")
|
80 |
+
return False
|
81 |
+
|
82 |
+
def set_vercel_env_var(key, value_obj, target_environments):
|
83 |
+
"""
|
84 |
+
Main function to set an environment variable.
|
85 |
+
It deletes the variable if it exists, then creates it.
|
86 |
+
"""
|
87 |
+
print(f"--- Processing environment variable: {key} ---")
|
88 |
+
headers = {"Authorization": f"Bearer {VERCEL_TOKEN}"}
|
89 |
+
params = {"teamId": TEAM_ID} if TEAM_ID else {}
|
90 |
+
|
91 |
+
existing_var = get_existing_env_var(key, headers, params)
|
92 |
+
|
93 |
+
if existing_var:
|
94 |
+
if not delete_env_var(existing_var['id'], headers, params):
|
95 |
+
print(f"Aborting update for '{key}' due to deletion failure.")
|
96 |
+
return False
|
97 |
+
|
98 |
+
return create_env_var(key, value_obj, target_environments, headers, params)
|
99 |
+
|
100 |
+
def get_project_git_info(headers, params):
|
101 |
+
"""Fetches project details to get the linked Git repository ID and type."""
|
102 |
+
print("Fetching project details to find Git repo ID...")
|
103 |
+
api_url = f"https://api.vercel.com/v9/projects/{PROJECT_ID}"
|
104 |
+
try:
|
105 |
+
response = requests.get(api_url, headers=headers, params=params)
|
106 |
+
response.raise_for_status()
|
107 |
+
project_data = response.json()
|
108 |
+
|
109 |
+
link_info = project_data.get('link')
|
110 |
+
if not link_info or 'repoId' not in link_info or 'type' not in link_info:
|
111 |
+
print("❌ Error: Could not find linked Git repository information (repoId).")
|
112 |
+
print(" Please ensure your Vercel project is connected to a Git repository.")
|
113 |
+
return None, None
|
114 |
+
|
115 |
+
repo_id = link_info['repoId']
|
116 |
+
git_type = link_info['type']
|
117 |
+
print(f"✅ Found Git repo ID: {repo_id} (type: {git_type})")
|
118 |
+
return repo_id, git_type
|
119 |
+
|
120 |
+
except requests.exceptions.RequestException as err:
|
121 |
+
print(f"❌ Error fetching project details: {err}")
|
122 |
+
return None, None
|
123 |
+
|
124 |
+
def trigger_vercel_deployment():
|
125 |
+
"""Triggers a new Vercel deployment for the project."""
|
126 |
+
print("\n--- Triggering new Vercel deployment ---")
|
127 |
+
headers = {"Authorization": f"Bearer {VERCEL_TOKEN}", "Content-Type": "application/json"}
|
128 |
+
params = {"teamId": TEAM_ID} if TEAM_ID else {}
|
129 |
+
|
130 |
+
# Dynamically get the Git repo ID and type required for the deployment payload
|
131 |
+
repo_id, git_type = get_project_git_info(headers, params)
|
132 |
+
if not repo_id:
|
133 |
+
print("Aborting deployment trigger due to missing Git info.")
|
134 |
+
return
|
135 |
+
|
136 |
+
api_url = "https://api.vercel.com/v13/deployments"
|
137 |
+
|
138 |
+
# The payload now correctly includes the repoId
|
139 |
+
payload = {
|
140 |
+
"name": PROJECT_NAME,
|
141 |
+
"target": "production",
|
142 |
+
"gitSource": {
|
143 |
+
"type": git_type,
|
144 |
+
"repoId": repo_id,
|
145 |
+
"ref": "main" # Change 'main' to your default branch if it's different
|
146 |
+
}
|
147 |
+
}
|
148 |
+
|
149 |
+
try:
|
150 |
+
response = requests.post(api_url, headers=headers, json=payload, params=params)
|
151 |
+
response.raise_for_status()
|
152 |
+
deployment_url = response.json().get('url')
|
153 |
+
print(f"✅ Successfully triggered new deployment.")
|
154 |
+
print(f" Inspect deployment: https://{deployment_url}")
|
155 |
+
|
156 |
+
except requests.exceptions.HTTPError as err:
|
157 |
+
print(f"❌ HTTP Error triggering deployment: {err}")
|
158 |
+
print(f" Response Body: {err.response.text}")
|
159 |
+
except Exception as err:
|
160 |
+
print(f"❌ An unexpected error occurred while triggering deployment: {err}")
|
161 |
+
|
162 |
+
|
163 |
+
def main():
|
164 |
+
"""Main function to run the script."""
|
165 |
+
print("Starting Vercel environment variable update process...")
|
166 |
+
|
167 |
+
if not all([VERCEL_TOKEN, PROJECT_ID, PROJECT_NAME]):
|
168 |
+
print("❌ Error: Missing required environment variables.")
|
169 |
+
print(" Please set VERCEL_TOKEN, VERCEL_PROJECT_ID, and VERCEL_PROJECT_NAME.")
|
170 |
+
sys.exit(1)
|
171 |
+
|
172 |
+
target_environments = ["production", "preview", "development"]
|
173 |
+
|
174 |
+
success1 = set_vercel_env_var(
|
175 |
+
key="REACT_APP_WHY_US_FEATURES",
|
176 |
+
value_obj=WHY_US_FEATURES_DATA,
|
177 |
+
target_environments=target_environments
|
178 |
+
)
|
179 |
+
|
180 |
+
print("\n")
|
181 |
+
|
182 |
+
success2 = set_vercel_env_var(
|
183 |
+
key="REACT_APP_FAQS",
|
184 |
+
value_obj=FAQS_DATA,
|
185 |
+
target_environments=target_environments
|
186 |
+
)
|
187 |
+
|
188 |
+
if success1 and success2:
|
189 |
+
trigger_vercel_deployment()
|
190 |
+
else:
|
191 |
+
print("\nSkipping deployment due to errors in updating environment variables.")
|
192 |
+
|
193 |
+
print("\nScript finished.")
|
194 |
+
|
195 |
+
|
196 |
+
if __name__ == "__main__":
|
197 |
+
main()
|
flask_session/aa71dde20eaf768ca7e5f90a25563ea6
CHANGED
Binary files a/flask_session/aa71dde20eaf768ca7e5f90a25563ea6 and b/flask_session/aa71dde20eaf768ca7e5f90a25563ea6 differ
|
|
search_engine.py
CHANGED
@@ -19,7 +19,7 @@ def categorise(product):
|
|
19 |
|
20 |
response = client.models.generate_content(
|
21 |
model="gemini-2.5-flash-lite-preview-06-17",
|
22 |
-
contents=f"Categorise this product:{product} , into one of the following categories: `Fruits,Vegetables,Bakery
|
23 |
config={
|
24 |
"response_mime_type": "application/json",
|
25 |
"response_schema": list[Categorise],
|
@@ -30,7 +30,7 @@ def categorise(product):
|
|
30 |
time.sleep(2)
|
31 |
response = client.models.generate_content(
|
32 |
model="gemini-2.5-flash",
|
33 |
-
contents=f"Categorise this product:{product} , into one of the following categories: `Fruits,Vegetables,Bakery
|
34 |
config={
|
35 |
"response_mime_type": "application/json",
|
36 |
"response_schema": list[Categorise],
|
|
|
19 |
|
20 |
response = client.models.generate_content(
|
21 |
model="gemini-2.5-flash-lite-preview-06-17",
|
22 |
+
contents=f"Categorise this product:{product} , into one of the following categories: `Fruits,Vegetables,Bakery`",
|
23 |
config={
|
24 |
"response_mime_type": "application/json",
|
25 |
"response_schema": list[Categorise],
|
|
|
30 |
time.sleep(2)
|
31 |
response = client.models.generate_content(
|
32 |
model="gemini-2.5-flash",
|
33 |
+
contents=f"Categorise this product:{product} , into one of the following categories: `Fruits,Vegetables,Bakery`",
|
34 |
config={
|
35 |
"response_mime_type": "application/json",
|
36 |
"response_schema": list[Categorise],
|