akiko19191 commited on
Commit
85354fe
·
verified ·
1 Parent(s): 892f25f

Upload folder using huggingface_hub

Browse files
__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
- added_messages.append(f"{item.get('quantity')} {item.get('unit')} of {p_doc['name']}")
 
 
 
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
- except Exception as e:
51
- current_app.logger.error(f"Failed to send registration email to {email}: {e}")
52
-
53
- return jsonify({"msg": "Registration successful! Your application is being processed."}), 201
54
-
55
- # ... (the rest of your api.py file remains unchanged)
56
- @api_bp.route('/login', methods=['POST'])
57
- def login():
58
- data = request.get_json()
59
- email, password = data.get('email'), data.get('password')
60
- user = mongo.db.users.find_one({'email': email})
61
-
62
- if user and user.get('password') and bcrypt.check_password_hash(user['password'], password):
63
- if not user.get('is_approved', False): return jsonify({"msg": "Account pending approval"}), 403
64
-
65
- try:
66
- send_login_notification_email(user)
67
- except Exception as e:
68
- current_app.logger.error(f"Failed to send login notification email to {email}: {e}")
69
-
70
- access_token = create_access_token(identity=email)
71
- return jsonify(access_token=access_token, email=user['email'], companyName=user['businessName'],contactPerson=user.get('contactPerson', '')) , 200
72
-
73
- return jsonify({"msg": "Bad email or password"}), 401
74
-
75
- @api_bp.route('/profile', methods=['GET'])
76
- @jwt_required()
77
- def get_user_profile():
78
- user_email = get_jwt_identity()
79
- user = mongo.db.users.find_one({'email': user_email})
80
-
81
- if not user:
82
- return jsonify({"msg": "User not found"}), 404
83
-
84
- profile_data = {
85
- 'deliveryAddress': user.get('businessAddress', ''),
86
- 'mobileNumber': user.get('phoneNumber', '')
87
- }
88
-
89
- return jsonify(profile_data), 200
90
-
91
- @api_bp.route('/products', methods=['GET'])
92
- def get_products():
93
- products = [{
94
- 'id': str(p['_id']), 'name': p.get('name'), 'category': p.get('category'),
95
- 'unit': p.get('unit'), 'image_url': p.get('image_url', ''), 'price': p.get('price', '')
96
- } for p in mongo.db.products.find()]
97
- return jsonify(products)
98
-
99
-
100
- @api_bp.route('/cart', methods=['GET', 'POST'])
101
- @jwt_required()
102
- def handle_cart():
103
- user_email = get_jwt_identity()
104
-
105
- if request.method == 'GET':
106
- cart = mongo.db.carts.find_one({'user_email': user_email})
107
- if not cart:
108
- return jsonify({'items': [], 'deliveryDate': None})
109
-
110
- populated_items = []
111
- if cart.get('items'):
112
- product_ids = [ObjectId(item['productId']) for item in cart['items']]
113
- if product_ids:
114
- products = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
115
- for item in cart['items']:
116
- details = products.get(item['productId'])
117
- if details:
118
- populated_items.append({
119
- 'product': {'id': str(details['_id']), 'name': details.get('name'), 'unit': details.get('unit'), 'image_url': details.get('image_url'), 'price': details.get('price')},
120
- 'quantity': item['quantity'],
121
- 'mode': item.get('mode', 'pieces')
122
- })
123
-
124
- return jsonify({
125
- 'items': populated_items,
126
- 'deliveryDate': cart.get('deliveryDate')
127
- })
128
-
129
- if request.method == 'POST':
130
- data = request.get_json()
131
-
132
- update_doc = {
133
- 'user_email': user_email,
134
- 'updated_at': datetime.utcnow()
135
- }
136
-
137
- if 'items' in data:
138
- update_doc['items'] = data['items']
139
-
140
- if 'deliveryDate' in data:
141
- update_doc['deliveryDate'] = data['deliveryDate']
142
-
143
- mongo.db.carts.update_one(
144
- {'user_email': user_email},
145
- {'$set': update_doc},
146
- upsert=True
147
- )
148
- return jsonify({"msg": "Cart updated successfully"})
149
-
150
- @api_bp.route('/orders', methods=['GET', 'POST'])
151
- @jwt_required()
152
- def handle_orders():
153
- user_email = get_jwt_identity()
154
-
155
- if request.method == 'POST':
156
- cart = mongo.db.carts.find_one({'user_email': user_email})
157
- if not cart or not cart.get('items'): return jsonify({"msg": "Your cart is empty"}), 400
158
-
159
- data = request.get_json()
160
- if not all([data.get('deliveryDate'), data.get('deliveryAddress'), data.get('mobileNumber')]): return jsonify({"msg": "Missing delivery information"}), 400
161
-
162
- user = mongo.db.users.find_one({'email': user_email})
163
- if not user:
164
- return jsonify({"msg": "User not found"}), 404
165
-
166
- order_doc = {
167
- 'user_email': user_email, 'items': cart['items'], 'delivery_date': data['deliveryDate'],
168
- 'delivery_address': data['deliveryAddress'], 'mobile_number': data['mobileNumber'],
169
- 'additional_info': data.get('additionalInfo'), 'total_amount': data.get('totalAmount'),
170
- 'status': 'pending', 'created_at': datetime.utcnow()
171
- }
172
- order_doc['serial_no'] = get_next_order_serial()
173
- order_id = mongo.db.orders.insert_one(order_doc).inserted_id
174
- order_doc['_id'] = order_id
175
-
176
- order_details_for_xero = {
177
- "order_id": str(order_id), "user_email": user_email, "items": cart['items'],
178
- "delivery_address": data['deliveryAddress'], "mobile_number": data['mobileNumber'],"deliverydate":data["deliveryDate"]
179
- }
180
- trigger_po_creation(order_details_for_xero)
181
-
182
- try:
183
- product_ids = [ObjectId(item['productId']) for item in cart['items']]
184
- products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
185
-
186
- order_doc['populated_items'] = [{
187
- "name": products_map.get(item['productId'], {}).get('name', 'N/A'),
188
- "quantity": item['quantity'],
189
- "mode": item.get('mode', 'pieces')
190
- } for item in cart['items']]
191
-
192
- send_order_confirmation_email(order_doc, user)
193
-
194
- except Exception as e:
195
- current_app.logger.error(f"Failed to send confirmation email for order {order_id}: {e}")
196
-
197
- mongo.db.carts.delete_one({'user_email': user_email})
198
- return jsonify({"msg": "Order placed successfully! You will be redirected shortly to the Orders Page!", "orderId": str(order_id)}), 201
199
-
200
- if request.method == 'GET':
201
- user_orders = list(mongo.db.orders.find({'user_email': user_email}).sort('created_at', -1))
202
- if not user_orders: return jsonify([])
203
-
204
- all_product_ids = {ObjectId(item['productId']) for order in user_orders for item in order.get('items', [])}
205
- products = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': list(all_product_ids)}})}
206
-
207
- for order in user_orders:
208
- order['items'] = [
209
- {
210
- 'quantity': item['quantity'],
211
- 'mode': item.get('mode', 'pieces'),
212
- 'product': {
213
- 'id': str(p['_id']),
214
- 'name': p.get('name'),
215
- 'unit': p.get('unit'),
216
- 'image_url': p.get('image_url')
217
- }
218
- }
219
- for item in order.get('items', []) if (p := products.get(item['productId']))
220
- ]
221
- order['_id'] = str(order['_id'])
222
- order['created_at'] = order['created_at'].isoformat()
223
- order['delivery_date'] = order['delivery_date'] if isinstance(order['delivery_date'], str) else order['delivery_date'].isoformat()
224
- return jsonify(user_orders)
225
-
226
- @api_bp.route('/orders/<order_id>', methods=['GET'])
227
- @jwt_required()
228
- def get_order(order_id):
229
- user_email = get_jwt_identity()
230
- try:
231
- order = mongo.db.orders.find_one({'_id': ObjectId(order_id), 'user_email': user_email})
232
- if not order:
233
- return jsonify({"msg": "Order not found or access denied"}), 404
234
-
235
- order['_id'] = str(order['_id'])
236
- return jsonify(order), 200
237
- except Exception as e:
238
- return jsonify({"msg": f"Invalid Order ID format: {e}"}), 400
239
-
240
- @api_bp.route('/orders/<order_id>', methods=['PUT'])
241
- @jwt_required()
242
- def update_order(order_id):
243
- user_email = get_jwt_identity()
244
-
245
- order = mongo.db.orders.find_one({'_id': ObjectId(order_id), 'user_email': user_email})
246
- if not order:
247
- return jsonify({"msg": "Order not found or access denied"}), 404
248
-
249
- if order.get('status') not in ['pending', 'confirmed']:
250
- return jsonify({"msg": f"Order with status '{order.get('status')}' cannot be modified."}), 400
251
-
252
- cart = mongo.db.carts.find_one({'user_email': user_email})
253
- if not cart or not cart.get('items'):
254
- return jsonify({"msg": "Cannot update with an empty cart. Please add items."}), 400
255
-
256
- data = request.get_json()
257
- update_doc = {
258
- 'items': cart['items'],
259
- 'delivery_date': data['deliveryDate'],
260
- 'delivery_address': data['deliveryAddress'],
261
- 'mobile_number': data['mobileNumber'],
262
- 'additional_info': data.get('additionalInfo'),
263
- 'total_amount': data.get('totalAmount'),
264
- 'updated_at': datetime.utcnow()
265
- }
266
-
267
- mongo.db.orders.update_one({'_id': ObjectId(order_id)}, {'$set': update_doc})
268
- mongo.db.carts.delete_one({'user_email': user_email})
269
-
270
- return jsonify({"msg": "Order updated successfully!", "orderId": order_id}), 200
271
-
272
- @api_bp.route('/orders/<order_id>/cancel', methods=['POST'])
273
- @jwt_required()
274
- def cancel_order(order_id):
275
- user_email = get_jwt_identity()
276
- order = mongo.db.orders.find_one({'_id': ObjectId(order_id), 'user_email': user_email})
277
-
278
- if not order:
279
- return jsonify({"msg": "Order not found or access denied"}), 404
280
-
281
- if order.get('status') in ['delivered', 'cancelled']:
282
- return jsonify({"msg": "This order can no longer be cancelled."}), 400
283
-
284
- mongo.db.orders.update_one(
285
- {'_id': ObjectId(order_id)},
286
- {'$set': {'status': 'cancelled', 'updated_at': datetime.utcnow()}}
287
- )
288
-
289
- return jsonify({"msg": "Order has been cancelled."}), 200
290
-
291
- @api_bp.route('/sendmail', methods=['GET'])
292
- def send_cart_reminders():
293
- try:
294
- carts_with_items = list(mongo.db.carts.find({'items': {'$exists': True, '$ne': []}}))
295
-
296
- if not carts_with_items:
297
- return jsonify({"msg": "No users with pending items in cart."}), 200
298
-
299
- user_emails = [cart['user_email'] for cart in carts_with_items]
300
- all_product_ids = {
301
- ObjectId(item['productId'])
302
- for cart in carts_with_items
303
- for item in cart.get('items', [])
304
- }
305
-
306
- users_cursor = mongo.db.users.find({'email': {'$in': user_emails}})
307
- products_cursor = mongo.db.products.find({'_id': {'$in': list(all_product_ids)}})
308
-
309
- users_map = {user['email']: user for user in users_cursor}
310
- products_map = {str(prod['_id']): prod for prod in products_cursor}
311
-
312
- emails_sent_count = 0
313
-
314
- for cart in carts_with_items:
315
- user = users_map.get(cart['user_email'])
316
- if not user:
317
- current_app.logger.warning(f"Cart found for non-existent user: {cart['user_email']}")
318
- continue
319
-
320
- populated_items = []
321
- for item in cart.get('items', []):
322
- product_details = products_map.get(item['productId'])
323
- if product_details:
324
- populated_items.append({
325
- 'product': {
326
- 'id': str(product_details['_id']),
327
- 'name': product_details.get('name'),
328
- },
329
- 'quantity': item['quantity']
330
- })
331
-
332
- if populated_items:
333
- try:
334
- send_cart_reminder_email(user, populated_items)
335
- emails_sent_count += 1
336
- except Exception as e:
337
- current_app.logger.error(f"Failed to send cart reminder to {user['email']}: {e}")
338
-
339
- return jsonify({"msg": f"Cart reminder process finished. Emails sent to {emails_sent_count} users."}), 200
340
-
341
- except Exception as e:
342
- current_app.logger.error(f"Error in /sendmail endpoint: {e}")
343
- return jsonify({"msg": "An internal error occurred while sending reminders."}), 500
344
-
345
- @api_bp.route('/admin/users/approve/<user_id>', methods=['POST'])
346
- @jwt_required()
347
- def approve_user(user_id):
348
- mongo.db.users.update_one({'_id': ObjectId(user_id)}, {'$set': {'is_approved': True}})
349
- return jsonify({"msg": f"User {user_id} approved"})
350
-
351
- # +++ START: NEW ENDPOINT FOR ITEM REQUESTS +++
352
- @api_bp.route('/request-item', methods=['POST'])
353
- @jwt_required()
354
- def request_item():
355
- """
356
- Allows a logged-in user to request an item that is not in the catalog.
357
- The request is saved to the database for admin review.
358
- """
359
- user_email = get_jwt_identity()
360
- data = request.get_json()
361
-
362
- if not data or not data.get('details'):
363
- return jsonify({"msg": "Item details are required."}), 400
364
-
365
- details = data.get('details').strip()
366
- if not details:
367
- return jsonify({"msg": "Item details cannot be empty."}), 400
368
-
369
- try:
370
- # Fetch user info for more context in the request
371
- user = mongo.db.users.find_one({'email': user_email}, {'company_name': 1})
372
- company_name = user.get('company_name', 'N/A') if user else 'N/A'
373
-
374
- request_doc = {
375
- 'user_email': user_email,
376
- 'company_name': company_name,
377
- 'details': details,
378
- 'status': 'new', # Possible statuses: 'new', 'reviewed', 'sourced', 'rejected'
379
- 'requested_at': datetime.utcnow()
380
- }
381
-
382
- # The collection 'item_requests' will be created if it doesn't exist
383
- mongo.db.item_requests.insert_one(request_doc)
384
-
385
- # Optional: Here you could add a call to an email utility to notify admins
386
- # For example: send_item_request_notification(user_email, company_name, details)
387
-
388
- return jsonify({"msg": "Your item request has been submitted. We will look into it!"}), 201
389
-
390
- except Exception as e:
391
- current_app.logger.error(f"Error processing item request for {user_email}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- from flask import Blueprint, request, jsonify, redirect
 
 
 
 
 
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/update')
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">&larr; 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
- <title>{% if title %}{{ title }}{% else %}Welcome to Xero Python oauth starter{% endif %}</title>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  </head>
7
- <body>
8
- <a href="{{ url_for('xero.index') }}">Index</a> |
9
- <a href="{{ url_for('xero.login') }}">Login</a> |
10
- <a href="{{ url_for('xero.logout') }}">Logout</a> |
11
-
12
- <a href="{{ url_for('xero.fetch_inventory') }}">Sync Xero Inventory with Website</a> |
13
- {% block content %}{% endblock %}
14
- <!--<script src="https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js"></script>-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 index():
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=product_name+" ("+str(item["mode"]) + ")",
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,Others`",
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,Others`",
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],