- 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>
293 lines
10 KiB
Python
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)
|