""" routes/bid_tracker.py — Bid / Opportunity tracker routes. """ import logging from flask import (Blueprint, render_template, request, redirect, url_for, flash, session, jsonify) from models import ( get_all_bids, get_bid, create_bid, update_bid, delete_bid, get_bid_updates, add_bid_update, delete_bid_update, BID_STATUSES, log_action, ) from utils.decorators import login_required logger = logging.getLogger("routes.bid_tracker") bid_tracker_bp = Blueprint("bid_tracker", __name__, url_prefix="/bids") STATUS_LABELS = { "open": "🟢 Open", "monitoring": "🔵 Monitoring", "awarded": "🏆 Awarded", "no_bid": "⛔ No Bid", "cancelled": "🚫 Cancelled", } def _ser(row): """Make a DB dict JSON-serialisable (dates/times → ISO strings).""" return {k: v.isoformat() if hasattr(v, "isoformat") else v for k, v in row.items()} @bid_tracker_bp.route("/") @login_required def bids_list(): status_filter = request.args.get("status", "") bids = get_all_bids(status_filter=status_filter) return render_template("bid_tracker.html", bids=bids, status_filter=status_filter, bid_statuses=BID_STATUSES, status_labels=STATUS_LABELS) @bid_tracker_bp.route("/") @login_required def bid_detail(bid_id): bid = get_bid(bid_id) updates = get_bid_updates(bid_id) if not bid: flash("Bid not found.", "warning") return redirect(url_for("bid_tracker.bids_list")) return render_template("bid_detail.html", bid=bid, updates=updates, bid_statuses=BID_STATUSES, status_labels=STATUS_LABELS) @bid_tracker_bp.route("/create", methods=["POST"]) @login_required def create(): user = session["user"] title = request.form.get("title", "").strip() url = request.form.get("url", "").strip() source = request.form.get("source", "").strip() sol_no = request.form.get("solicitation_number", "").strip() status = request.form.get("status", "open") due_date = request.form.get("due_date") or None notes = request.form.get("notes", "").strip() if not title or not url: flash("Title and URL are required.", "danger") return redirect(url_for("bid_tracker.bids_list")) try: bid_id = create_bid(user["id"], title, url, source, sol_no, status, due_date, notes) log_action(user["id"], "CREATE_BID", "bid_tracker", bid_id, f"Created bid '{title}' status='{status}'.") flash(f"Bid '{title}' added.", "success") logger.info(f"Bid '{title}' created by user_id={user['id']}.") except Exception as e: logger.error(f"create_bid error: {e}") flash(f"Error: {e}", "danger") return redirect(url_for("bid_tracker.bids_list")) @bid_tracker_bp.route("//edit", methods=["POST"]) @login_required def edit(bid_id): user = session["user"] title = request.form.get("title", "").strip() url = request.form.get("url", "").strip() source = request.form.get("source", "").strip() sol_no = request.form.get("solicitation_number", "").strip() status = request.form.get("status", "open") due_date = request.form.get("due_date") or None notes = request.form.get("notes", "").strip() try: update_bid(user["id"], bid_id, title, url, source, sol_no, status, due_date, notes) log_action(user["id"], "UPDATE_BID", "bid_tracker", bid_id, f"Updated bid id={bid_id} status='{status}'.") flash(f"Bid '{title}' updated.", "success") logger.info(f"Bid id={bid_id} updated by user_id={user['id']}.") except Exception as e: logger.error(f"update_bid error: {e}") flash(f"Error: {e}", "danger") return redirect(url_for("bid_tracker.bid_detail", bid_id=bid_id)) @bid_tracker_bp.route("//delete", methods=["POST"]) @login_required def delete(bid_id): user = session["user"] try: delete_bid(user["id"], bid_id) log_action(user["id"], "DELETE_BID", "bid_tracker", bid_id, f"Deleted bid id={bid_id}.") flash("Bid deleted.", "success") logger.info(f"Bid id={bid_id} deleted by user_id={user['id']}.") except Exception as e: logger.error(f"delete_bid error: {e}") flash(f"Error: {e}", "danger") return redirect(url_for("bid_tracker.bids_list")) @bid_tracker_bp.route("//updates", methods=["POST"]) @login_required def add_update(bid_id): user = session["user"] content = request.form.get("content", "").strip() if not content: flash("Update content cannot be empty.", "warning") else: try: upd_id = add_bid_update(user["id"], bid_id, content) log_action(user["id"], "ADD_BID_UPDATE", "bid_updates", upd_id, f"Posted update on bid_id={bid_id}.") flash("Update posted.", "success") logger.info(f"Bid update posted on bid_id={bid_id} by user_id={user['id']}.") except Exception as e: logger.error(f"add_bid_update error: {e}") flash(f"Error: {e}", "danger") return redirect(url_for("bid_tracker.bid_detail", bid_id=bid_id)) @bid_tracker_bp.route("/updates//delete", methods=["POST"]) @login_required def delete_update(update_id): user = session["user"] try: delete_bid_update(user["id"], update_id) log_action(user["id"], "DELETE_BID_UPDATE", "bid_updates", update_id, f"Deleted bid update id={update_id}.") flash("Update deleted.", "success") logger.info(f"Bid update id={update_id} deleted by user_id={user['id']}.") except Exception as e: logger.error(f"delete_bid_update error: {e}") flash(f"Error: {e}", "danger") bid_id = request.form.get("bid_id") if bid_id: return redirect(url_for("bid_tracker.bid_detail", bid_id=int(bid_id))) return redirect(url_for("bid_tracker.bids_list")) @bid_tracker_bp.route("//json") @login_required def bid_json(bid_id): """Return bid detail + updates as JSON for the split-pane detail panel.""" user = session["user"] bid = get_bid(bid_id) if not bid: return jsonify({"error": "Not found"}), 404 updates = get_bid_updates(bid_id) is_owner = bid.get("added_by") == user["id"] can_edit = user["role"] == "admin" or is_owner return jsonify({ "bid": _ser(bid), "updates": [_ser(u) for u in updates], "can_edit": can_edit, "user_id": user["id"], "is_admin": user["role"] == "admin", }) @bid_tracker_bp.route("/list/json") @login_required def list_json(): """Return all bids as JSON for the AJAX list panel.""" status_filter = request.args.get("status", "") try: bids = get_all_bids(status_filter=status_filter) except Exception as e: return jsonify({"error": str(e)}), 500 return jsonify([_ser(b) for b in bids]) @bid_tracker_bp.route("//updates/json", methods=["POST"]) @login_required def add_update_json(bid_id): """Post a new update; return JSON so the page doesn't reload.""" user = session["user"] content = request.form.get("content", "").strip() if not content: return jsonify({"error": "Update content cannot be empty."}), 400 try: update_id = add_bid_update(user["id"], bid_id, content) log_action(user["id"], "ADD_BID_UPDATE", "bid_updates", update_id, f"Posted update on bid_id={bid_id}.") logger.info(f"Bid update id={update_id} posted on bid_id={bid_id} by user_id={user['id']}.") return jsonify({"ok": True, "update_id": update_id}) except Exception as e: logger.error(f"add_update_json error: {e}") return jsonify({"error": str(e)}), 500 @bid_tracker_bp.route("/updates//delete/json", methods=["POST"]) @login_required def delete_update_json(update_id): """Delete an update; return JSON so the page doesn't reload.""" user = session["user"] try: delete_bid_update(user["id"], update_id) log_action(user["id"], "DELETE_BID_UPDATE", "bid_updates", update_id, f"Deleted bid update id={update_id}.") logger.info(f"Bid update id={update_id} deleted by user_id={user['id']}.") return jsonify({"ok": True}) except Exception as e: logger.error(f"delete_update_json error: {e}") return jsonify({"error": str(e)}), 500