feat: complete session — catalog, marketplace, WhatsApp, peer-to-peer, install scripts
Major features: - Pixel-Perfect glassmorphism design (landing + POS + public catalog) - OEM/Local catalog toggle with Nexpart taxonomy (14 groups, 108 subgroups, 558 part types) - Marketplace B2B Phase 1 (bodegas, POs, status machine, WA+email notifications) - Peer-to-peer inventory (multi-instance, LAN discovery) - WhatsApp: photo→Vision AI, voice→Whisper, conversational quotations - Smart unified search (VIN/plate/part_number/keyword auto-detect) - Shop Supplies tab (vehicle-independent parts) - Chatbot AI fallback chain (5 models) + response cache - CSV inventory import tool + setup_instance.sh installer - Tablet-responsive CSS + sidebar toggle - Filters, export CSV, employee edit, business data save - Quotation system (WA→POS) with auto-print on confirmation - Live stats on landing page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,9 @@ import uuid
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'pos'))
|
||||
_base = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.join(_base, '..', 'pos')) # pos/ for auth, services
|
||||
sys.path.insert(0, os.path.join(_base, '..')) # root config.py (has DB_URL)
|
||||
from config import DB_URL
|
||||
from auth import hash_password, check_password, create_access_token, create_refresh_token, decode_token, require_auth
|
||||
from services.translations import translate_part_name, translate_category
|
||||
@@ -225,6 +226,10 @@ def public_catalog():
|
||||
def catalog_public_js():
|
||||
return send_from_directory('.', 'catalog-public.js')
|
||||
|
||||
@app.route('/landing.js')
|
||||
def landing_js():
|
||||
return send_from_directory('.', 'landing.js')
|
||||
|
||||
@app.route('/static/<path:path>')
|
||||
def static_files(path):
|
||||
return send_from_directory('static', path)
|
||||
@@ -372,8 +377,10 @@ NORTH_AMERICA_BRANDS = REGION_BRANDS['north-america']
|
||||
|
||||
@app.route('/api/catalog/brands')
|
||||
def api_catalog_brands():
|
||||
from services.catalog_modes import get_brands_for_mode, normalize_mode
|
||||
region = request.args.get('region', 'north-america')
|
||||
year_id = request.args.get('year_id', type=int)
|
||||
mode = normalize_mode(request.args.get('mode'))
|
||||
session = Session()
|
||||
try:
|
||||
params = {}
|
||||
@@ -382,7 +389,18 @@ def api_catalog_brands():
|
||||
year_filter = " AND mye.year_id = :year_id"
|
||||
params['year_id'] = year_id
|
||||
|
||||
if region == 'all':
|
||||
# 'local' mode overrides the region filter — curated bodega list only.
|
||||
if mode == 'local':
|
||||
params['brands'] = list(get_brands_for_mode('local'))
|
||||
rows = session.execute(text("""
|
||||
SELECT DISTINCT b.id_brand, b.name_brand
|
||||
FROM brands b
|
||||
JOIN models m ON m.brand_id = b.id_brand
|
||||
JOIN model_year_engine mye ON mye.model_id = m.id_model
|
||||
WHERE b.name_brand = ANY(:brands)""" + year_filter + """
|
||||
ORDER BY b.name_brand
|
||||
"""), params).mappings().all()
|
||||
elif region == 'all':
|
||||
rows = session.execute(text("""
|
||||
SELECT DISTINCT b.id_brand, b.name_brand
|
||||
FROM brands b
|
||||
@@ -472,11 +490,39 @@ def api_catalog_engines():
|
||||
session.close()
|
||||
|
||||
|
||||
def _nexpart_master_conn():
|
||||
"""Return a raw psycopg2 connection for passing to services.catalog_service
|
||||
Nexpart functions (they expect .cursor() / .commit() / .close() DBAPI).
|
||||
|
||||
Uses SQLAlchemy's connection pool so we don't open a new socket per call.
|
||||
"""
|
||||
return engine.raw_connection()
|
||||
|
||||
|
||||
@app.route('/api/catalog/categories')
|
||||
def api_catalog_categories():
|
||||
"""Categories for a vehicle.
|
||||
|
||||
OEM mode: TecDoc part_categories (integer ids).
|
||||
Local mode: 14 Nexpart top-level groups filtered by what has parts for
|
||||
this vehicle (strings as slugs).
|
||||
"""
|
||||
from services.catalog_modes import normalize_mode
|
||||
mye_id = request.args.get('mye_id', type=int)
|
||||
mode = normalize_mode(request.args.get('mode'))
|
||||
if not mye_id:
|
||||
return jsonify({'error': 'mye_id required'}), 400
|
||||
|
||||
if mode == 'local':
|
||||
from services.catalog_service import get_nexpart_groups_for_vehicle
|
||||
conn = _nexpart_master_conn()
|
||||
try:
|
||||
data = get_nexpart_groups_for_vehicle(conn, mye_id)
|
||||
finally:
|
||||
conn.close()
|
||||
return jsonify({'data': data, 'mode': 'local'})
|
||||
|
||||
# OEM mode (original behavior)
|
||||
session = Session()
|
||||
try:
|
||||
rows = session.execute(text("""
|
||||
@@ -502,10 +548,33 @@ def api_catalog_categories():
|
||||
|
||||
@app.route('/api/catalog/groups')
|
||||
def api_catalog_groups():
|
||||
"""Subgroups for a vehicle + parent category.
|
||||
|
||||
OEM mode: TecDoc part_groups (integer category_id).
|
||||
Local mode: Nexpart subgroups within a Nexpart group (category_slug string).
|
||||
"""
|
||||
from services.catalog_modes import normalize_mode
|
||||
mye_id = request.args.get('mye_id', type=int)
|
||||
category_id = request.args.get('category_id', type=int)
|
||||
if not mye_id or not category_id:
|
||||
return jsonify({'error': 'mye_id and category_id required'}), 400
|
||||
category_slug = request.args.get('category_slug')
|
||||
mode = normalize_mode(request.args.get('mode'))
|
||||
if not mye_id:
|
||||
return jsonify({'error': 'mye_id required'}), 400
|
||||
|
||||
if mode == 'local':
|
||||
if not category_slug:
|
||||
return jsonify({'error': 'category_slug required for local mode'}), 400
|
||||
from services.catalog_service import get_nexpart_subgroups_for_vehicle
|
||||
conn = _nexpart_master_conn()
|
||||
try:
|
||||
data = get_nexpart_subgroups_for_vehicle(conn, mye_id, category_slug)
|
||||
finally:
|
||||
conn.close()
|
||||
return jsonify({'data': data, 'mode': 'local'})
|
||||
|
||||
# OEM mode
|
||||
if not category_id:
|
||||
return jsonify({'error': 'category_id required for oem mode'}), 400
|
||||
session = Session()
|
||||
try:
|
||||
rows = session.execute(text("""
|
||||
@@ -526,33 +595,275 @@ def api_catalog_groups():
|
||||
session.close()
|
||||
|
||||
|
||||
@app.route('/api/catalog/parts')
|
||||
def api_catalog_parts():
|
||||
@app.route('/api/catalog/part-types')
|
||||
def api_catalog_part_types():
|
||||
"""Distinct part types within a vehicle+group (3rd subcategory level).
|
||||
|
||||
OEM mode: distinct name_part values within a TecDoc part_group_id.
|
||||
Local mode: Nexpart Part Types within a Nexpart group + subgroup.
|
||||
"""
|
||||
from services.translations import translate_part_name
|
||||
from services.catalog_modes import normalize_mode
|
||||
mye_id = request.args.get('mye_id', type=int)
|
||||
group_id = request.args.get('group_id', type=int)
|
||||
if not mye_id or not group_id:
|
||||
return jsonify({'error': 'mye_id and group_id required'}), 400
|
||||
group_slug = request.args.get('group_slug')
|
||||
subgroup_slug = request.args.get('subgroup_slug')
|
||||
mode = normalize_mode(request.args.get('mode'))
|
||||
if not mye_id:
|
||||
return jsonify({'error': 'mye_id required'}), 400
|
||||
|
||||
if mode == 'local':
|
||||
if not group_slug or not subgroup_slug:
|
||||
return jsonify({'error': 'group_slug and subgroup_slug required for local mode'}), 400
|
||||
from services.catalog_service import get_nexpart_part_types_for_vehicle
|
||||
conn = _nexpart_master_conn()
|
||||
try:
|
||||
data = get_nexpart_part_types_for_vehicle(conn, mye_id, group_slug, subgroup_slug)
|
||||
finally:
|
||||
conn.close()
|
||||
return jsonify({'data': data, 'mode': 'local'})
|
||||
|
||||
# OEM mode
|
||||
if not group_id:
|
||||
return jsonify({'error': 'group_id required for oem mode'}), 400
|
||||
session = Session()
|
||||
try:
|
||||
rows = session.execute(text("""
|
||||
SELECT
|
||||
p.name_part AS slug,
|
||||
COALESCE(p.name_es, p.name_part) AS display_name,
|
||||
COUNT(*) AS variants,
|
||||
(ARRAY_AGG(p.image_url) FILTER (WHERE p.image_url IS NOT NULL))[1] AS sample_image
|
||||
FROM vehicle_parts vp
|
||||
JOIN parts p ON p.id_part = vp.part_id
|
||||
WHERE vp.model_year_engine_id = :mye_id AND p.group_id = :group_id
|
||||
GROUP BY p.name_part, COALESCE(p.name_es, p.name_part)
|
||||
ORDER BY variants DESC, display_name ASC
|
||||
"""), {'mye_id': mye_id, 'group_id': group_id}).mappings().all()
|
||||
return jsonify({'data': [{
|
||||
'slug': r['slug'],
|
||||
'name': translate_part_name(r['display_name']),
|
||||
'variant_count': r['variants'],
|
||||
'sample_image': r['sample_image'],
|
||||
} for r in rows]})
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@app.route('/api/catalog/shop-supplies/groups')
|
||||
def api_shop_supplies_groups():
|
||||
"""Public Shop Supplies tab: vehicle-independent groups."""
|
||||
from services.catalog_service import get_shop_supplies_groups
|
||||
return jsonify({'data': get_shop_supplies_groups()})
|
||||
|
||||
|
||||
@app.route('/api/catalog/shop-supplies/subgroups')
|
||||
def api_shop_supplies_subgroups():
|
||||
group_slug = request.args.get('group_slug')
|
||||
if not group_slug:
|
||||
return jsonify({'error': 'group_slug required'}), 400
|
||||
from services.catalog_service import get_shop_supplies_subgroups
|
||||
conn = _nexpart_master_conn()
|
||||
try:
|
||||
return jsonify({'data': get_shop_supplies_subgroups(conn, group_slug)})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.route('/api/catalog/shop-supplies/part-types')
|
||||
def api_shop_supplies_part_types():
|
||||
group_slug = request.args.get('group_slug')
|
||||
subgroup_slug = request.args.get('subgroup_slug')
|
||||
if not group_slug or not subgroup_slug:
|
||||
return jsonify({'error': 'group_slug and subgroup_slug required'}), 400
|
||||
from services.catalog_service import get_shop_supplies_part_types
|
||||
conn = _nexpart_master_conn()
|
||||
try:
|
||||
return jsonify({'data': get_shop_supplies_part_types(conn, group_slug, subgroup_slug)})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.route('/api/catalog/shop-supplies/parts')
|
||||
def api_shop_supplies_parts():
|
||||
group_slug = request.args.get('group_slug')
|
||||
subgroup_slug = request.args.get('subgroup_slug')
|
||||
part_type_slug = request.args.get('part_type_slug')
|
||||
page = max(1, request.args.get('page', 1, type=int))
|
||||
per_page = min(request.args.get('per_page', 30, type=int), 100)
|
||||
if not group_slug or not subgroup_slug or not part_type_slug:
|
||||
return jsonify({'error': 'group_slug, subgroup_slug, part_type_slug required'}), 400
|
||||
from services.catalog_service import get_shop_supplies_parts
|
||||
conn = _nexpart_master_conn()
|
||||
try:
|
||||
result = get_shop_supplies_parts(
|
||||
conn, group_slug, subgroup_slug, part_type_slug,
|
||||
tenant_conn=None, branch_id=None,
|
||||
page=page, per_page=per_page,
|
||||
)
|
||||
return jsonify(result)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.route('/api/catalog/parts')
|
||||
def api_catalog_parts():
|
||||
from services.catalog_modes import (
|
||||
normalize_mode,
|
||||
LOCAL_PRIORITY_MANUFACTURERS_TIER1,
|
||||
LOCAL_PRIORITY_MANUFACTURERS_TIER2,
|
||||
)
|
||||
mye_id = request.args.get('mye_id', type=int)
|
||||
group_id = request.args.get('group_id', type=int)
|
||||
part_type = request.args.get('part_type')
|
||||
|
||||
# Nexpart navigation slugs (Local mode, chosen via new Nexpart hierarchy)
|
||||
nexpart_group = request.args.get('nexpart_group')
|
||||
nexpart_subgroup = request.args.get('nexpart_subgroup')
|
||||
nexpart_part_type = request.args.get('nexpart_part_type')
|
||||
|
||||
if not mye_id:
|
||||
return jsonify({'error': 'mye_id required'}), 400
|
||||
page = max(1, request.args.get('page', 1, type=int))
|
||||
per_page = min(request.args.get('per_page', 30, type=int), 100)
|
||||
offset = (page - 1) * per_page
|
||||
mode = normalize_mode(request.args.get('mode'))
|
||||
|
||||
# ─── Nexpart-nav dispatch (delegates to POS service layer) ───
|
||||
# Public catalog has no tenant context — pass tenant_conn=None which
|
||||
# the service gracefully handles (no local stock / price enrichment).
|
||||
if mode == 'local' and nexpart_group and nexpart_subgroup and nexpart_part_type:
|
||||
from services.catalog_service import get_parts_for_nexpart_triple
|
||||
conn = _nexpart_master_conn()
|
||||
try:
|
||||
result = get_parts_for_nexpart_triple(
|
||||
conn, mye_id,
|
||||
nexpart_group, nexpart_subgroup, nexpart_part_type,
|
||||
tenant_conn=None, branch_id=None,
|
||||
page=page, per_page=per_page,
|
||||
)
|
||||
return jsonify(result)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# Below here: legacy group_id based flow (OEM or legacy Local)
|
||||
if not group_id:
|
||||
return jsonify({'error': 'group_id required'}), 400
|
||||
|
||||
session = Session()
|
||||
try:
|
||||
# Optional 3rd-level Part Type filter (applies to both OEM and Local modes)
|
||||
pt_clause = " AND p.name_part = :part_type" if part_type else ""
|
||||
|
||||
if mode == 'local':
|
||||
# Aftermarket-oriented listing, prioritized + stock-aware.
|
||||
count_params = {'mye_id': mye_id, 'group_id': group_id}
|
||||
if part_type:
|
||||
count_params['part_type'] = part_type
|
||||
total = session.execute(text("""
|
||||
SELECT COUNT(*)
|
||||
FROM vehicle_parts vp
|
||||
JOIN parts p ON p.id_part = vp.part_id
|
||||
JOIN aftermarket_parts ap ON ap.oem_part_id = p.id_part
|
||||
JOIN manufacturers m ON m.id_manufacture = ap.manufacturer_id
|
||||
WHERE vp.model_year_engine_id = :mye_id AND p.group_id = :group_id""" + pt_clause), count_params).scalar()
|
||||
|
||||
fetch_params = {
|
||||
'mye_id': mye_id,
|
||||
'group_id': group_id,
|
||||
'tier1': list(LOCAL_PRIORITY_MANUFACTURERS_TIER1),
|
||||
'tier2': list(LOCAL_PRIORITY_MANUFACTURERS_TIER2),
|
||||
'limit': per_page,
|
||||
'offset': offset,
|
||||
}
|
||||
if part_type:
|
||||
fetch_params['part_type'] = part_type
|
||||
|
||||
rows = session.execute(text("""
|
||||
WITH aftermarket_for_vehicle AS (
|
||||
SELECT DISTINCT
|
||||
ap.id_aftermarket_parts,
|
||||
ap.oem_part_id,
|
||||
ap.part_number,
|
||||
COALESCE(ap.name_es, ap.name_aftermarket_parts) AS am_name,
|
||||
m.name_manufacture,
|
||||
p.oem_part_number,
|
||||
COALESCE(p.name_es, p.name_part) AS oem_name,
|
||||
COALESCE(p.description_es, p.description) AS oem_desc,
|
||||
p.image_url AS oem_image
|
||||
FROM vehicle_parts vp
|
||||
JOIN parts p ON p.id_part = vp.part_id
|
||||
JOIN aftermarket_parts ap ON ap.oem_part_id = p.id_part
|
||||
JOIN manufacturers m ON m.id_manufacture = ap.manufacturer_id
|
||||
WHERE vp.model_year_engine_id = :mye_id AND p.group_id = :group_id""" + pt_clause + """
|
||||
),
|
||||
stock_per_oem AS (
|
||||
SELECT part_id, COUNT(*) AS bodega_count, MIN(price) AS min_price, SUM(stock_quantity) AS total_stock
|
||||
FROM warehouse_inventory
|
||||
WHERE stock_quantity > 0
|
||||
GROUP BY part_id
|
||||
)
|
||||
SELECT afv.id_aftermarket_parts, afv.oem_part_id, afv.part_number, afv.am_name,
|
||||
afv.name_manufacture, afv.oem_part_number, afv.oem_name, afv.oem_desc, afv.oem_image,
|
||||
COALESCE(s.bodega_count, 0) AS bodega_count,
|
||||
s.min_price AS warehouse_price,
|
||||
COALESCE(s.total_stock, 0) AS warehouse_stock,
|
||||
CASE
|
||||
WHEN UPPER(afv.name_manufacture) = ANY(:tier1) THEN 1
|
||||
WHEN UPPER(afv.name_manufacture) = ANY(:tier2) THEN 2
|
||||
ELSE 3
|
||||
END AS tier
|
||||
FROM aftermarket_for_vehicle afv
|
||||
LEFT JOIN stock_per_oem s ON s.part_id = afv.oem_part_id
|
||||
ORDER BY tier ASC,
|
||||
(COALESCE(s.bodega_count, 0) > 0) DESC,
|
||||
afv.name_manufacture ASC,
|
||||
afv.am_name ASC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""), fetch_params).mappings().all()
|
||||
|
||||
items = [{
|
||||
'id_part': r['oem_part_id'],
|
||||
'id_aftermarket': r['id_aftermarket_parts'],
|
||||
'oem_part_number': r['oem_part_number'],
|
||||
'part_number': r['part_number'],
|
||||
'name': translate_part_name(r['am_name'] or r['oem_name']),
|
||||
'description': r['oem_desc'],
|
||||
'image_url': r['oem_image'],
|
||||
'manufacturer': r['name_manufacture'],
|
||||
'priority_tier': r['tier'],
|
||||
'bodega_count': r['bodega_count'],
|
||||
'warehouse_stock': r['warehouse_stock'],
|
||||
'warehouse_price': float(r['warehouse_price']) if r['warehouse_price'] is not None else None,
|
||||
'in_stock_network': r['bodega_count'] > 0,
|
||||
} for r in rows]
|
||||
|
||||
total_pages = max(1, (total + per_page - 1) // per_page)
|
||||
return jsonify({'data': items, 'mode': 'local', 'pagination': {
|
||||
'page': page, 'per_page': per_page, 'total': total, 'total_pages': total_pages
|
||||
}})
|
||||
|
||||
# OEM mode (original behavior)
|
||||
oem_count_params = {'mye_id': mye_id, 'group_id': group_id}
|
||||
oem_fetch_params = {'mye_id': mye_id, 'group_id': group_id, 'limit': per_page, 'offset': offset}
|
||||
if part_type:
|
||||
oem_count_params['part_type'] = part_type
|
||||
oem_fetch_params['part_type'] = part_type
|
||||
total = session.execute(text("""
|
||||
SELECT COUNT(*)
|
||||
FROM vehicle_parts vp
|
||||
JOIN parts p ON p.id_part = vp.part_id
|
||||
WHERE vp.model_year_engine_id = :mye_id AND p.group_id = :group_id
|
||||
"""), {'mye_id': mye_id, 'group_id': group_id}).scalar()
|
||||
WHERE vp.model_year_engine_id = :mye_id AND p.group_id = :group_id""" + pt_clause), oem_count_params).scalar()
|
||||
|
||||
rows = session.execute(text("""
|
||||
SELECT p.id_part, p.oem_part_number, p.name_part, p.name_es,
|
||||
p.description, p.description_es, p.image_url
|
||||
FROM vehicle_parts vp
|
||||
JOIN parts p ON p.id_part = vp.part_id
|
||||
WHERE vp.model_year_engine_id = :mye_id AND p.group_id = :group_id
|
||||
WHERE vp.model_year_engine_id = :mye_id AND p.group_id = :group_id""" + pt_clause + """
|
||||
ORDER BY p.name_part
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""), {'mye_id': mye_id, 'group_id': group_id, 'limit': per_page, 'offset': offset}).mappings().all()
|
||||
"""), oem_fetch_params).mappings().all()
|
||||
|
||||
items = [{
|
||||
'id_part': r['id_part'],
|
||||
@@ -563,7 +874,7 @@ def api_catalog_parts():
|
||||
} for r in rows]
|
||||
|
||||
total_pages = max(1, (total + per_page - 1) // per_page)
|
||||
return jsonify({'data': items, 'pagination': {
|
||||
return jsonify({'data': items, 'mode': 'oem', 'pagination': {
|
||||
'page': page, 'per_page': per_page, 'total': total, 'total_pages': total_pages
|
||||
}})
|
||||
finally:
|
||||
@@ -660,6 +971,7 @@ def api_catalog_search():
|
||||
|
||||
if is_part_number:
|
||||
clean_q = q.replace(' ', '').upper()
|
||||
# Search OEM part numbers first
|
||||
rows = session.execute(text("""
|
||||
SELECT p.id_part, p.oem_part_number, p.name_part, p.name_es,
|
||||
p.image_url
|
||||
@@ -668,6 +980,28 @@ def api_catalog_search():
|
||||
ORDER BY p.oem_part_number
|
||||
LIMIT :limit
|
||||
"""), {'q': f'%{clean_q}%', 'limit': limit}).mappings().all()
|
||||
|
||||
# If no OEM match, search aftermarket + cross-reference numbers
|
||||
if not rows:
|
||||
rows = session.execute(text("""
|
||||
SELECT DISTINCT p.id_part, p.oem_part_number, p.name_part, p.name_es,
|
||||
p.image_url
|
||||
FROM parts p
|
||||
JOIN aftermarket_parts ap ON ap.oem_part_id = p.id_part
|
||||
WHERE REPLACE(UPPER(ap.part_number), ' ', '') LIKE :q
|
||||
ORDER BY p.oem_part_number
|
||||
LIMIT :limit
|
||||
"""), {'q': f'%{clean_q}%', 'limit': limit}).mappings().all()
|
||||
if not rows:
|
||||
rows = session.execute(text("""
|
||||
SELECT DISTINCT p.id_part, p.oem_part_number, p.name_part, p.name_es,
|
||||
p.image_url
|
||||
FROM parts p
|
||||
JOIN part_cross_references cr ON cr.part_id = p.id_part
|
||||
WHERE REPLACE(UPPER(cr.cross_reference_number), ' ', '') LIKE :q
|
||||
ORDER BY p.oem_part_number
|
||||
LIMIT :limit
|
||||
"""), {'q': f'%{clean_q}%', 'limit': limit}).mappings().all()
|
||||
else:
|
||||
tsquery = ' & '.join(q.split())
|
||||
rows = session.execute(text("""
|
||||
@@ -723,19 +1057,46 @@ def api_catalog_search():
|
||||
|
||||
@app.route('/api/catalog/stats')
|
||||
def api_catalog_stats():
|
||||
"""Public stats endpoint consumed by the landing page hero cards.
|
||||
|
||||
Returns live counts from the master catalog so the landing never shows
|
||||
stale numbers. Counts are fast (pg_class reltuples for the big tables)
|
||||
and cached in-process for 10 minutes.
|
||||
"""
|
||||
# In-process cache — counts barely change and are called on every
|
||||
# landing pageview; no reason to hit the DB every time.
|
||||
import time as _t
|
||||
now = _t.time()
|
||||
cache = getattr(api_catalog_stats, '_cache', None)
|
||||
if cache and cache[0] > now:
|
||||
return jsonify(cache[1])
|
||||
|
||||
session = Session()
|
||||
try:
|
||||
# Use reltuples for large tables (parts, aftermarket_parts,
|
||||
# part_cross_references) — exact COUNT(*) on 1M+ row tables is slow
|
||||
# and the landing doesn't need exact accuracy, just "big numbers".
|
||||
row = session.execute(text("""
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM brands) AS brands,
|
||||
(SELECT COUNT(*) FROM models) AS models,
|
||||
(SELECT COUNT(*) FROM model_year_engine) AS vehicles,
|
||||
(SELECT COUNT(*) FROM parts) AS parts
|
||||
(SELECT reltuples::bigint FROM pg_class WHERE relname = 'parts') AS parts,
|
||||
(SELECT reltuples::bigint FROM pg_class WHERE relname = 'aftermarket_parts') AS aftermarket_parts,
|
||||
(SELECT reltuples::bigint FROM pg_class WHERE relname = 'part_cross_references') AS cross_references,
|
||||
(SELECT COUNT(*) FROM manufacturers) AS manufacturers
|
||||
""")).mappings().first()
|
||||
return jsonify({
|
||||
'brands': row['brands'], 'models': row['models'],
|
||||
'vehicles': row['vehicles'], 'parts': row['parts']
|
||||
})
|
||||
data = {
|
||||
'brands': row['brands'] or 0,
|
||||
'models': row['models'] or 0,
|
||||
'vehicles': row['vehicles'] or 0,
|
||||
'parts': row['parts'] or 0,
|
||||
'aftermarket_parts': row['aftermarket_parts'] or 0,
|
||||
'cross_references': row['cross_references'] or 0,
|
||||
'manufacturers': row['manufacturers'] or 0,
|
||||
}
|
||||
api_catalog_stats._cache = (now + 600, data)
|
||||
return jsonify(data)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@@ -3581,9 +3942,13 @@ def api_pos_search_parts():
|
||||
# Store Dashboard Endpoints
|
||||
# ============================================================================
|
||||
|
||||
# Old page routes removed (demo, bodega, pitch, login, tienda)
|
||||
# Old page routes removed (demo, bodega, login, tienda)
|
||||
# APIs below are kept for backward compatibility
|
||||
|
||||
@app.route('/pitch')
|
||||
def pitch_deck():
|
||||
return send_from_directory(os.path.join(_base, '..', 'pitch'), 'deck.html')
|
||||
|
||||
|
||||
@app.route('/api/tienda/stats')
|
||||
def api_tienda_stats():
|
||||
|
||||
Reference in New Issue
Block a user