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>
This commit is contained in:
@@ -54,6 +54,9 @@ def create_app():
|
||||
from blueprints.marketplace_bp import marketplace_bp
|
||||
app.register_blueprint(marketplace_bp)
|
||||
|
||||
from blueprints.diagrams_bp import diagrams_bp
|
||||
app.register_blueprint(diagrams_bp)
|
||||
|
||||
# Health check
|
||||
@app.route('/pos/health')
|
||||
def health():
|
||||
@@ -120,6 +123,10 @@ def create_app():
|
||||
def pos_marketplace():
|
||||
return render_template('marketplace.html')
|
||||
|
||||
@app.route('/pos/diagrams')
|
||||
def pos_diagrams():
|
||||
return render_template('diagrams.html')
|
||||
|
||||
@app.route('/pos/static/<path:filename>')
|
||||
def pos_static(filename):
|
||||
return send_from_directory('static', filename)
|
||||
|
||||
292
pos/blueprints/diagrams_bp.py
Normal file
292
pos/blueprints/diagrams_bp.py
Normal file
@@ -0,0 +1,292 @@
|
||||
# /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)
|
||||
103
pos/static/diagrams/braking-system.svg
Normal file
103
pos/static/diagrams/braking-system.svg
Normal file
@@ -0,0 +1,103 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" fill="none" stroke-width="2">
|
||||
<defs>
|
||||
<style>
|
||||
.part-area { fill: transparent; stroke: none; cursor: pointer; }
|
||||
.part-area:hover { fill: rgba(245, 166, 35, 0.15); stroke: #F5A623; stroke-width: 2; }
|
||||
.label { font-family: 'Inter', sans-serif; font-size: 13px; fill: #666; }
|
||||
.callout { font-family: 'Inter', sans-serif; font-size: 16px; font-weight: 700; fill: #F5A623; }
|
||||
.line { stroke: #999; stroke-width: 1; stroke-dasharray: 4 2; }
|
||||
.component { stroke: #444; fill: #e8e8e8; }
|
||||
.component-detail { stroke: #666; fill: #d0d0d0; }
|
||||
.component-accent { stroke: #444; fill: #F5A623; opacity: 0.3; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="400" y="35" text-anchor="middle" font-family="Inter, sans-serif" font-size="22" font-weight="700" fill="#333">Sistema de Frenos - Vista Explodida</text>
|
||||
|
||||
<!-- Brake disc (rotor) -->
|
||||
<g id="part-disc">
|
||||
<ellipse cx="400" cy="320" rx="130" ry="130" class="component" stroke-width="3"/>
|
||||
<ellipse cx="400" cy="320" rx="110" ry="110" stroke="#888" fill="none" stroke-width="1"/>
|
||||
<ellipse cx="400" cy="320" rx="45" ry="45" class="component-detail"/>
|
||||
<!-- Ventilation slots -->
|
||||
<line x1="310" y1="290" x2="350" y2="290" stroke="#aaa" stroke-width="1"/>
|
||||
<line x1="310" y1="310" x2="350" y2="310" stroke="#aaa" stroke-width="1"/>
|
||||
<line x1="310" y1="330" x2="350" y2="330" stroke="#aaa" stroke-width="1"/>
|
||||
<line x1="310" y1="350" x2="350" y2="350" stroke="#aaa" stroke-width="1"/>
|
||||
<line x1="450" y1="290" x2="490" y2="290" stroke="#aaa" stroke-width="1"/>
|
||||
<line x1="450" y1="310" x2="490" y2="310" stroke="#aaa" stroke-width="1"/>
|
||||
<line x1="450" y1="330" x2="490" y2="330" stroke="#aaa" stroke-width="1"/>
|
||||
<line x1="450" y1="350" x2="490" y2="350" stroke="#aaa" stroke-width="1"/>
|
||||
<!-- Mounting holes -->
|
||||
<circle cx="400" cy="280" r="5" fill="#999" stroke="#666"/>
|
||||
<circle cx="380" cy="285" r="5" fill="#999" stroke="#666"/>
|
||||
<circle cx="420" cy="285" r="5" fill="#999" stroke="#666"/>
|
||||
<circle cx="385" cy="360" r="5" fill="#999" stroke="#666"/>
|
||||
<circle cx="415" cy="360" r="5" fill="#999" stroke="#666"/>
|
||||
<rect class="part-area" data-hotspot="1" x="270" y="190" width="260" height="260" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Brake caliper (exploded out to right) -->
|
||||
<g id="part-caliper">
|
||||
<line x1="530" y1="320" x2="580" y2="250" class="line"/>
|
||||
<rect x="580" y="200" width="120" height="100" rx="12" class="component" stroke-width="2.5"/>
|
||||
<rect x="595" y="215" width="90" height="30" rx="4" class="component-detail"/>
|
||||
<rect x="595" y="255" width="90" height="30" rx="4" class="component-detail"/>
|
||||
<!-- Piston -->
|
||||
<circle cx="640" cy="230" r="10" fill="#bbb" stroke="#888"/>
|
||||
<circle cx="640" cy="270" r="10" fill="#bbb" stroke="#888"/>
|
||||
<!-- Bleeder valve -->
|
||||
<rect x="705" y="230" width="20" height="10" rx="3" fill="#999" stroke="#666"/>
|
||||
<rect class="part-area" data-hotspot="2" x="575" y="195" width="155" height="115" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Brake pads (exploded above) -->
|
||||
<g id="part-pads">
|
||||
<line x1="400" y1="190" x2="400" y2="120" class="line"/>
|
||||
<rect x="340" y="60" width="120" height="55" rx="6" class="component" stroke-width="2"/>
|
||||
<rect x="348" y="68" width="104" height="20" rx="3" class="component-accent"/>
|
||||
<!-- Friction material -->
|
||||
<rect x="348" y="93" width="104" height="15" rx="2" fill="#a0522d" stroke="#8b4513" stroke-width="1"/>
|
||||
<!-- Wear indicator slot -->
|
||||
<rect x="370" y="100" width="4" height="8" fill="#F5A623"/>
|
||||
<rect class="part-area" data-hotspot="3" x="335" y="55" width="130" height="65" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Brake hose (exploded to left) -->
|
||||
<g id="part-hose">
|
||||
<line x1="270" y1="320" x2="180" y2="280" class="line"/>
|
||||
<path d="M80,240 Q100,220 120,240 Q140,260 160,240 Q180,220 180,260" stroke="#333" fill="none" stroke-width="4" stroke-linecap="round"/>
|
||||
<!-- Fittings -->
|
||||
<rect x="65" y="232" width="20" height="16" rx="3" fill="#999" stroke="#666"/>
|
||||
<rect x="175" y="252" width="20" height="16" rx="3" fill="#999" stroke="#666"/>
|
||||
<rect class="part-area" data-hotspot="4" x="60" y="215" width="145" height="60" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Master cylinder (top-left) -->
|
||||
<g id="part-master">
|
||||
<line x1="270" y1="200" x2="180" y2="120" class="line"/>
|
||||
<rect x="50" y="80" width="140" height="50" rx="8" class="component" stroke-width="2.5"/>
|
||||
<circle cx="80" cy="105" r="12" class="component-detail"/>
|
||||
<circle cx="120" cy="105" r="12" class="component-detail"/>
|
||||
<!-- Reservoir -->
|
||||
<rect x="85" y="55" width="60" height="25" rx="5" fill="#cde" stroke="#89a"/>
|
||||
<!-- Pushrod -->
|
||||
<rect x="190" y="97" width="30" height="16" rx="3" fill="#bbb" stroke="#888"/>
|
||||
<rect class="part-area" data-hotspot="5" x="45" y="50" width="180" height="85" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Callout numbers -->
|
||||
<circle cx="400" cy="475" r="14" fill="#F5A623"/><text class="callout" x="400" y="480" text-anchor="middle" fill="white">1</text>
|
||||
<circle cx="720" cy="175" r="14" fill="#F5A623"/><text class="callout" x="720" y="180" text-anchor="middle" fill="white">2</text>
|
||||
<circle cx="400" cy="45" r="14" fill="#F5A623"/><text class="callout" x="400" y="50" text-anchor="middle" fill="white">3</text>
|
||||
<circle cx="65" cy="200" r="14" fill="#F5A623"/><text class="callout" x="65" y="205" text-anchor="middle" fill="white">4</text>
|
||||
<circle cx="135" cy="45" r="14" fill="#F5A623"/><text class="callout" x="135" y="50" text-anchor="middle" fill="white">5</text>
|
||||
|
||||
<!-- Labels -->
|
||||
<text class="label" x="400" y="500" text-anchor="middle">Disco de freno</text>
|
||||
<text class="label" x="720" y="195" text-anchor="middle">Caliper</text>
|
||||
<text class="label" x="400" y="30" text-anchor="middle" dy="-10">Pastillas de freno</text>
|
||||
<text class="label" x="65" y="220" text-anchor="middle">Manguera de freno</text>
|
||||
<text class="label" x="135" y="30" text-anchor="middle" dy="-10">Cilindro maestro</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
117
pos/static/diagrams/engine-system.svg
Normal file
117
pos/static/diagrams/engine-system.svg
Normal file
@@ -0,0 +1,117 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" fill="none" stroke-width="2">
|
||||
<defs>
|
||||
<style>
|
||||
.part-area { fill: transparent; stroke: none; cursor: pointer; }
|
||||
.part-area:hover { fill: rgba(245, 166, 35, 0.15); stroke: #F5A623; stroke-width: 2; }
|
||||
.label { font-family: 'Inter', sans-serif; font-size: 13px; fill: #666; }
|
||||
.callout { font-family: 'Inter', sans-serif; font-size: 16px; font-weight: 700; fill: #F5A623; }
|
||||
.line { stroke: #999; stroke-width: 1; stroke-dasharray: 4 2; }
|
||||
.component { stroke: #444; fill: #e8e8e8; }
|
||||
.component-detail { stroke: #666; fill: #d0d0d0; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<text x="400" y="35" text-anchor="middle" font-family="Inter, sans-serif" font-size="22" font-weight="700" fill="#333">Motor - Componentes Principales</text>
|
||||
|
||||
<!-- Engine block (center) -->
|
||||
<g id="engine-block">
|
||||
<rect x="250" y="180" width="300" height="250" rx="8" class="component" stroke-width="3"/>
|
||||
<!-- Cylinder bores -->
|
||||
<rect x="280" y="200" width="50" height="80" rx="4" class="component-detail"/>
|
||||
<rect x="340" y="200" width="50" height="80" rx="4" class="component-detail"/>
|
||||
<rect x="400" y="200" width="50" height="80" rx="4" class="component-detail"/>
|
||||
<rect x="460" y="200" width="50" height="80" rx="4" class="component-detail"/>
|
||||
<!-- Oil pan area -->
|
||||
<rect x="260" y="380" width="280" height="40" rx="4" fill="#ddd" stroke="#999"/>
|
||||
</g>
|
||||
|
||||
<!-- Air filter (top-left, exploded) -->
|
||||
<g id="part-filter">
|
||||
<line x1="270" y1="180" x2="130" y2="100" class="line"/>
|
||||
<ellipse cx="100" cy="75" rx="60" ry="30" class="component" stroke-width="2"/>
|
||||
<ellipse cx="100" cy="75" rx="45" ry="20" fill="#c4a35a" stroke="#a08030" stroke-width="1"/>
|
||||
<!-- Pleats -->
|
||||
<line x1="65" y1="70" x2="65" y2="80" stroke="#b89040" stroke-width="1"/>
|
||||
<line x1="80" y1="65" x2="80" y2="85" stroke="#b89040" stroke-width="1"/>
|
||||
<line x1="95" y1="63" x2="95" y2="87" stroke="#b89040" stroke-width="1"/>
|
||||
<line x1="110" y1="63" x2="110" y2="87" stroke="#b89040" stroke-width="1"/>
|
||||
<line x1="125" y1="65" x2="125" y2="85" stroke="#b89040" stroke-width="1"/>
|
||||
<line x1="135" y1="70" x2="135" y2="80" stroke="#b89040" stroke-width="1"/>
|
||||
<rect class="part-area" data-hotspot="1" x="35" y="40" width="130" height="70" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Spark plugs (top, exploded) -->
|
||||
<g id="part-spark-plugs">
|
||||
<line x1="400" y1="180" x2="400" y2="100" class="line"/>
|
||||
<!-- Spark plug -->
|
||||
<rect x="385" y="50" width="30" height="50" rx="3" fill="#bbb" stroke="#888"/>
|
||||
<!-- Ceramic insulator -->
|
||||
<rect x="392" y="40" width="16" height="20" rx="4" fill="#f0e8d0" stroke="#c0b090"/>
|
||||
<!-- Terminal -->
|
||||
<rect x="397" y="30" width="6" height="12" rx="1" fill="#999" stroke="#777"/>
|
||||
<!-- Electrode -->
|
||||
<path d="M398,100 L398,108 L405,108" stroke="#888" stroke-width="2"/>
|
||||
<!-- Second plug hint -->
|
||||
<rect x="430" y="53" width="25" height="44" rx="3" fill="#bbb" stroke="#888" opacity="0.5"/>
|
||||
<rect x="340" y="53" width="25" height="44" rx="3" fill="#bbb" stroke="#888" opacity="0.5"/>
|
||||
<rect class="part-area" data-hotspot="2" x="330" y="25" width="135" height="85" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Belt (right side, exploded) -->
|
||||
<g id="part-belt">
|
||||
<line x1="550" y1="280" x2="610" y2="230" class="line"/>
|
||||
<!-- Serpentine belt shape -->
|
||||
<path d="M620,160 Q680,160 680,210 Q680,260 650,280 Q620,300 620,340 Q620,370 660,370" stroke="#333" fill="none" stroke-width="6" stroke-linecap="round"/>
|
||||
<!-- Ribs pattern -->
|
||||
<path d="M622,165 Q678,165 678,210 Q678,258 650,278" stroke="#555" fill="none" stroke-width="1" stroke-dasharray="3 3"/>
|
||||
<!-- Pulleys -->
|
||||
<circle cx="620" cy="160" r="15" fill="#ccc" stroke="#999" stroke-width="2"/>
|
||||
<circle cx="660" cy="370" r="15" fill="#ccc" stroke="#999" stroke-width="2"/>
|
||||
<circle cx="680" cy="260" r="10" fill="#ccc" stroke="#999" stroke-width="2"/>
|
||||
<rect class="part-area" data-hotspot="3" x="600" y="140" width="100" height="250" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Gasket (bottom, exploded) -->
|
||||
<g id="part-gasket">
|
||||
<line x1="400" y1="430" x2="400" y2="480" class="line"/>
|
||||
<!-- Head gasket flat view -->
|
||||
<rect x="280" y="490" width="240" height="20" rx="3" fill="#a0a0a0" stroke="#777" stroke-width="1.5"/>
|
||||
<!-- Cylinder holes -->
|
||||
<ellipse cx="320" cy="500" rx="18" ry="7" fill="#888" stroke="#666"/>
|
||||
<ellipse cx="370" cy="500" rx="18" ry="7" fill="#888" stroke="#666"/>
|
||||
<ellipse cx="420" cy="500" rx="18" ry="7" fill="#888" stroke="#666"/>
|
||||
<ellipse cx="470" cy="500" rx="18" ry="7" fill="#888" stroke="#666"/>
|
||||
<!-- Bolt holes -->
|
||||
<circle cx="290" cy="500" r="3" fill="#666"/>
|
||||
<circle cx="505" cy="500" r="3" fill="#666"/>
|
||||
<rect class="part-area" data-hotspot="4" x="275" y="485" width="250" height="30" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Oil filter (bottom-left, exploded) -->
|
||||
<g id="part-oil-filter">
|
||||
<line x1="270" y1="400" x2="160" y2="470" class="line"/>
|
||||
<!-- Canister -->
|
||||
<rect x="80" y="450" width="60" height="90" rx="10" class="component" stroke-width="2"/>
|
||||
<!-- Base plate -->
|
||||
<ellipse cx="110" cy="540" rx="30" ry="8" fill="#999" stroke="#777"/>
|
||||
<!-- Gasket ring -->
|
||||
<ellipse cx="110" cy="536" rx="25" ry="4" fill="none" stroke="#333" stroke-width="2"/>
|
||||
<!-- Thread -->
|
||||
<rect x="102" y="540" width="16" height="12" rx="2" fill="#bbb" stroke="#999"/>
|
||||
<rect class="part-area" data-hotspot="5" x="75" y="445" width="70" height="115" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Callout numbers -->
|
||||
<circle cx="100" cy="120" r="14" fill="#F5A623"/><text class="callout" x="100" y="125" text-anchor="middle" fill="white">1</text>
|
||||
<circle cx="400" cy="18" r="14" fill="#F5A623"/><text class="callout" x="400" y="23" text-anchor="middle" fill="white">2</text>
|
||||
<circle cx="720" cy="140" r="14" fill="#F5A623"/><text class="callout" x="720" y="145" text-anchor="middle" fill="white">3</text>
|
||||
<circle cx="400" cy="530" r="14" fill="#F5A623"/><text class="callout" x="400" y="535" text-anchor="middle" fill="white">4</text>
|
||||
<circle cx="110" cy="575" r="14" fill="#F5A623"/><text class="callout" x="110" y="580" text-anchor="middle" fill="white">5</text>
|
||||
|
||||
<!-- Labels -->
|
||||
<text class="label" x="100" y="140" text-anchor="middle">Filtro de aire</text>
|
||||
<text class="label" x="400" y="10" text-anchor="middle" dy="-8">Bujias</text>
|
||||
<text class="label" x="720" y="160" text-anchor="middle">Banda serpentina</text>
|
||||
<text class="label" x="400" y="550" text-anchor="middle">Junta de culata</text>
|
||||
<text class="label" x="110" y="595" text-anchor="middle">Filtro de aceite</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
97
pos/static/diagrams/suspension-system.svg
Normal file
97
pos/static/diagrams/suspension-system.svg
Normal file
@@ -0,0 +1,97 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" fill="none" stroke-width="2">
|
||||
<defs>
|
||||
<style>
|
||||
.part-area { fill: transparent; stroke: none; cursor: pointer; }
|
||||
.part-area:hover { fill: rgba(245, 166, 35, 0.15); stroke: #F5A623; stroke-width: 2; }
|
||||
.label { font-family: 'Inter', sans-serif; font-size: 13px; fill: #666; }
|
||||
.callout { font-family: 'Inter', sans-serif; font-size: 16px; font-weight: 700; fill: #F5A623; }
|
||||
.line { stroke: #999; stroke-width: 1; stroke-dasharray: 4 2; }
|
||||
.component { stroke: #444; fill: #e8e8e8; }
|
||||
.component-detail { stroke: #666; fill: #d0d0d0; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<text x="400" y="35" text-anchor="middle" font-family="Inter, sans-serif" font-size="22" font-weight="700" fill="#333">Suspension Delantera - Vista Explodida</text>
|
||||
|
||||
<!-- Shock absorber (center) -->
|
||||
<g id="part-shock">
|
||||
<rect x="370" y="80" width="60" height="200" rx="10" class="component" stroke-width="2.5"/>
|
||||
<!-- Piston rod -->
|
||||
<rect x="393" y="55" width="14" height="40" rx="3" fill="#bbb" stroke="#888"/>
|
||||
<!-- Body detail -->
|
||||
<rect x="380" y="130" width="40" height="100" rx="5" class="component-detail"/>
|
||||
<!-- Top mount -->
|
||||
<ellipse cx="400" cy="55" rx="25" ry="10" fill="#ccc" stroke="#999"/>
|
||||
<!-- Bottom eye -->
|
||||
<circle cx="400" cy="290" r="12" fill="#ccc" stroke="#999"/>
|
||||
<circle cx="400" cy="290" r="5" fill="#999" stroke="#666"/>
|
||||
<rect class="part-area" data-hotspot="1" x="365" y="43" width="70" height="260" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Coil spring (wrapped around shock, exploded to right) -->
|
||||
<g id="part-spring">
|
||||
<line x1="430" y1="180" x2="520" y2="180" class="line"/>
|
||||
<!-- Spring coils -->
|
||||
<path d="M540,80 Q580,95 540,110 Q500,125 540,140 Q580,155 540,170 Q500,185 540,200 Q580,215 540,230 Q500,245 540,260 Q580,275 540,290" stroke="#666" fill="none" stroke-width="5" stroke-linecap="round"/>
|
||||
<!-- End plates -->
|
||||
<rect x="520" y="70" width="40" height="8" rx="2" fill="#999" stroke="#777"/>
|
||||
<rect x="520" y="290" width="40" height="8" rx="2" fill="#999" stroke="#777"/>
|
||||
<rect class="part-area" data-hotspot="2" x="495" y="65" width="95" height="240" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Control arm (lower, exploded down) -->
|
||||
<g id="part-control-arm">
|
||||
<line x1="400" y1="302" x2="400" y2="360" class="line"/>
|
||||
<!-- A-arm shape -->
|
||||
<path d="M250,400 L400,380 L550,400 L400,420 Z" class="component" stroke-width="2"/>
|
||||
<!-- Bushings -->
|
||||
<circle cx="270" cy="405" r="12" fill="#777" stroke="#555"/>
|
||||
<circle cx="530" cy="405" r="12" fill="#777" stroke="#555"/>
|
||||
<!-- Ball joint socket -->
|
||||
<circle cx="400" cy="395" r="10" fill="#bbb" stroke="#888"/>
|
||||
<rect class="part-area" data-hotspot="3" x="245" y="375" width="310" height="50" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Ball joint (exploded down-left) -->
|
||||
<g id="part-ball-joint">
|
||||
<line x1="350" y1="395" x2="200" y2="460" class="line"/>
|
||||
<!-- Ball joint body -->
|
||||
<path d="M160,450 L240,450 L230,500 L170,500 Z" class="component" stroke-width="2"/>
|
||||
<!-- Ball -->
|
||||
<circle cx="200" cy="455" r="10" fill="#bbb" stroke="#888"/>
|
||||
<!-- Stud -->
|
||||
<rect x="194" y="500" width="12" height="30" rx="2" fill="#999" stroke="#777"/>
|
||||
<!-- Boot -->
|
||||
<path d="M172,465 Q200,490 228,465" stroke="#333" fill="#555" opacity="0.5"/>
|
||||
<rect class="part-area" data-hotspot="4" x="155" y="440" width="90" height="95" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Tie rod (exploded to the left) -->
|
||||
<g id="part-tie-rod">
|
||||
<line x1="250" y1="400" x2="150" y2="320" class="line"/>
|
||||
<!-- Rod -->
|
||||
<rect x="30" y="280" width="160" height="14" rx="5" class="component"/>
|
||||
<!-- Inner end -->
|
||||
<circle cx="40" cy="287" r="10" fill="#bbb" stroke="#888"/>
|
||||
<!-- Outer end (ball joint) -->
|
||||
<path d="M180,275 L200,275 L195,300 L175,300 Z" class="component-detail"/>
|
||||
<circle cx="190" cy="278" r="6" fill="#bbb" stroke="#888"/>
|
||||
<!-- Boot -->
|
||||
<path d="M175,280 Q190,295 205,280" stroke="#333" fill="#555" opacity="0.5"/>
|
||||
<rect class="part-area" data-hotspot="5" x="25" y="265" width="185" height="45" rx="8"/>
|
||||
</g>
|
||||
|
||||
<!-- Callout numbers -->
|
||||
<circle cx="400" cy="170" r="14" fill="#F5A623"/><text class="callout" x="400" y="175" text-anchor="middle" fill="white">1</text>
|
||||
<circle cx="560" cy="55" r="14" fill="#F5A623"/><text class="callout" x="560" y="60" text-anchor="middle" fill="white">2</text>
|
||||
<circle cx="400" cy="440" r="14" fill="#F5A623"/><text class="callout" x="400" y="445" text-anchor="middle" fill="white">3</text>
|
||||
<circle cx="200" cy="550" r="14" fill="#F5A623"/><text class="callout" x="200" y="555" text-anchor="middle" fill="white">4</text>
|
||||
<circle cx="40" cy="260" r="14" fill="#F5A623"/><text class="callout" x="40" y="265" text-anchor="middle" fill="white">5</text>
|
||||
|
||||
<!-- Labels -->
|
||||
<text class="label" x="400" y="192" text-anchor="middle">Amortiguador</text>
|
||||
<text class="label" x="560" y="48" text-anchor="middle" dy="-8">Resorte helicoidal</text>
|
||||
<text class="label" x="400" y="460" text-anchor="middle">Brazo de control</text>
|
||||
<text class="label" x="200" y="570" text-anchor="middle">Rotula</text>
|
||||
<text class="label" x="40" y="253" text-anchor="middle" dy="-8">Barra de acoplamiento</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -122,7 +122,7 @@
|
||||
}
|
||||
|
||||
// ─── UI helpers ───
|
||||
function showLoading() { loading.classList.add('is-visible'); navGrid.innerHTML = ''; partsGrid.style.display = 'none'; partsGrid.innerHTML = ''; emptyState.classList.remove('is-visible'); paginationNav.innerHTML = ''; }
|
||||
function showLoading() { loading.classList.add('is-visible'); navGrid.innerHTML = ''; partsGrid.style.display = 'none'; partsGrid.innerHTML = ''; emptyState.classList.remove('is-visible'); paginationNav.innerHTML = ''; var dl = document.getElementById('diagLink'); if (dl && nav.level !== 'categories') dl.style.display = 'none'; }
|
||||
function hideLoading() { loading.classList.remove('is-visible'); }
|
||||
|
||||
function showEmpty(title, subtitle) {
|
||||
@@ -345,6 +345,19 @@
|
||||
updateBreadcrumb();
|
||||
levelTitle.textContent = 'Categorias de partes';
|
||||
setupLevelFilter(true);
|
||||
// Add "Ver diagramas" link
|
||||
var diagLink = document.getElementById('diagLink');
|
||||
if (!diagLink) {
|
||||
diagLink = document.createElement('a');
|
||||
diagLink.id = 'diagLink';
|
||||
diagLink.href = '/pos/diagrams';
|
||||
diagLink.className = 'btn-diagram-link';
|
||||
diagLink.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/></svg> Ver diagramas';
|
||||
diagLink.style.cssText = 'display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:var(--color-primary-muted);color:var(--color-primary);border:1px solid var(--color-primary);border-radius:var(--radius-md);font-size:var(--text-body-sm);font-weight:600;text-decoration:none;cursor:pointer;transition:var(--transition-fast);margin-left:auto;';
|
||||
var titleContainer = levelTitle.parentElement;
|
||||
if (titleContainer) titleContainer.appendChild(diagLink);
|
||||
}
|
||||
diagLink.style.display = 'inline-flex';
|
||||
showLoading();
|
||||
|
||||
apiFetch(API + '/categories?mye_id=' + nav.engine.id_mye).then(function (data) {
|
||||
|
||||
513
pos/static/js/diagrams.js
Normal file
513
pos/static/js/diagrams.js
Normal file
@@ -0,0 +1,513 @@
|
||||
// /home/Autopartes/pos/static/js/diagrams.js
|
||||
// Interactive exploded-view diagrams with zoom/pan and clickable hotspots.
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var API = '/pos/api/diagrams';
|
||||
var CATALOG_API = '/pos/api/catalog';
|
||||
var token = localStorage.getItem('pos_token');
|
||||
if (!token) { window.location.href = '/pos/login'; return; }
|
||||
|
||||
var headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
|
||||
|
||||
// ---- State ----
|
||||
var state = {
|
||||
view: 'list', // 'list' | 'viewer'
|
||||
diagrams: [],
|
||||
categories: [], // grouped by category
|
||||
currentDiagram: null,
|
||||
hotspots: [],
|
||||
selectedHotspot: null,
|
||||
// zoom/pan
|
||||
scale: 1,
|
||||
panX: 0,
|
||||
panY: 0,
|
||||
isPanning: false,
|
||||
lastPointer: null,
|
||||
};
|
||||
|
||||
// ---- DOM refs ----
|
||||
var diagramList = document.getElementById('diagramList');
|
||||
var diagramViewer = document.getElementById('diagramViewer');
|
||||
var svgContainer = document.getElementById('svgContainer');
|
||||
var svgWrapper = document.getElementById('svgWrapper');
|
||||
var backBtn = document.getElementById('backBtn');
|
||||
var zoomInBtn = document.getElementById('zoomInBtn');
|
||||
var zoomOutBtn = document.getElementById('zoomOutBtn');
|
||||
var zoomResetBtn = document.getElementById('zoomResetBtn');
|
||||
var diagramTitle = document.getElementById('diagramTitle');
|
||||
var hotspotPanel = document.getElementById('hotspotPanel');
|
||||
var hotspotBody = document.getElementById('hotspotBody');
|
||||
var hotspotClose = document.getElementById('hotspotClose');
|
||||
var partsListEl = document.getElementById('partsList');
|
||||
var loadingEl = document.getElementById('loading');
|
||||
var emptyEl = document.getElementById('emptyState');
|
||||
var filterInput = document.getElementById('diagramFilter');
|
||||
var profileAvatar = document.getElementById('profileAvatar');
|
||||
var profileName = document.getElementById('profileName');
|
||||
var profileRole = document.getElementById('profileRole');
|
||||
|
||||
// ---- Init ----
|
||||
function init() {
|
||||
loadProfile();
|
||||
loadAllDiagrams();
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
function loadProfile() {
|
||||
try {
|
||||
var u = JSON.parse(localStorage.getItem('pos_user') || '{}');
|
||||
if (profileName) profileName.textContent = u.name || '--';
|
||||
if (profileRole) profileRole.textContent = u.role || '--';
|
||||
if (profileAvatar) {
|
||||
var n = (u.name || '--');
|
||||
profileAvatar.textContent = n.split(' ').map(function(w){return w[0]||'';}).join('').substring(0,2).toUpperCase();
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
if (backBtn) backBtn.addEventListener('click', showList);
|
||||
if (zoomInBtn) zoomInBtn.addEventListener('click', function() { zoom(1.25); });
|
||||
if (zoomOutBtn) zoomOutBtn.addEventListener('click', function() { zoom(0.8); });
|
||||
if (zoomResetBtn) zoomResetBtn.addEventListener('click', resetZoom);
|
||||
if (hotspotClose) hotspotClose.addEventListener('click', closeHotspotPanel);
|
||||
if (filterInput) filterInput.addEventListener('input', renderList);
|
||||
|
||||
// Zoom with mouse wheel on SVG container
|
||||
if (svgContainer) {
|
||||
svgContainer.addEventListener('wheel', function(e) {
|
||||
e.preventDefault();
|
||||
var factor = e.deltaY < 0 ? 1.1 : 0.9;
|
||||
zoom(factor);
|
||||
}, { passive: false });
|
||||
|
||||
// Pan with mouse drag
|
||||
svgContainer.addEventListener('mousedown', startPan);
|
||||
svgContainer.addEventListener('mousemove', doPan);
|
||||
svgContainer.addEventListener('mouseup', endPan);
|
||||
svgContainer.addEventListener('mouseleave', endPan);
|
||||
|
||||
// Touch support
|
||||
svgContainer.addEventListener('touchstart', function(e) {
|
||||
if (e.touches.length === 1) {
|
||||
startPan({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY, preventDefault: function(){} });
|
||||
}
|
||||
});
|
||||
svgContainer.addEventListener('touchmove', function(e) {
|
||||
if (e.touches.length === 1) {
|
||||
e.preventDefault();
|
||||
doPan({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY });
|
||||
}
|
||||
}, { passive: false });
|
||||
svgContainer.addEventListener('touchend', endPan);
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (state.view !== 'viewer') return;
|
||||
if (e.key === 'Escape') {
|
||||
if (state.selectedHotspot) closeHotspotPanel();
|
||||
else showList();
|
||||
}
|
||||
if (e.key === '+' || e.key === '=') zoom(1.15);
|
||||
if (e.key === '-') zoom(0.87);
|
||||
if (e.key === '0') resetZoom();
|
||||
});
|
||||
}
|
||||
|
||||
// ---- API calls ----
|
||||
function apiFetch(url, cb) {
|
||||
loadingEl.style.display = 'flex';
|
||||
emptyEl.style.display = 'none';
|
||||
fetch(url, { headers: headers })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
loadingEl.style.display = 'none';
|
||||
cb(null, data);
|
||||
})
|
||||
.catch(function(err) {
|
||||
loadingEl.style.display = 'none';
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
function loadAllDiagrams() {
|
||||
apiFetch(API + '/', function(err, data) {
|
||||
if (err || !data || !data.data) {
|
||||
emptyEl.style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
state.diagrams = data.data;
|
||||
// Group by category
|
||||
var cats = {};
|
||||
data.data.forEach(function(d) {
|
||||
var key = d.category_name || 'Other';
|
||||
if (!cats[key]) cats[key] = { name: key, name_es: d.category_name_es || key, diagrams: [] };
|
||||
cats[key].diagrams.push(d);
|
||||
});
|
||||
state.categories = Object.values(cats);
|
||||
renderList();
|
||||
});
|
||||
}
|
||||
|
||||
function loadDiagram(id) {
|
||||
apiFetch(API + '/' + id, function(err, data) {
|
||||
if (err || !data || data.error) return;
|
||||
state.currentDiagram = data;
|
||||
state.hotspots = data.hotspots || [];
|
||||
showViewer();
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Render list view ----
|
||||
function renderList() {
|
||||
state.view = 'list';
|
||||
diagramViewer.style.display = 'none';
|
||||
diagramList.style.display = 'block';
|
||||
|
||||
var filter = (filterInput && filterInput.value || '').toLowerCase();
|
||||
var html = '';
|
||||
|
||||
state.categories.forEach(function(cat) {
|
||||
var filtered = cat.diagrams.filter(function(d) {
|
||||
if (!filter) return true;
|
||||
return (d.name_es || d.name || '').toLowerCase().indexOf(filter) !== -1 ||
|
||||
(d.category_name_es || '').toLowerCase().indexOf(filter) !== -1 ||
|
||||
(d.group_name_es || '').toLowerCase().indexOf(filter) !== -1;
|
||||
});
|
||||
if (filtered.length === 0) return;
|
||||
|
||||
html += '<div class="diagram-category">';
|
||||
html += '<h3 class="category-title">' + esc(cat.name_es || cat.name) + '</h3>';
|
||||
html += '<div class="diagram-grid">';
|
||||
|
||||
filtered.forEach(function(d) {
|
||||
html += '<div class="diagram-card" data-id="' + d.id_diagram + '" onclick="DiagramsApp.openDiagram(' + d.id_diagram + ')">';
|
||||
html += '<div class="diagram-card__preview">';
|
||||
html += '<img src="/pos/static/' + esc(d.image_path) + '" alt="' + esc(d.name_es || d.name) + '" loading="lazy" />';
|
||||
html += '</div>';
|
||||
html += '<div class="diagram-card__info">';
|
||||
html += '<div class="diagram-card__name">' + esc(d.name_es || d.name) + '</div>';
|
||||
html += '<div class="diagram-card__group">' + esc(d.group_name_es || d.group_name || '') + '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
html += '</div></div>';
|
||||
});
|
||||
|
||||
if (!html) {
|
||||
emptyEl.style.display = 'flex';
|
||||
} else {
|
||||
emptyEl.style.display = 'none';
|
||||
}
|
||||
|
||||
diagramList.innerHTML = html;
|
||||
}
|
||||
|
||||
// ---- Viewer ----
|
||||
function showViewer() {
|
||||
state.view = 'viewer';
|
||||
diagramList.style.display = 'none';
|
||||
diagramViewer.style.display = 'flex';
|
||||
closeHotspotPanel();
|
||||
|
||||
var d = state.currentDiagram;
|
||||
if (diagramTitle) diagramTitle.textContent = d.name_es || d.name;
|
||||
|
||||
// Load SVG inline for interactivity
|
||||
resetZoom();
|
||||
loadSVGInline(d.id_diagram);
|
||||
renderPartsList();
|
||||
}
|
||||
|
||||
function showList() {
|
||||
state.view = 'list';
|
||||
state.currentDiagram = null;
|
||||
state.selectedHotspot = null;
|
||||
diagramViewer.style.display = 'none';
|
||||
diagramList.style.display = 'block';
|
||||
closeHotspotPanel();
|
||||
}
|
||||
|
||||
function loadSVGInline(diagramId) {
|
||||
svgWrapper.innerHTML = '<div class="svg-loading">Cargando diagrama...</div>';
|
||||
fetch(API + '/' + diagramId + '/svg', { headers: headers })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(svgText) {
|
||||
svgWrapper.innerHTML = svgText;
|
||||
var svg = svgWrapper.querySelector('svg');
|
||||
if (svg) {
|
||||
svg.style.width = '100%';
|
||||
svg.style.height = '100%';
|
||||
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
||||
attachHotspotListeners(svg);
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
svgWrapper.innerHTML = '<div class="svg-error">Error al cargar el diagrama</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function attachHotspotListeners(svg) {
|
||||
var areas = svg.querySelectorAll('[data-hotspot]');
|
||||
areas.forEach(function(area) {
|
||||
var callout = parseInt(area.getAttribute('data-hotspot'));
|
||||
area.style.cursor = 'pointer';
|
||||
|
||||
area.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
selectHotspot(callout);
|
||||
});
|
||||
|
||||
area.addEventListener('mouseenter', function() {
|
||||
highlightHotspot(callout, true);
|
||||
});
|
||||
area.addEventListener('mouseleave', function() {
|
||||
highlightHotspot(callout, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function highlightHotspot(callout, on) {
|
||||
// Highlight in parts list
|
||||
var items = partsListEl.querySelectorAll('.part-item');
|
||||
items.forEach(function(item) {
|
||||
if (parseInt(item.dataset.callout) === callout) {
|
||||
item.classList.toggle('is-highlighted', on);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function selectHotspot(callout) {
|
||||
var hotspot = state.hotspots.find(function(h) { return h.callout_number === callout; });
|
||||
if (!hotspot) return;
|
||||
|
||||
state.selectedHotspot = hotspot;
|
||||
|
||||
// Highlight in SVG
|
||||
var svg = svgWrapper.querySelector('svg');
|
||||
if (svg) {
|
||||
svg.querySelectorAll('[data-hotspot]').forEach(function(area) {
|
||||
var num = parseInt(area.getAttribute('data-hotspot'));
|
||||
if (num === callout) {
|
||||
area.style.fill = 'rgba(245, 166, 35, 0.25)';
|
||||
area.style.stroke = '#F5A623';
|
||||
area.style.strokeWidth = '3';
|
||||
} else {
|
||||
area.style.fill = 'transparent';
|
||||
area.style.stroke = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Highlight in parts list
|
||||
var items = partsListEl.querySelectorAll('.part-item');
|
||||
items.forEach(function(item) {
|
||||
item.classList.toggle('is-active', parseInt(item.dataset.callout) === callout);
|
||||
});
|
||||
|
||||
// Show hotspot detail panel
|
||||
showHotspotDetail(hotspot);
|
||||
}
|
||||
|
||||
function showHotspotDetail(hotspot) {
|
||||
hotspotPanel.classList.add('is-open');
|
||||
|
||||
var partName = hotspot.part_name_es || hotspot.part_name || 'Parte #' + hotspot.callout_number;
|
||||
var partNum = hotspot.part_number || '';
|
||||
var desc = hotspot.description_es || hotspot.description || '';
|
||||
|
||||
// Determine search term for catalog link
|
||||
var searchTerm = partNum || partName;
|
||||
|
||||
// Map callout to friendly part names for placeholder diagrams
|
||||
var placeholderNames = getPlaceholderPartInfo(hotspot.callout_number);
|
||||
|
||||
var displayName = hotspot.part_name_es || hotspot.part_name || placeholderNames.name;
|
||||
var displayDesc = desc || placeholderNames.desc;
|
||||
|
||||
var html = '';
|
||||
html += '<div class="hotspot-detail">';
|
||||
html += '<div class="hotspot-callout">' + hotspot.callout_number + '</div>';
|
||||
html += '<h4 class="hotspot-name">' + esc(displayName) + '</h4>';
|
||||
if (partNum) {
|
||||
html += '<div class="hotspot-partnumber">No. Parte: ' + esc(partNum) + '</div>';
|
||||
}
|
||||
if (displayDesc) {
|
||||
html += '<p class="hotspot-desc">' + esc(displayDesc) + '</p>';
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
html += '<div class="hotspot-actions">';
|
||||
if (hotspot.part_id) {
|
||||
html += '<button class="btn btn-primary btn-sm" onclick="DiagramsApp.viewPart(' + hotspot.part_id + ')">Ver detalle</button>';
|
||||
html += '<button class="btn btn-accent btn-sm" onclick="DiagramsApp.addToCart(' + hotspot.part_id + ')"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 002 1.61h9.72a2 2 0 002-1.61L23 6H6"/></svg> Agregar</button>';
|
||||
} else {
|
||||
html += '<button class="btn btn-primary btn-sm" onclick="DiagramsApp.searchPart(\'' + esc(displayName) + '\')">Buscar en catalogo</button>';
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
hotspotBody.innerHTML = html;
|
||||
}
|
||||
|
||||
function getPlaceholderPartInfo(callout) {
|
||||
// For built-in placeholder diagrams, provide friendly names
|
||||
var d = state.currentDiagram;
|
||||
if (!d) return { name: 'Parte ' + callout, desc: '' };
|
||||
|
||||
var brakeNames = {
|
||||
1: { name: 'Disco de freno', desc: 'Disco ventilado de freno delantero. Se recomienda cambiar en pares.' },
|
||||
2: { name: 'Caliper de freno', desc: 'Caliper con pistones, incluye purga. Verificar compatibilidad con tipo de pastilla.' },
|
||||
3: { name: 'Pastillas de freno', desc: 'Juego de pastillas con indicador de desgaste. Material ceramico o semi-metalico.' },
|
||||
4: { name: 'Manguera de freno', desc: 'Manguera flexible de alta presion. Revisar por grietas cada 40,000 km.' },
|
||||
5: { name: 'Cilindro maestro', desc: 'Cilindro maestro con deposito de liquido de frenos. Incluye empaques.' },
|
||||
};
|
||||
var suspNames = {
|
||||
1: { name: 'Amortiguador', desc: 'Amortiguador delantero de gas. Se recomienda cambiar en pares.' },
|
||||
2: { name: 'Resorte helicoidal', desc: 'Resorte de suspension delantera. Verificar altura libre.' },
|
||||
3: { name: 'Brazo de control', desc: 'Brazo inferior de control con bujes. Incluye herraje de montaje.' },
|
||||
4: { name: 'Rotula', desc: 'Rotula inferior de suspension. Incluye guardapolvo y seguros.' },
|
||||
5: { name: 'Barra de acoplamiento', desc: 'Barra de acoplamiento de direccion con terminales. Requiere alineacion.' },
|
||||
};
|
||||
var engineNames = {
|
||||
1: { name: 'Filtro de aire', desc: 'Filtro de aire del motor. Cambiar cada 15,000-20,000 km.' },
|
||||
2: { name: 'Bujias', desc: 'Juego de bujias. Verificar tipo (platino, iridio) segun especificacion del motor.' },
|
||||
3: { name: 'Banda serpentina', desc: 'Banda de accesorios. Revisar tension y desgaste. Incluye alternador, A/C y direccion.' },
|
||||
4: { name: 'Junta de culata', desc: 'Junta de cabeza de cilindros. Material MLS multicapa. Requiere torque especifico.' },
|
||||
5: { name: 'Filtro de aceite', desc: 'Filtro de aceite del motor. Cambiar en cada servicio de aceite.' },
|
||||
};
|
||||
|
||||
var name = (d.name || '').toLowerCase();
|
||||
if (name.indexOf('brak') !== -1 || name.indexOf('freno') !== -1) return brakeNames[callout] || { name: 'Parte ' + callout, desc: '' };
|
||||
if (name.indexOf('susp') !== -1) return suspNames[callout] || { name: 'Parte ' + callout, desc: '' };
|
||||
if (name.indexOf('engine') !== -1 || name.indexOf('motor') !== -1) return engineNames[callout] || { name: 'Parte ' + callout, desc: '' };
|
||||
return { name: 'Parte ' + callout, desc: '' };
|
||||
}
|
||||
|
||||
function closeHotspotPanel() {
|
||||
hotspotPanel.classList.remove('is-open');
|
||||
state.selectedHotspot = null;
|
||||
// Clear SVG highlights
|
||||
var svg = svgWrapper.querySelector('svg');
|
||||
if (svg) {
|
||||
svg.querySelectorAll('[data-hotspot]').forEach(function(area) {
|
||||
area.style.fill = 'transparent';
|
||||
area.style.stroke = 'none';
|
||||
});
|
||||
}
|
||||
// Clear list highlights
|
||||
if (partsListEl) {
|
||||
partsListEl.querySelectorAll('.part-item').forEach(function(item) {
|
||||
item.classList.remove('is-active');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Parts list sidebar ----
|
||||
function renderPartsList() {
|
||||
if (!partsListEl) return;
|
||||
var html = '<h4 class="parts-list__title">Partes en diagrama</h4>';
|
||||
|
||||
state.hotspots.forEach(function(h) {
|
||||
var info = getPlaceholderPartInfo(h.callout_number);
|
||||
var name = h.part_name_es || h.part_name || info.name;
|
||||
var num = h.part_number || '';
|
||||
|
||||
html += '<div class="part-item" data-callout="' + h.callout_number + '" onclick="DiagramsApp.selectHotspot(' + h.callout_number + ')">';
|
||||
html += '<span class="part-item__callout">' + h.callout_number + '</span>';
|
||||
html += '<div class="part-item__info">';
|
||||
html += '<div class="part-item__name">' + esc(name) + '</div>';
|
||||
if (num) html += '<div class="part-item__number">' + esc(num) + '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
if (state.hotspots.length === 0) {
|
||||
html += '<div class="parts-list__empty">Sin partes definidas</div>';
|
||||
}
|
||||
|
||||
partsListEl.innerHTML = html;
|
||||
}
|
||||
|
||||
// ---- Zoom / Pan ----
|
||||
function zoom(factor) {
|
||||
state.scale = Math.max(0.3, Math.min(5, state.scale * factor));
|
||||
applyTransform();
|
||||
}
|
||||
|
||||
function resetZoom() {
|
||||
state.scale = 1;
|
||||
state.panX = 0;
|
||||
state.panY = 0;
|
||||
applyTransform();
|
||||
}
|
||||
|
||||
function applyTransform() {
|
||||
if (!svgWrapper) return;
|
||||
svgWrapper.style.transform = 'translate(' + state.panX + 'px, ' + state.panY + 'px) scale(' + state.scale + ')';
|
||||
}
|
||||
|
||||
function startPan(e) {
|
||||
// Only pan with left button and not on a hotspot
|
||||
if (e.button && e.button !== 0) return;
|
||||
state.isPanning = true;
|
||||
state.lastPointer = { x: e.clientX, y: e.clientY };
|
||||
}
|
||||
|
||||
function doPan(e) {
|
||||
if (!state.isPanning || !state.lastPointer) return;
|
||||
var dx = e.clientX - state.lastPointer.x;
|
||||
var dy = e.clientY - state.lastPointer.y;
|
||||
state.panX += dx;
|
||||
state.panY += dy;
|
||||
state.lastPointer = { x: e.clientX, y: e.clientY };
|
||||
applyTransform();
|
||||
}
|
||||
|
||||
function endPan() {
|
||||
state.isPanning = false;
|
||||
state.lastPointer = null;
|
||||
}
|
||||
|
||||
// ---- Actions ----
|
||||
function viewPart(partId) {
|
||||
window.open('/pos/catalog?part=' + partId, '_blank');
|
||||
}
|
||||
|
||||
function searchPart(query) {
|
||||
window.open('/pos/catalog?search=' + encodeURIComponent(query), '_blank');
|
||||
}
|
||||
|
||||
function addToCart(partId) {
|
||||
// Reuse the catalog cart logic if available
|
||||
alert('Funcion disponible desde el catalogo. Busca la parte para agregarla al carrito.');
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
function esc(s) {
|
||||
if (!s) return '';
|
||||
var d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(s));
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// ---- Public API ----
|
||||
window.DiagramsApp = {
|
||||
openDiagram: loadDiagram,
|
||||
selectHotspot: selectHotspot,
|
||||
viewPart: viewPart,
|
||||
searchPart: searchPart,
|
||||
addToCart: addToCart,
|
||||
};
|
||||
|
||||
// Init on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -9,6 +9,7 @@ var I18N = {
|
||||
'pos': 'Punto de Venta',
|
||||
'catalog': 'Catalogo',
|
||||
'inventory': 'Inventario',
|
||||
'diagrams': 'Diagramas',
|
||||
'customers': 'Clientes',
|
||||
'invoicing': 'Facturacion',
|
||||
'accounting': 'Contabilidad',
|
||||
@@ -164,6 +165,7 @@ var I18N = {
|
||||
'pos': 'Point of Sale',
|
||||
'catalog': 'Catalog',
|
||||
'inventory': 'Inventory',
|
||||
'diagrams': 'Diagrams',
|
||||
'customers': 'Customers',
|
||||
'invoicing': 'Invoicing',
|
||||
'accounting': 'Accounting',
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
{ name: _t('pos'), href: '/pos/sale', icon: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>' },
|
||||
{ name: _t('catalog'), href: '/pos/catalog', icon: '<path d="M4 6h16M4 10h16M4 14h16M4 18h16"/>' },
|
||||
{ name: _t('inventory'), href: '/pos/inventory', icon: '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>' },
|
||||
{ name: _t('diagrams'), href: '/pos/diagrams', icon: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>' },
|
||||
]},
|
||||
{ label: _t('nav_management'), items: [
|
||||
{ name: _t('customers'), href: '/pos/customers', icon: '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/>' },
|
||||
|
||||
613
pos/templates/diagrams.html
Normal file
613
pos/templates/diagrams.html
Normal file
@@ -0,0 +1,613 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" data-theme="industrial">
|
||||
<head>
|
||||
<script>/*pos_theme_early*/(function(){var t=localStorage.getItem("pos_theme")||"industrial";document.documentElement.setAttribute("data-theme",t);})()</script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Diagramas — Nexus Autoparts POS</title>
|
||||
<link rel="stylesheet" href="/pos/static/css/tokens.css" />
|
||||
<link rel="stylesheet" href="/pos/static/css/chat.css" />
|
||||
<link rel="stylesheet" href="/pos/static/css/onboarding.css" />
|
||||
<link rel="manifest" href="/pos/static/pwa/manifest.json" />
|
||||
<meta name="theme-color" content="#F5A623" />
|
||||
<script src="/pos/static/js/native-bridge.js"></script>
|
||||
|
||||
<style>
|
||||
/* =========================================================================
|
||||
BASE RESET & SHELL
|
||||
========================================================================= */
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; }
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-base);
|
||||
transition: background-color var(--duration-normal) var(--ease-in-out),
|
||||
color var(--duration-normal) var(--ease-in-out);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-theme="modern"] body {
|
||||
background-image: radial-gradient(circle, var(--dot-grid-color) 1px, transparent 1px);
|
||||
background-size: var(--dot-grid-size) var(--dot-grid-size);
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
APP LAYOUT
|
||||
========================================================================= */
|
||||
|
||||
.app-shell { display: flex; height: 100vh; padding-top: 36px; }
|
||||
|
||||
/* =========================================================================
|
||||
SIDEBAR (shared pattern)
|
||||
========================================================================= */
|
||||
|
||||
.sidebar {
|
||||
width: 260px; flex-shrink: 0; display: flex; flex-direction: column;
|
||||
background: var(--color-bg-elevated); border-right: 1px solid var(--color-border);
|
||||
overflow-y: auto; transition: var(--transition-normal);
|
||||
}
|
||||
.sidebar__brand {
|
||||
display: flex; align-items: center; gap: var(--space-3);
|
||||
padding: var(--space-5) var(--space-5) var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border); flex-shrink: 0;
|
||||
}
|
||||
.brand-logo {
|
||||
width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;
|
||||
background: var(--color-primary); color: var(--color-text-inverse);
|
||||
font-family: var(--font-heading); font-weight: var(--heading-weight-primary);
|
||||
font-size: 1.375rem; letter-spacing: var(--tracking-tight); flex-shrink: 0;
|
||||
}
|
||||
[data-theme="industrial"] .brand-logo { clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%); border-radius: 0; }
|
||||
[data-theme="modern"] .brand-logo { border-radius: var(--radius-md); }
|
||||
.brand-name { display: flex; flex-direction: column; line-height: 1; }
|
||||
.brand-name__primary { font-family: var(--font-heading); font-weight: var(--heading-weight-primary); font-size: 1.125rem; letter-spacing: var(--tracking-wide); color: var(--color-text-primary); text-transform: uppercase; }
|
||||
.brand-name__sub { font-family: var(--font-body); font-size: var(--text-caption); color: var(--color-text-muted); letter-spacing: var(--tracking-wider); text-transform: uppercase; margin-top: 2px; }
|
||||
|
||||
.sidebar__nav { flex: 1; padding: var(--space-3) 0; }
|
||||
.nav-section-label { padding: var(--space-3) var(--space-5) var(--space-1); font-size: var(--text-caption); font-family: var(--font-body); font-weight: var(--font-weight-semibold); color: var(--color-text-muted); letter-spacing: var(--tracking-widest); text-transform: uppercase; }
|
||||
.nav-item { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-5); color: var(--color-text-secondary); font-family: var(--font-body); font-size: var(--text-body-sm); font-weight: var(--font-weight-regular); text-decoration: none; cursor: pointer; border: none; background: none; width: 100%; text-align: left; transition: var(--transition-fast); border-left: 3px solid transparent; }
|
||||
.nav-item:hover { background: var(--color-primary-muted); color: var(--color-text-primary); border-left-color: var(--color-primary); }
|
||||
.nav-item.is-active { background: var(--color-primary-muted); color: var(--color-primary); font-weight: var(--font-weight-semibold); border-left-color: var(--color-primary); }
|
||||
[data-theme="industrial"] .nav-item.is-active { background: rgba(245, 166, 35, 0.12); }
|
||||
.nav-item__icon { width: 18px; height: 18px; opacity: 0.75; flex-shrink: 0; }
|
||||
.nav-item.is-active .nav-item__icon, .nav-item:hover .nav-item__icon { opacity: 1; }
|
||||
|
||||
.sidebar__profile { padding: var(--space-4) var(--space-5); border-top: 1px solid var(--color-border); display: flex; align-items: center; gap: var(--space-3); flex-shrink: 0; }
|
||||
.profile-avatar { width: 36px; height: 36px; border-radius: var(--radius-full); background: var(--color-primary-muted); color: var(--color-primary); display: flex; align-items: center; justify-content: center; font-family: var(--font-heading); font-weight: var(--font-weight-bold); font-size: var(--text-body-sm); flex-shrink: 0; }
|
||||
.profile-info { overflow: hidden; }
|
||||
.profile-info__name { font-weight: var(--font-weight-semibold); font-size: var(--text-body-sm); color: var(--color-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.profile-info__role { font-size: var(--text-caption); color: var(--color-text-muted); }
|
||||
|
||||
/* =========================================================================
|
||||
MAIN CONTENT
|
||||
========================================================================= */
|
||||
|
||||
.main-content {
|
||||
flex: 1; display: flex; flex-direction: column; overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex; align-items: center; gap: var(--space-4);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-bg-elevated);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.page-header__title {
|
||||
font-family: var(--font-heading); font-weight: var(--heading-weight-primary);
|
||||
font-size: 1.25rem; color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.page-header__filter {
|
||||
margin-left: auto;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-bg-base);
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-body-sm);
|
||||
min-width: 220px;
|
||||
outline: none;
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
.page-header__filter:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-muted);
|
||||
}
|
||||
|
||||
.page-body {
|
||||
flex: 1; overflow-y: auto; padding: var(--space-5);
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
LOADING & EMPTY
|
||||
========================================================================= */
|
||||
|
||||
.loading {
|
||||
display: none; align-items: center; justify-content: center;
|
||||
padding: var(--space-8); flex: 1;
|
||||
}
|
||||
.spinner {
|
||||
width: 40px; height: 40px; border: 3px solid var(--color-border);
|
||||
border-top-color: var(--color-primary); border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.empty-state {
|
||||
display: none; flex-direction: column; align-items: center;
|
||||
justify-content: center; gap: var(--space-3); padding: var(--space-8);
|
||||
color: var(--color-text-muted); text-align: center;
|
||||
}
|
||||
.empty-state__title { font-weight: var(--font-weight-semibold); font-size: 1.1rem; }
|
||||
.empty-state__subtitle { font-size: var(--text-body-sm); }
|
||||
|
||||
/* =========================================================================
|
||||
DIAGRAM LIST VIEW
|
||||
========================================================================= */
|
||||
|
||||
.diagram-category { margin-bottom: var(--space-6); }
|
||||
|
||||
.category-title {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: var(--heading-weight-primary);
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-primary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-wide);
|
||||
padding-bottom: var(--space-2);
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.diagram-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.diagram-card {
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
.diagram-card:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
[data-theme="industrial"] .diagram-card {
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.diagram-card__preview {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
background: #f8f8f8;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
[data-theme="modern"] .diagram-card__preview {
|
||||
background: var(--color-bg-base);
|
||||
}
|
||||
.diagram-card__preview img {
|
||||
width: 100%; height: 100%;
|
||||
object-fit: contain;
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.diagram-card__info {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
.diagram-card__name {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.diagram-card__group {
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
DIAGRAM VIEWER
|
||||
========================================================================= */
|
||||
|
||||
.diagram-viewer {
|
||||
display: none; flex: 1; flex-direction: row; overflow: hidden;
|
||||
}
|
||||
|
||||
.viewer-main {
|
||||
flex: 1; display: flex; flex-direction: column; overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.viewer-toolbar {
|
||||
display: flex; align-items: center; gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-bg-elevated);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.viewer-toolbar .btn-icon {
|
||||
width: 36px; height: 36px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: var(--color-bg-base); border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md); cursor: pointer;
|
||||
color: var(--color-text-secondary);
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
.viewer-toolbar .btn-icon:hover {
|
||||
background: var(--color-primary-muted);
|
||||
color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.viewer-toolbar__title {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: var(--heading-weight-primary);
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-primary);
|
||||
flex: 1;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex; gap: var(--space-1); margin-left: auto;
|
||||
}
|
||||
|
||||
.svg-container {
|
||||
flex: 1; overflow: hidden; position: relative;
|
||||
background: var(--color-bg-base);
|
||||
cursor: grab;
|
||||
}
|
||||
.svg-container:active { cursor: grabbing; }
|
||||
|
||||
.svg-wrapper {
|
||||
width: 100%; height: 100%;
|
||||
transform-origin: center center;
|
||||
transition: none;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.svg-wrapper svg {
|
||||
max-width: 100%; max-height: 100%;
|
||||
}
|
||||
|
||||
.svg-loading, .svg-error {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-body);
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
/* ---- Parts list sidebar ---- */
|
||||
.parts-sidebar {
|
||||
width: 280px; flex-shrink: 0;
|
||||
background: var(--color-bg-elevated);
|
||||
border-left: 1px solid var(--color-border);
|
||||
overflow-y: auto;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
|
||||
.parts-list__title {
|
||||
padding: var(--space-4);
|
||||
font-family: var(--font-heading);
|
||||
font-weight: var(--heading-weight-primary);
|
||||
font-size: var(--text-body-sm);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-wide);
|
||||
color: var(--color-text-muted);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.part-item {
|
||||
display: flex; align-items: center; gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
cursor: pointer; border-bottom: 1px solid var(--color-border);
|
||||
transition: var(--transition-fast);
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
.part-item:hover, .part-item.is-highlighted {
|
||||
background: var(--color-primary-muted);
|
||||
border-left-color: var(--color-primary);
|
||||
}
|
||||
.part-item.is-active {
|
||||
background: rgba(245, 166, 35, 0.15);
|
||||
border-left-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.part-item__callout {
|
||||
width: 28px; height: 28px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-inverse);
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--text-body-sm);
|
||||
border-radius: var(--radius-full);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.part-item__info { min-width: 0; }
|
||||
.part-item__name {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-primary);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.part-item__number {
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-mono, monospace);
|
||||
}
|
||||
|
||||
.parts-list__empty {
|
||||
padding: var(--space-6);
|
||||
text-align: center;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
/* ---- Hotspot detail panel ---- */
|
||||
.hotspot-panel {
|
||||
position: fixed;
|
||||
bottom: 0; left: 260px; right: 280px;
|
||||
background: var(--color-bg-elevated);
|
||||
border-top: 2px solid var(--color-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
transform: translateY(100%);
|
||||
transition: transform var(--duration-normal) var(--ease-in-out);
|
||||
z-index: 50;
|
||||
}
|
||||
.hotspot-panel.is-open {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.hotspot-panel__close {
|
||||
position: absolute; top: var(--space-3); right: var(--space-4);
|
||||
background: none; border: none; cursor: pointer;
|
||||
font-size: 1.3rem; color: var(--color-text-muted);
|
||||
padding: var(--space-1);
|
||||
}
|
||||
.hotspot-panel__close:hover { color: var(--color-text-primary); }
|
||||
|
||||
.hotspot-detail {
|
||||
display: flex; align-items: center; gap: var(--space-5); flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hotspot-callout {
|
||||
width: 40px; height: 40px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: var(--color-primary); color: var(--color-text-inverse);
|
||||
font-weight: var(--font-weight-bold); font-size: 1.2rem;
|
||||
border-radius: var(--radius-full); flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hotspot-name {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: var(--heading-weight-primary);
|
||||
font-size: 1.1rem;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
.hotspot-partnumber {
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-muted);
|
||||
font-family: var(--font-mono, monospace);
|
||||
}
|
||||
.hotspot-desc {
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-secondary);
|
||||
max-width: 400px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hotspot-actions {
|
||||
display: flex; gap: var(--space-2); margin-left: auto;
|
||||
}
|
||||
|
||||
/* ---- Buttons ---- */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-body-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
background: var(--color-bg-elevated);
|
||||
color: var(--color-text-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-text-inverse);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
.btn-primary:hover { opacity: 0.9; }
|
||||
.btn-accent {
|
||||
background: var(--color-success, #22c55e);
|
||||
color: white;
|
||||
border-color: var(--color-success, #22c55e);
|
||||
}
|
||||
.btn-accent:hover { opacity: 0.9; }
|
||||
.btn-sm {
|
||||
padding: var(--space-1) var(--space-3);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
RESPONSIVE
|
||||
========================================================================= */
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.sidebar { width: 56px; overflow: hidden; }
|
||||
.sidebar__brand { padding: var(--space-3); justify-content: center; }
|
||||
.brand-name { display: none; }
|
||||
.nav-section-label { display: none; }
|
||||
.nav-item { padding: var(--space-2) var(--space-3); justify-content: center; }
|
||||
.nav-item span:not(.nav-item__icon) { display: none; }
|
||||
|
||||
.parts-sidebar { width: 200px; }
|
||||
.hotspot-panel { left: 56px; right: 200px; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.parts-sidebar { display: none; }
|
||||
.hotspot-panel { left: 56px; right: 0; }
|
||||
.diagram-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
|
||||
<!-- SIDEBAR -->
|
||||
<aside class="sidebar themed-scrollbar" id="sidebar" role="navigation" aria-label="Menu principal">
|
||||
<div class="sidebar__brand">
|
||||
<div class="brand-logo" aria-hidden="true">N</div>
|
||||
<div class="brand-name">
|
||||
<span class="brand-name__primary">Nexus</span>
|
||||
<span class="brand-name__sub">Autoparts POS</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="sidebar__nav">
|
||||
<div class="nav-section-label">Principal</div>
|
||||
<a class="nav-item" href="/pos/dashboard" role="menuitem">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
||||
Dashboard
|
||||
</a>
|
||||
<a class="nav-item" href="/pos/sale" role="menuitem">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 01-8 0"/></svg>
|
||||
Punto de Venta
|
||||
</a>
|
||||
<a class="nav-item" href="/pos/inventory" role="menuitem">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/></svg>
|
||||
Inventario
|
||||
</a>
|
||||
<a class="nav-item" href="/pos/catalog" role="menuitem">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 7h8M8 12h8M8 17h5"/></svg>
|
||||
Catalogo
|
||||
</a>
|
||||
<a class="nav-item is-active" href="/pos/diagrams" role="menuitem" aria-current="page">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/></svg>
|
||||
Diagramas
|
||||
</a>
|
||||
<div class="nav-section-label" style="margin-top: var(--space-2);">Gestion</div>
|
||||
<a class="nav-item" href="/pos/customers" role="menuitem">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg>
|
||||
Clientes
|
||||
</a>
|
||||
<a class="nav-item" href="/pos/invoicing" role="menuitem">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
Facturacion
|
||||
</a>
|
||||
<a class="nav-item" href="/pos/accounting" role="menuitem">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/></svg>
|
||||
Contabilidad
|
||||
</a>
|
||||
<a class="nav-item" href="/pos/reports" role="menuitem">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><polyline points="22,12 18,12 15,21 9,3 6,12 2,12"/></svg>
|
||||
Reportes
|
||||
</a>
|
||||
<div class="nav-section-label" style="margin-top: var(--space-2);">Sistema</div>
|
||||
<a class="nav-item" href="/pos/config" role="menuitem">
|
||||
<svg class="nav-item__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93l-1.41 1.41M6.34 17.66l-1.41 1.41M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M12 2v2M12 20v2M2 12h2M20 12h2"/></svg>
|
||||
Configuracion
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar__profile">
|
||||
<div class="profile-avatar" id="profileAvatar" aria-hidden="true">--</div>
|
||||
<div class="profile-info">
|
||||
<div class="profile-info__name" id="profileName">--</div>
|
||||
<div class="profile-info__role" id="profileRole">--</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
<main class="main-content">
|
||||
|
||||
<!-- Page header (list view) -->
|
||||
<div class="page-header" id="pageHeader">
|
||||
<h1 class="page-header__title">Diagramas Explodidos</h1>
|
||||
<input type="text" class="page-header__filter" id="diagramFilter" placeholder="Filtrar diagramas..." />
|
||||
</div>
|
||||
|
||||
<!-- Loading spinner -->
|
||||
<div class="loading" id="loading"><div class="spinner"></div></div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div class="empty-state" id="emptyState">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="color:var(--color-text-disabled)">
|
||||
<path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/>
|
||||
</svg>
|
||||
<div class="empty-state__title">Sin diagramas</div>
|
||||
<div class="empty-state__subtitle">No hay diagramas disponibles. Los diagramas se asocian a vehiculos y categorias de partes.</div>
|
||||
</div>
|
||||
|
||||
<!-- Diagram list (cards grid) -->
|
||||
<div class="page-body" id="diagramList"></div>
|
||||
|
||||
<!-- Diagram viewer (SVG + parts list) -->
|
||||
<div class="diagram-viewer" id="diagramViewer">
|
||||
<div class="viewer-main">
|
||||
<div class="viewer-toolbar">
|
||||
<button class="btn-icon" id="backBtn" title="Volver a la lista">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
</button>
|
||||
<span class="viewer-toolbar__title" id="diagramTitle"></span>
|
||||
<div class="zoom-controls">
|
||||
<button class="btn-icon" id="zoomOutBtn" title="Alejar (-)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
<button class="btn-icon" id="zoomResetBtn" title="Restablecer zoom (0)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 102.13-9.36L1 10"/></svg>
|
||||
</button>
|
||||
<button class="btn-icon" id="zoomInBtn" title="Acercar (+)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SVG display area -->
|
||||
<div class="svg-container" id="svgContainer">
|
||||
<div class="svg-wrapper" id="svgWrapper"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parts list sidebar -->
|
||||
<aside class="parts-sidebar" id="partsList"></aside>
|
||||
</div>
|
||||
|
||||
<!-- Hotspot detail panel (slides up from bottom) -->
|
||||
<div class="hotspot-panel" id="hotspotPanel">
|
||||
<button class="hotspot-panel__close" id="hotspotClose" aria-label="Cerrar">✕</button>
|
||||
<div id="hotspotBody"></div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/pos/static/js/i18n.js"></script>
|
||||
<script src="/pos/static/js/kiosk.js"></script>
|
||||
<script src="/pos/static/js/app-init.js"></script>
|
||||
<script src="/pos/static/js/sidebar.js"></script>
|
||||
<script src="/pos/static/js/diagrams.js"></script>
|
||||
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user