feat: complete session — catalog, marketplace, WhatsApp, peer-to-peer, install scripts
Major features: - Pixel-Perfect glassmorphism design (landing + POS + public catalog) - OEM/Local catalog toggle with Nexpart taxonomy (14 groups, 108 subgroups, 558 part types) - Marketplace B2B Phase 1 (bodegas, POs, status machine, WA+email notifications) - Peer-to-peer inventory (multi-instance, LAN discovery) - WhatsApp: photo→Vision AI, voice→Whisper, conversational quotations - Smart unified search (VIN/plate/part_number/keyword auto-detect) - Shop Supplies tab (vehicle-independent parts) - Chatbot AI fallback chain (5 models) + response cache - CSV inventory import tool + setup_instance.sh installer - Tablet-responsive CSS + sidebar toggle - Filters, export CSV, employee edit, business data save - Quotation system (WA→POS) with auto-print on confirmation - Live stats on landing page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,10 +64,12 @@ def _master_only(fn):
|
||||
@catalog_bp.route('/brands', methods=['GET'])
|
||||
@require_auth('catalog.view')
|
||||
def brands():
|
||||
from services.catalog_modes import normalize_mode
|
||||
year_id = request.args.get('year_id', type=int)
|
||||
mode = normalize_mode(request.args.get('mode'))
|
||||
def _do(master):
|
||||
data = catalog_service.get_brands(master, year_id=year_id)
|
||||
return jsonify({'data': data})
|
||||
data = catalog_service.get_brands(master, year_id=year_id, mode=mode)
|
||||
return jsonify({'data': data, 'mode': mode})
|
||||
return _master_only(_do)
|
||||
|
||||
|
||||
@@ -125,41 +127,191 @@ def engines():
|
||||
@catalog_bp.route('/categories', methods=['GET'])
|
||||
@require_auth('catalog.view')
|
||||
def categories():
|
||||
"""Categories for a vehicle.
|
||||
|
||||
OEM mode: TecDoc part_categories (id_part_category, name).
|
||||
Local mode: 14 Nexpart top-level groups, filtered by what's available
|
||||
for this vehicle. Returns 'slug' (string) instead of integer id.
|
||||
"""
|
||||
from services.catalog_modes import normalize_mode
|
||||
mye_id = request.args.get('mye_id', type=int)
|
||||
mode = normalize_mode(request.args.get('mode'))
|
||||
if not mye_id:
|
||||
return jsonify({'error': 'mye_id required'}), 400
|
||||
def _do(master):
|
||||
data = catalog_service.get_categories(master, mye_id)
|
||||
return jsonify({'data': data})
|
||||
if mode == 'local':
|
||||
data = catalog_service.get_nexpart_groups_for_vehicle(master, mye_id)
|
||||
else:
|
||||
data = catalog_service.get_categories(master, mye_id)
|
||||
return jsonify({'data': data, 'mode': mode})
|
||||
return _master_only(_do)
|
||||
|
||||
|
||||
@catalog_bp.route('/groups', methods=['GET'])
|
||||
@require_auth('catalog.view')
|
||||
def groups():
|
||||
"""Subgroups for a vehicle + parent category.
|
||||
|
||||
OEM mode: TecDoc part_groups within a TecDoc part_category (integer ids).
|
||||
Local mode: Nexpart subgroups within a Nexpart group (string slugs).
|
||||
"""
|
||||
from services.catalog_modes import normalize_mode
|
||||
mye_id = request.args.get('mye_id', type=int)
|
||||
category_id = request.args.get('category_id', type=int)
|
||||
if not mye_id or not category_id:
|
||||
return jsonify({'error': 'mye_id and category_id required'}), 400
|
||||
category_slug = request.args.get('category_slug')
|
||||
mode = normalize_mode(request.args.get('mode'))
|
||||
if not mye_id:
|
||||
return jsonify({'error': 'mye_id required'}), 400
|
||||
def _do(master):
|
||||
data = catalog_service.get_groups(master, mye_id, category_id)
|
||||
return jsonify({'data': data})
|
||||
if mode == 'local':
|
||||
if not category_slug:
|
||||
return jsonify({'error': 'category_slug required for local mode'}), 400
|
||||
data = catalog_service.get_nexpart_subgroups_for_vehicle(master, mye_id, category_slug)
|
||||
else:
|
||||
if not category_id:
|
||||
return jsonify({'error': 'category_id required for oem mode'}), 400
|
||||
data = catalog_service.get_groups(master, mye_id, category_id)
|
||||
return jsonify({'data': data, 'mode': mode})
|
||||
return _master_only(_do)
|
||||
|
||||
|
||||
# ─── Parts with stock enrichment (master + tenant) ───
|
||||
|
||||
@catalog_bp.route('/part-types', methods=['GET'])
|
||||
@require_auth('catalog.view')
|
||||
def part_types():
|
||||
"""Distinct part types (3rd subcategory level) for a vehicle + group/subgroup.
|
||||
|
||||
OEM mode: distinct name_part values within a TecDoc part_group_id.
|
||||
Local mode: Nexpart Part Types within a Nexpart group + subgroup.
|
||||
"""
|
||||
from services.catalog_modes import normalize_mode
|
||||
mye_id = request.args.get('mye_id', type=int)
|
||||
group_id = request.args.get('group_id', type=int)
|
||||
group_slug = request.args.get('group_slug') # parent Nexpart group
|
||||
subgroup_slug = request.args.get('subgroup_slug') # current Nexpart subgroup
|
||||
mode = normalize_mode(request.args.get('mode'))
|
||||
if not mye_id:
|
||||
return jsonify({'error': 'mye_id required'}), 400
|
||||
def _do(master):
|
||||
if mode == 'local':
|
||||
if not group_slug or not subgroup_slug:
|
||||
return jsonify({'error': 'group_slug and subgroup_slug required for local mode'}), 400
|
||||
data = catalog_service.get_nexpart_part_types_for_vehicle(
|
||||
master, mye_id, group_slug, subgroup_slug
|
||||
)
|
||||
else:
|
||||
if not group_id:
|
||||
return jsonify({'error': 'group_id required for oem mode'}), 400
|
||||
data = catalog_service.get_part_types(master, mye_id, group_id)
|
||||
return jsonify({'data': data, 'mode': mode})
|
||||
return _master_only(_do)
|
||||
|
||||
|
||||
@catalog_bp.route('/shop-supplies/groups', methods=['GET'])
|
||||
@require_auth('catalog.view')
|
||||
def shop_supplies_groups():
|
||||
"""Vehicle-independent groups (Chemicals + Tires/Tools)."""
|
||||
def _do(master):
|
||||
data = catalog_service.get_shop_supplies_groups()
|
||||
return jsonify({'data': data})
|
||||
return _master_only(_do)
|
||||
|
||||
|
||||
@catalog_bp.route('/shop-supplies/subgroups', methods=['GET'])
|
||||
@require_auth('catalog.view')
|
||||
def shop_supplies_subgroups():
|
||||
group_slug = request.args.get('group_slug')
|
||||
if not group_slug:
|
||||
return jsonify({'error': 'group_slug required'}), 400
|
||||
def _do(master):
|
||||
data = catalog_service.get_shop_supplies_subgroups(master, group_slug)
|
||||
return jsonify({'data': data})
|
||||
return _master_only(_do)
|
||||
|
||||
|
||||
@catalog_bp.route('/shop-supplies/part-types', methods=['GET'])
|
||||
@require_auth('catalog.view')
|
||||
def shop_supplies_part_types():
|
||||
group_slug = request.args.get('group_slug')
|
||||
subgroup_slug = request.args.get('subgroup_slug')
|
||||
if not group_slug or not subgroup_slug:
|
||||
return jsonify({'error': 'group_slug and subgroup_slug required'}), 400
|
||||
def _do(master):
|
||||
data = catalog_service.get_shop_supplies_part_types(master, group_slug, subgroup_slug)
|
||||
return jsonify({'data': data})
|
||||
return _master_only(_do)
|
||||
|
||||
|
||||
@catalog_bp.route('/shop-supplies/parts', methods=['GET'])
|
||||
@require_auth('catalog.view')
|
||||
def shop_supplies_parts():
|
||||
group_slug = request.args.get('group_slug')
|
||||
subgroup_slug = request.args.get('subgroup_slug')
|
||||
part_type_slug = request.args.get('part_type_slug')
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 30, type=int)
|
||||
if not group_slug or not subgroup_slug or not part_type_slug:
|
||||
return jsonify({'error': 'group_slug, subgroup_slug, part_type_slug required'}), 400
|
||||
def _do(master, tenant, branch_id):
|
||||
result = catalog_service.get_shop_supplies_parts(
|
||||
master, group_slug, subgroup_slug, part_type_slug,
|
||||
tenant, branch_id, page, per_page,
|
||||
)
|
||||
return jsonify(result)
|
||||
return _with_conns(_do)
|
||||
|
||||
|
||||
@catalog_bp.route('/parts', methods=['GET'])
|
||||
@require_auth('catalog.view')
|
||||
def parts():
|
||||
"""Parts list for the deepest navigation level.
|
||||
|
||||
Three call shapes (the endpoint chooses based on which params are present):
|
||||
|
||||
A) OEM mode legacy:
|
||||
?mode=oem&mye_id=&group_id=&part_type=...
|
||||
B) Local mode legacy (TecDoc-style):
|
||||
?mode=local&mye_id=&group_id=&part_type=...
|
||||
C) Local mode Nexpart navigation (NEW):
|
||||
?mode=local&mye_id=&nexpart_group=&nexpart_subgroup=&nexpart_part_type=
|
||||
"""
|
||||
from services.catalog_modes import normalize_mode
|
||||
mye_id = request.args.get('mye_id', type=int)
|
||||
group_id = request.args.get('group_id', type=int)
|
||||
part_type = request.args.get('part_type') # optional 3rd-level (legacy)
|
||||
|
||||
# Nexpart navigation slugs (Local mode only)
|
||||
nexpart_group = request.args.get('nexpart_group')
|
||||
nexpart_subgroup = request.args.get('nexpart_subgroup')
|
||||
nexpart_part_type = request.args.get('nexpart_part_type')
|
||||
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 30, type=int)
|
||||
if not mye_id or not group_id:
|
||||
return jsonify({'error': 'mye_id and group_id required'}), 400
|
||||
mode = normalize_mode(request.args.get('mode'))
|
||||
|
||||
if not mye_id:
|
||||
return jsonify({'error': 'mye_id required'}), 400
|
||||
|
||||
use_nexpart_nav = mode == 'local' and nexpart_group and nexpart_subgroup and nexpart_part_type
|
||||
|
||||
if not use_nexpart_nav and not group_id:
|
||||
return jsonify({'error': 'group_id (or nexpart_group + subgroup + part_type) required'}), 400
|
||||
|
||||
def _do(master, tenant, branch_id):
|
||||
result = catalog_service.get_parts(master, mye_id, group_id, tenant, branch_id, page, per_page)
|
||||
if use_nexpart_nav:
|
||||
result = catalog_service.get_parts_for_nexpart_triple(
|
||||
master, mye_id, nexpart_group, nexpart_subgroup, nexpart_part_type,
|
||||
tenant, branch_id, page, per_page,
|
||||
)
|
||||
elif mode == 'local':
|
||||
result = catalog_service.get_parts_local(
|
||||
master, mye_id, group_id, tenant, branch_id, page, per_page, part_type=part_type,
|
||||
)
|
||||
else:
|
||||
result = catalog_service.get_parts(
|
||||
master, mye_id, group_id, tenant, branch_id, page, per_page, part_type=part_type,
|
||||
)
|
||||
return jsonify(result)
|
||||
return _with_conns(_do)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user