Add bid tracker
This commit is contained in:
@@ -1693,3 +1693,423 @@ def get_ai_analysis_detail(analysis_id: int):
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── Bid Tracker ──────────────────────────────────────────────────────────────
|
||||
|
||||
def get_all_bids(status_filter=None):
|
||||
"""
|
||||
Return all bids ordered by updated_at DESC.
|
||||
status_filter: optional string to filter by status, e.g. 'open'.
|
||||
Includes creator username and latest update timestamp.
|
||||
"""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
where = ""
|
||||
params = []
|
||||
if status_filter and status_filter != "all":
|
||||
where = "WHERE b.status = %s"
|
||||
params.append(status_filter)
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT b.*,
|
||||
COALESCE(u.full_name, u.username) AS creator_name,
|
||||
u.username AS creator_username,
|
||||
(SELECT COUNT(*) FROM bid_updates bu WHERE bu.bid_id = b.id)
|
||||
AS update_count,
|
||||
(SELECT MAX(bu2.created_at) FROM bid_updates bu2
|
||||
WHERE bu2.bid_id = b.id) AS last_update_at
|
||||
FROM bid_tracker b
|
||||
LEFT JOIN users u ON u.id = b.created_by
|
||||
{where}
|
||||
ORDER BY b.updated_at DESC
|
||||
""",
|
||||
params
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
return rows
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_bid_by_id(bid_id: int):
|
||||
"""Return a single bid row with creator info."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT b.*,
|
||||
COALESCE(u.full_name, u.username) AS creator_name,
|
||||
u.username AS creator_username
|
||||
FROM bid_tracker b
|
||||
LEFT JOIN users u ON u.id = b.created_by
|
||||
WHERE b.id = %s
|
||||
""",
|
||||
(bid_id,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
return row
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def create_bid(user_id: int, title: str, url: str,
|
||||
source: str, notes: str, status: str) -> int:
|
||||
"""Insert a new bid. Returns the new row id."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO bid_tracker (title, url, source, notes, status, created_by)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(title, url, source or None, notes or None, status, user_id)
|
||||
)
|
||||
conn.commit()
|
||||
new_id = cur.lastrowid
|
||||
cur.close()
|
||||
log_action(user_id, "CREATE_BID", "bid_tracker", new_id,
|
||||
f"Created bid '{title}' status={status} url={url[:80]}")
|
||||
logger.info(f"Bid id={new_id} '{title}' created by user_id={user_id}.")
|
||||
return new_id
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def update_bid(user_id: int, bid_id: int, title: str, url: str,
|
||||
source: str, notes: str, status: str):
|
||||
"""Update an existing bid record."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE bid_tracker
|
||||
SET title=%s, url=%s, source=%s, notes=%s, status=%s
|
||||
WHERE id=%s
|
||||
""",
|
||||
(title, url, source or None, notes or None, status, bid_id)
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
log_action(user_id, "UPDATE_BID", "bid_tracker", bid_id,
|
||||
f"Updated bid id={bid_id} '{title}' status={status}")
|
||||
logger.info(f"Bid id={bid_id} updated by user_id={user_id}.")
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_bid(user_id: int, bid_id: int):
|
||||
"""Hard-delete a bid and all its updates (CASCADE)."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute("SELECT title FROM bid_tracker WHERE id=%s", (bid_id,))
|
||||
row = cur.fetchone()
|
||||
title = row["title"] if row else str(bid_id)
|
||||
cur.execute("DELETE FROM bid_tracker WHERE id=%s", (bid_id,))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
log_action(user_id, "DELETE_BID", "bid_tracker", bid_id,
|
||||
f"Deleted bid id={bid_id} '{title}'")
|
||||
logger.info(f"Bid id={bid_id} '{title}' deleted by user_id={user_id}.")
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_bid_updates(bid_id: int):
|
||||
"""Return all updates for a bid, newest first."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT bu.*,
|
||||
COALESCE(u.full_name, u.username) AS author_name,
|
||||
u.username AS author_username
|
||||
FROM bid_updates bu
|
||||
LEFT JOIN users u ON u.id = bu.user_id
|
||||
WHERE bu.bid_id = %s
|
||||
ORDER BY bu.created_at DESC
|
||||
""",
|
||||
(bid_id,)
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
return rows
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def add_bid_update(user_id: int, bid_id: int, content: str) -> int:
|
||||
"""Add an update entry to a bid. Also touches bid_tracker.updated_at."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"INSERT INTO bid_updates (bid_id, user_id, content) VALUES (%s, %s, %s)",
|
||||
(bid_id, user_id, content)
|
||||
)
|
||||
# Touch bid updated_at so it sorts to top of list
|
||||
cur.execute(
|
||||
"UPDATE bid_tracker SET updated_at=NOW() WHERE id=%s", (bid_id,))
|
||||
conn.commit()
|
||||
new_id = cur.lastrowid
|
||||
cur.close()
|
||||
log_action(user_id, "ADD_BID_UPDATE", "bid_updates", new_id,
|
||||
f"Added update to bid id={bid_id}: {content[:100]}")
|
||||
logger.info(f"Bid update id={new_id} added to bid id={bid_id} "
|
||||
f"by user_id={user_id}.")
|
||||
return new_id
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_bid_update(user_id: int, update_id: int):
|
||||
"""Delete a single bid update entry."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute("SELECT bid_id, content FROM bid_updates WHERE id=%s",
|
||||
(update_id,))
|
||||
row = cur.fetchone()
|
||||
bid_id = row["bid_id"] if row else 0
|
||||
snippet = (row["content"][:60] if row else "") if row else ""
|
||||
cur.execute("DELETE FROM bid_updates WHERE id=%s", (update_id,))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
log_action(user_id, "DELETE_BID_UPDATE", "bid_updates", update_id,
|
||||
f"Deleted update id={update_id} from bid id={bid_id}: {snippet}")
|
||||
logger.info(f"Bid update id={update_id} deleted by user_id={user_id}.")
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── Bid Tracker CRUD ─────────────────────────────────────────────────────────
|
||||
|
||||
BID_STATUSES = ("open", "monitoring", "awarded", "no_bid", "cancelled")
|
||||
|
||||
|
||||
def get_all_bids(status_filter: str = "") -> list:
|
||||
"""
|
||||
Return all bids ordered by due_date (nulls last), then created_at desc.
|
||||
When status_filter is given, only bids with that status are returned.
|
||||
Includes the adder's username and the count of updates per bid.
|
||||
"""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
where = "WHERE b.status = %s" if status_filter else ""
|
||||
params = (status_filter,) if status_filter else ()
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT b.*,
|
||||
u.username AS added_by_username,
|
||||
COUNT(bu.id) AS update_count
|
||||
FROM bid_tracker b
|
||||
LEFT JOIN users u ON u.id = b.added_by
|
||||
LEFT JOIN bid_updates bu ON bu.bid_id = b.id
|
||||
{where}
|
||||
GROUP BY b.id
|
||||
ORDER BY b.due_date IS NULL, b.due_date ASC, b.created_at DESC
|
||||
""",
|
||||
params,
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
return rows
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_bid(bid_id: int) -> dict | None:
|
||||
"""Return a single bid row with adder username."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT b.*, u.username AS added_by_username
|
||||
FROM bid_tracker b
|
||||
LEFT JOIN users u ON u.id = b.added_by
|
||||
WHERE b.id = %s
|
||||
""",
|
||||
(bid_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
return row
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def create_bid(user_id: int, title: str, url: str, source: str,
|
||||
solicitation_number: str, status: str,
|
||||
due_date, notes: str) -> int:
|
||||
"""Insert a new bid. Returns new row id."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO bid_tracker
|
||||
(title, url, source, solicitation_number, status, due_date, notes, added_by)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(title, url,
|
||||
source or None, solicitation_number or None,
|
||||
status, due_date or None, notes or None,
|
||||
user_id),
|
||||
)
|
||||
conn.commit()
|
||||
new_id = cur.lastrowid
|
||||
cur.close()
|
||||
log_action(user_id, "CREATE_BID", "bid_tracker", new_id,
|
||||
f"Created bid '{title}' status={status}.")
|
||||
logger.info(f"Bid id={new_id} '{title}' created by user_id={user_id}.")
|
||||
return new_id
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def update_bid(user_id: int, bid_id: int, title: str, url: str,
|
||||
source: str, solicitation_number: str, status: str,
|
||||
due_date, notes: str):
|
||||
"""Update an existing bid."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE bid_tracker
|
||||
SET title=%s, url=%s, source=%s, solicitation_number=%s,
|
||||
status=%s, due_date=%s, notes=%s
|
||||
WHERE id=%s
|
||||
""",
|
||||
(title, url,
|
||||
source or None, solicitation_number or None,
|
||||
status, due_date or None, notes or None,
|
||||
bid_id),
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
log_action(user_id, "UPDATE_BID", "bid_tracker", bid_id,
|
||||
f"Updated bid '{title}' status={status}.")
|
||||
logger.info(f"Bid id={bid_id} updated by user_id={user_id}.")
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_bid(user_id: int, bid_id: int):
|
||||
"""Hard-delete a bid and all its updates (CASCADE)."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute("SELECT title FROM bid_tracker WHERE id=%s", (bid_id,))
|
||||
row = cur.fetchone()
|
||||
title = row["title"] if row else str(bid_id)
|
||||
cur.execute("DELETE FROM bid_tracker WHERE id=%s", (bid_id,))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
log_action(user_id, "DELETE_BID", "bid_tracker", bid_id,
|
||||
f"Deleted bid '{title}'.")
|
||||
logger.info(f"Bid id={bid_id} '{title}' deleted by user_id={user_id}.")
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── Bid Updates CRUD ─────────────────────────────────────────────────────────
|
||||
|
||||
def get_bid_updates(bid_id: int) -> list:
|
||||
"""Return all updates for a bid, newest first."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT bu.*, u.username AS posted_by_username,
|
||||
COALESCE(u.full_name, u.username) AS posted_by_full_name
|
||||
FROM bid_updates bu
|
||||
LEFT JOIN users u ON u.id = bu.user_id
|
||||
WHERE bu.bid_id = %s
|
||||
ORDER BY bu.created_at DESC
|
||||
""",
|
||||
(bid_id,),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
return rows
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def add_bid_update(user_id: int, bid_id: int, content: str) -> int:
|
||||
"""Post a new update on a bid. Returns new row id."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"INSERT INTO bid_updates (bid_id, user_id, content) VALUES (%s, %s, %s)",
|
||||
(bid_id, user_id, content),
|
||||
)
|
||||
conn.commit()
|
||||
new_id = cur.lastrowid
|
||||
cur.close()
|
||||
log_action(user_id, "ADD_BID_UPDATE", "bid_updates", new_id,
|
||||
f"Posted update on bid_id={bid_id}.")
|
||||
logger.info(f"Bid update id={new_id} posted on bid_id={bid_id} by user_id={user_id}.")
|
||||
return new_id
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_bid_update(user_id: int, update_id: int):
|
||||
"""Delete a single bid update. Any user can delete their own; admin can delete any."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("DELETE FROM bid_updates WHERE id=%s", (update_id,))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
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}.")
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user