235 lines
7.9 KiB
Python
235 lines
7.9 KiB
Python
"""
|
|
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,
|
|
)
|
|
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",
|
|
}
|
|
|
|
|
|
@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("/<int:bid_id>")
|
|
@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:
|
|
create_bid(user["id"], title, url, source, sol_no, status, due_date, notes)
|
|
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("/<int:bid_id>/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)
|
|
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("/<int:bid_id>/delete", methods=["POST"])
|
|
@login_required
|
|
def delete(bid_id):
|
|
user = session["user"]
|
|
try:
|
|
delete_bid(user["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("/<int:bid_id>/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:
|
|
add_bid_update(user["id"], bid_id, content)
|
|
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/<int:update_id>/delete", methods=["POST"])
|
|
@login_required
|
|
def delete_update(update_id):
|
|
user = session["user"]
|
|
try:
|
|
delete_bid_update(user["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("/<int:bid_id>/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
|
|
|
|
def ser(row):
|
|
"""Make a dict JSON-serialisable (dates → str)."""
|
|
out = {}
|
|
for k, v in row.items():
|
|
if hasattr(v, "isoformat"):
|
|
out[k] = v.isoformat()
|
|
else:
|
|
out[k] = v
|
|
return out
|
|
|
|
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
|
|
|
|
def ser(row):
|
|
out = {}
|
|
for k, v in row.items():
|
|
if hasattr(v, "isoformat"):
|
|
out[k] = v.isoformat()
|
|
else:
|
|
out[k] = v
|
|
return out
|
|
|
|
return jsonify([ser(b) for b in bids])
|
|
|
|
|
|
@bid_tracker_bp.route("/<int:bid_id>/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)
|
|
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/<int:update_id>/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)
|
|
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
|