diff --git a/models.py b/models.py index a141db7..44d556d 100644 --- a/models.py +++ b/models.py @@ -1272,26 +1272,27 @@ def get_ai_analysis_detail(analysis_id: int): BID_STATUSES = ("open", "monitoring", "awarded", "no_bid", "cancelled") -def get_all_bids(status_filter: str = "") -> list: +def get_all_bids(status_filter: str = "", limit: int = 50, offset: int = 0) -> list: 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, + parts = [ + "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" + ] + params = [] + if status_filter: + parts.append("WHERE b.status=%s") + params.append(status_filter) + parts.append( + "GROUP BY b.id ORDER BY b.due_date IS NULL, b.due_date ASC, b.created_at DESC" + " LIMIT %s OFFSET %s" ) + params.extend([limit, offset]) + cur.execute(" ".join(parts), params) rows = cur.fetchall() cur.close() return rows diff --git a/routes/bid_tracker.py b/routes/bid_tracker.py index f58dc89..08c348c 100644 --- a/routes/bid_tracker.py +++ b/routes/bid_tracker.py @@ -190,10 +190,14 @@ def bid_json(bid_id): @bid_tracker_bp.route("/list/json") @login_required def list_json(): - """Return all bids as JSON for the AJAX list panel.""" + """Return paginated bids as JSON for the AJAX list panel.""" status_filter = request.args.get("status", "") try: - bids = get_all_bids(status_filter=status_filter) + offset = int(request.args.get("offset", 0)) + except (ValueError, TypeError): + offset = 0 + try: + bids = get_all_bids(status_filter=status_filter, limit=50, offset=offset) except Exception as e: return jsonify({"error": str(e)}), 500 diff --git a/static/css/style.css b/static/css/style.css index f7a7934..d75838f 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -694,6 +694,14 @@ textarea{resize:vertical;min-height:88px} .sidebar-footer{align-items:center} .main-content{padding:1.1rem .9rem 2rem} } +/* Hamburger button — hidden on desktop */ +.sidebar-toggle{display:none} +/* Backdrop — always hidden until .open is added */ +.sidebar-backdrop{ + display:none;position:fixed;inset:0; + background:rgba(0,0,0,.45);z-index:199; +} + @media(max-width:700px){ .sc-actions{flex-direction:column;align-items:flex-start} .sc-url{display:none} @@ -702,6 +710,33 @@ textarea{resize:vertical;min-height:88px} .filter-bar{flex-direction:column} .credential-entry{grid-template-columns:1fr 1fr} .page-header{flex-direction:column;align-items:flex-start} + + /* ── Mobile sidebar ──────────────────────────────────── */ + .sidebar{ + width:240px; + transform:translateX(-240px); + transition:transform .25s ease; + } + .sidebar.open{transform:translateX(0)} + /* Restore full labels when sidebar slides in */ + .sidebar.open .brand-name, + .sidebar.open .nav-group-label, + .sidebar.open .user-info, + .sidebar.open .sidebar-btn{display:block} + .sidebar.open .sidebar-brand{justify-content:flex-start;padding:1.1rem 1rem .9rem} + .sidebar.open .brand-icon{margin:0} + .sidebar.open .nav-item{justify-content:flex-start;padding:.65rem 1rem;gap:.6rem} + .sidebar.open .sidebar-footer{align-items:stretch} + + .sidebar-backdrop.open{display:block} + .sidebar-toggle{ + display:flex;align-items:center;justify-content:center; + position:fixed;top:.75rem;left:.75rem;z-index:201; + background:var(--bg-white);border:1px solid var(--border-strong); + border-radius:var(--r-sm);width:2.25rem;height:2.25rem; + font-size:1.1rem;cursor:pointer;box-shadow:var(--shadow-sm); + } + .main-content{margin-left:0;padding-top:3.5rem} } /* ══════════════════════════════════════════════════════════ diff --git a/static/js/app.js b/static/js/app.js index fd9d993..7d623c0 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -214,6 +214,52 @@ function getCsrfToken() { }); }()); +/* ── Mobile sidebar toggle ─────────────────────────────────── */ +(function() { + var toggle = document.getElementById('sidebar-toggle'); + var backdrop = document.getElementById('sidebar-backdrop'); + var sidebar = document.getElementById('sidebar'); + if (!toggle || !sidebar) return; + + function openSidebar() { + sidebar.classList.add('open'); + backdrop.classList.add('open'); + document.body.style.overflow = 'hidden'; + } + function closeSidebar() { + sidebar.classList.remove('open'); + backdrop.classList.remove('open'); + document.body.style.overflow = ''; + } + + toggle.addEventListener('click', openSidebar); + backdrop.addEventListener('click', closeSidebar); + sidebar.querySelectorAll('.nav-item').forEach(function(link) { + link.addEventListener('click', closeSidebar); + }); +}()); + +/* ── Relative timestamps ───────────────────────────────────── */ +function timeAgo(dateStr) { + if (!dateStr) return '—'; + var d = new Date(dateStr.replace(' ', 'T')); + var diff = Math.round((Date.now() - d.getTime()) / 1000); + if (diff < 60) return 'just now'; + if (diff < 3600) return Math.floor(diff / 60) + ' min ago'; + if (diff < 86400) return Math.floor(diff / 3600) + ' hr ago'; + if (diff < 604800) return Math.floor(diff / 86400) + ' days ago'; + return d.toLocaleDateString(); +} + +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('[data-ts]').forEach(function(el) { + var iso = el.dataset.ts; + if (!iso) return; + el.title = el.textContent.trim(); + el.textContent = timeAgo(iso); + }); +}); + /* ── Generic fetch-based form submit (JSON response) ───────── */ async function submitJson(url, data, method = 'POST') { const res = await fetch(url, { diff --git a/templates/admin/logs.html b/templates/admin/logs.html index c47caad..f412fc0 100644 --- a/templates/admin/logs.html +++ b/templates/admin/logs.html @@ -40,7 +40,7 @@
{% for row in activity_rows %}{{ row.action }}No analyses yet. Upload a document above to get started.
+ +