Files
Autoparts-DB/pos/blueprints/diagrams_bp.py
consultoria-as c333f2eaf0 feat(pos): add exploded diagrams feature (#9) with interactive SVG viewer
- Blueprint with API endpoints: list, detail, SVG serve, vehicle-linked diagrams
- Interactive SVG viewer with zoom/pan (mouse wheel, drag, touch, keyboard)
- Clickable hotspots that highlight on hover and show part detail panel
- Parts sidebar listing all callout numbers with catalog search integration
- 3 placeholder SVG diagrams: braking system, suspension, engine components
- Seeded diagrams, hotspots, and vehicle_diagrams in DB
- Added to sidebar nav, i18n (ES/EN), and "Ver diagramas" link in catalog

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 04:24:19 +00:00

293 lines
10 KiB
Python

# /home/Autopartes/pos/blueprints/diagrams_bp.py
"""Diagrams blueprint: exploded-view SVG diagrams with clickable hotspots.
Endpoints (all under /pos/api/diagrams):
GET / -- list all available diagrams (optionally filter by mye_id or group_id)
GET /<id> -- get diagram detail with hotspots
GET /<id>/svg -- serve the SVG image file
GET /for-vehicle?mye_id= -- diagrams linked to a specific vehicle (MYE)
"""
from flask import Blueprint, request, jsonify, send_from_directory, current_app
from middleware import require_auth
from tenant_db import get_master_conn
import os
diagrams_bp = Blueprint('diagrams', __name__, url_prefix='/pos/api/diagrams')
def _master_only(fn):
"""Helper: open only master connection."""
master = None
try:
master = get_master_conn()
return fn(master)
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
if master:
try:
master.close()
except Exception:
pass
@diagrams_bp.route('/', methods=['GET'])
@require_auth('catalog.view')
def list_diagrams():
"""List diagrams, optionally filtered by mye_id or group_id."""
mye_id = request.args.get('mye_id', type=int)
group_id = request.args.get('group_id', type=int)
category_id = request.args.get('category_id', type=int)
def _do(master):
cur = master.cursor()
if mye_id:
# Diagrams linked to a specific vehicle
cur.execute("""
SELECT d.id_diagram, d.name_diagram, d.name_es,
d.group_id, d.image_path, d.display_order,
pg.name_part_group, pg.name_es AS group_name_es,
pc.name_part_category, pc.name_es AS category_name_es,
vd.notes
FROM diagrams d
JOIN vehicle_diagrams vd ON vd.diagram_id = d.id_diagram
JOIN part_groups pg ON pg.id_part_group = d.group_id
JOIN part_categories pc ON pc.id_part_category = pg.category_id
WHERE vd.model_year_engine_id = %s
ORDER BY d.display_order, d.name_es, d.name_diagram
""", (mye_id,))
elif group_id:
cur.execute("""
SELECT d.id_diagram, d.name_diagram, d.name_es,
d.group_id, d.image_path, d.display_order,
pg.name_part_group, pg.name_es AS group_name_es,
pc.name_part_category, pc.name_es AS category_name_es,
NULL AS notes
FROM diagrams d
JOIN part_groups pg ON pg.id_part_group = d.group_id
JOIN part_categories pc ON pc.id_part_category = pg.category_id
WHERE d.group_id = %s
ORDER BY d.display_order, d.name_es
""", (group_id,))
elif category_id:
cur.execute("""
SELECT d.id_diagram, d.name_diagram, d.name_es,
d.group_id, d.image_path, d.display_order,
pg.name_part_group, pg.name_es AS group_name_es,
pc.name_part_category, pc.name_es AS category_name_es,
NULL AS notes
FROM diagrams d
JOIN part_groups pg ON pg.id_part_group = d.group_id
JOIN part_categories pc ON pc.id_part_category = pg.category_id
WHERE pg.category_id = %s
ORDER BY d.display_order, d.name_es
""", (category_id,))
else:
# All diagrams
cur.execute("""
SELECT d.id_diagram, d.name_diagram, d.name_es,
d.group_id, d.image_path, d.display_order,
pg.name_part_group, pg.name_es AS group_name_es,
pc.name_part_category, pc.name_es AS category_name_es,
NULL AS notes
FROM diagrams d
JOIN part_groups pg ON pg.id_part_group = d.group_id
JOIN part_categories pc ON pc.id_part_category = pg.category_id
ORDER BY pc.name_part_category, d.display_order, d.name_es
LIMIT 200
""")
rows = cur.fetchall()
data = []
for r in rows:
data.append({
'id_diagram': r[0],
'name': r[1],
'name_es': r[2] or r[1],
'group_id': r[3],
'image_path': r[4],
'display_order': r[5],
'group_name': r[6],
'group_name_es': r[7] or r[6],
'category_name': r[8],
'category_name_es': r[9] or r[8],
'notes': r[10],
})
cur.close()
return jsonify({'data': data, 'count': len(data)})
return _master_only(_do)
@diagrams_bp.route('/<int:diagram_id>', methods=['GET'])
@require_auth('catalog.view')
def get_diagram(diagram_id):
"""Get a single diagram with its hotspots."""
def _do(master):
cur = master.cursor()
# Diagram info
cur.execute("""
SELECT d.id_diagram, d.name_diagram, d.name_es,
d.group_id, d.image_path, d.display_order,
pg.name_part_group, pg.name_es AS group_name_es,
pc.id_part_category, pc.name_part_category, pc.name_es AS category_name_es
FROM diagrams d
JOIN part_groups pg ON pg.id_part_group = d.group_id
JOIN part_categories pc ON pc.id_part_category = pg.category_id
WHERE d.id_diagram = %s
""", (diagram_id,))
row = cur.fetchone()
if not row:
cur.close()
return jsonify({'error': 'Diagram not found'}), 404
diagram = {
'id_diagram': row[0],
'name': row[1],
'name_es': row[2] or row[1],
'group_id': row[3],
'image_path': row[4],
'display_order': row[5],
'group_name': row[6],
'group_name_es': row[7] or row[6],
'category_id': row[8],
'category_name': row[9],
'category_name_es': row[10] or row[9],
}
# Hotspots
cur.execute("""
SELECT h.id_dgr_hotspot, h.callout_number, h.part_id,
h.coords, s.name_shape,
p.oem_part_number, p.name_part, p.name_es AS part_name_es,
p.description, p.description_es
FROM diagram_hotspots h
LEFT JOIN shapes s ON s.id_shape = h.id_shape
LEFT JOIN parts p ON p.id_part = h.part_id
WHERE h.diagram_id = %s
ORDER BY h.callout_number
""", (diagram_id,))
hotspots = []
for h in cur.fetchall():
hotspots.append({
'id': h[0],
'callout_number': h[1],
'part_id': h[2],
'coords': h[3],
'shape': h[4] or 'rect',
'part_number': h[5],
'part_name': h[6],
'part_name_es': h[7] or h[6],
'description': h[8],
'description_es': h[9] or h[8],
})
diagram['hotspots'] = hotspots
# Linked vehicles count
cur.execute(
"SELECT count(*) FROM vehicle_diagrams WHERE diagram_id = %s",
(diagram_id,)
)
diagram['vehicle_count'] = cur.fetchone()[0]
cur.close()
return jsonify(diagram)
return _master_only(_do)
@diagrams_bp.route('/<int:diagram_id>/svg', methods=['GET'])
@require_auth('catalog.view')
def get_diagram_svg(diagram_id):
"""Serve the SVG image file for a diagram."""
def _do(master):
cur = master.cursor()
cur.execute(
"SELECT image_path FROM diagrams WHERE id_diagram = %s",
(diagram_id,)
)
row = cur.fetchone()
cur.close()
if not row:
return jsonify({'error': 'Diagram not found'}), 404
image_path = row[0]
static_dir = os.path.join(current_app.root_path, 'static')
full_path = os.path.join(static_dir, image_path)
if not os.path.isfile(full_path):
return jsonify({'error': 'SVG file not found'}), 404
directory = os.path.dirname(full_path)
filename = os.path.basename(full_path)
return send_from_directory(directory, filename, mimetype='image/svg+xml')
return _master_only(_do)
@diagrams_bp.route('/for-vehicle', methods=['GET'])
@require_auth('catalog.view')
def diagrams_for_vehicle():
"""Get diagrams linked to a specific vehicle MYE, grouped by category."""
mye_id = request.args.get('mye_id', type=int)
if not mye_id:
return jsonify({'error': 'mye_id required'}), 400
def _do(master):
cur = master.cursor()
cur.execute("""
SELECT d.id_diagram, d.name_diagram, d.name_es,
d.group_id, d.image_path, d.display_order,
pg.name_part_group, pg.name_es AS group_name_es,
pc.id_part_category, pc.name_part_category, pc.name_es AS category_name_es,
(SELECT count(*) FROM diagram_hotspots dh WHERE dh.diagram_id = d.id_diagram) AS hotspot_count
FROM diagrams d
JOIN vehicle_diagrams vd ON vd.diagram_id = d.id_diagram
JOIN part_groups pg ON pg.id_part_group = d.group_id
JOIN part_categories pc ON pc.id_part_category = pg.category_id
WHERE vd.model_year_engine_id = %s
ORDER BY pc.name_part_category, d.display_order, d.name_es
""", (mye_id,))
rows = cur.fetchall()
# Group by category
categories = {}
for r in rows:
cat_id = r[8]
if cat_id not in categories:
categories[cat_id] = {
'category_id': cat_id,
'category_name': r[9],
'category_name_es': r[10] or r[9],
'diagrams': [],
}
categories[cat_id]['diagrams'].append({
'id_diagram': r[0],
'name': r[1],
'name_es': r[2] or r[1],
'group_id': r[3],
'image_path': r[4],
'display_order': r[5],
'group_name': r[6],
'group_name_es': r[7] or r[6],
'hotspot_count': r[11],
})
cur.close()
return jsonify({
'data': list(categories.values()),
'total_diagrams': len(rows),
})
return _master_only(_do)