From 5c700e98c393023e2af5b0dffdd4ee9c94e9f638 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Tue, 30 Jun 2026 19:45:41 +0000 Subject: [PATCH] feat(workshop): add new customer from new order modal - Added POST /pos/api/service-orders/customers endpoint for workshop.edit users. - Added 'Nuevo cliente' modal reachable from the new service order form. - After creation the customer is automatically selected in the order. --- pos/blueprints/service_order_bp.py | 36 +++++++++++++++++ pos/static/js/workshop.js | 64 ++++++++++++++++++++++++++++++ pos/templates/workshop.html | 55 +++++++++++++++++++++++-- 3 files changed, 152 insertions(+), 3 deletions(-) diff --git a/pos/blueprints/service_order_bp.py b/pos/blueprints/service_order_bp.py index 5ffb82d..224acf3 100644 --- a/pos/blueprints/service_order_bp.py +++ b/pos/blueprints/service_order_bp.py @@ -3,6 +3,8 @@ Prefix: /pos/api/service-orders """ +import json + from flask import Blueprint, g, jsonify, request from middleware import require_auth from services.service_order_engine import ( @@ -374,6 +376,40 @@ def assign_mechanic_endpoint(so_id): conn.close() +@service_order_bp.route('/customers', methods=['POST']) +@require_auth('workshop.edit') +def create_customer_for_workshop(): + """Create a customer directly from the workshop flow.""" + data = request.get_json() or {} + if not data.get('name'): + return jsonify({'error': 'El nombre es obligatorio'}), 400 + + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + try: + cur.execute(""" + INSERT INTO customers + (branch_id, name, rfc, razon_social, regimen_fiscal, uso_cfdi, + cp, email, phone, address, price_tier, credit_limit, max_discount_pct, vehicle_info) + VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + RETURNING id + """, ( + data.get('branch_id', g.branch_id), data['name'], data.get('rfc'), + data.get('razon_social'), data.get('regimen_fiscal'), data.get('uso_cfdi', 'G03'), + data.get('cp'), data.get('email'), data.get('phone'), data.get('address'), + data.get('price_tier', 1), data.get('credit_limit', 0), data.get('max_discount_pct', 0), + json.dumps(data['vehicle_info']) if data.get('vehicle_info') else None + )) + customer_id = cur.fetchone()[0] + conn.commit() + cur.close(); conn.close() + return jsonify({'id': customer_id, 'message': 'Cliente creado'}), 201 + except Exception as e: + conn.rollback() + cur.close(); conn.close() + return jsonify({'error': str(e)}), 500 + + @service_order_bp.route('/vehicles', methods=['POST']) @require_auth('workshop.edit') def create_vehicle_for_workshop(): diff --git a/pos/static/js/workshop.js b/pos/static/js/workshop.js index 61ed494..4162cd0 100644 --- a/pos/static/js/workshop.js +++ b/pos/static/js/workshop.js @@ -23,6 +23,7 @@ var Workshop = (function() { var vehicleSearchTimer = null; var selectedEditVehicle = null; var newVehicleContext = null; + var newCustomerContext = null; var currentView = 'list'; var currentPage = 1; var perPage = 25; @@ -768,6 +769,65 @@ var Workshop = (function() { } } + function openNewCustomerModal(context) { + newCustomerContext = context || 'new'; + document.getElementById('newCustomerModal').classList.add('is-open'); + document.getElementById('ncName').value = ''; + document.getElementById('ncPhone').value = ''; + document.getElementById('ncEmail').value = ''; + document.getElementById('ncRfc').value = ''; + document.getElementById('ncPriceTier').value = '1'; + document.getElementById('ncAddress').value = ''; + } + + function openNewCustomerModalFromNewOrder() { + openNewCustomerModal('new'); + } + + function closeNewCustomerModal() { + document.getElementById('newCustomerModal').classList.remove('is-open'); + } + + async function saveNewCustomer() { + var name = document.getElementById('ncName').value.trim(); + if (!name) { + alert('El nombre es obligatorio'); + return; + } + var payload = { + name: name, + phone: document.getElementById('ncPhone').value.trim() || null, + email: document.getElementById('ncEmail').value.trim() || null, + rfc: document.getElementById('ncRfc').value.trim() || null, + price_tier: parseInt(document.getElementById('ncPriceTier').value, 10) || 1, + address: document.getElementById('ncAddress').value.trim() || null, + branch_id: (currentOrder ? currentOrder.branch_id : null) || (window.POS_USER ? window.POS_USER.branch_id : null) + }; + try { + var res = await api('POST', '/customers', payload); + var label = name + (payload.phone ? ' (' + payload.phone + ')' : ''); + if (newCustomerContext === 'new') { + var sel = document.getElementById('noCustomer'); + var opt = document.createElement('option'); + opt.value = res.id; + opt.textContent = label.trim(); + sel.appendChild(opt); + sel.value = res.id; + if (typeof customers !== 'undefined') { + customers.push({id: res.id, name: name, phone: payload.phone, rfc: payload.rfc}); + } + } else { + selectedEditCustomer = {id: res.id, name: name}; + document.getElementById('eoCustomerSearch').value = name; + document.getElementById('eoCustomerId').value = res.id; + } + toast('Cliente creado'); + closeNewCustomerModal(); + } catch (e) { + alert('Error: ' + e.message); + } + } + // ─── Actions ─── function changeStatus() { @@ -1181,6 +1241,10 @@ var Workshop = (function() { openNewVehicleModalFromNewOrder: openNewVehicleModalFromNewOrder, closeNewVehicleModal: closeNewVehicleModal, saveNewVehicle: saveNewVehicle, + openNewCustomerModal: openNewCustomerModal, + openNewCustomerModalFromNewOrder: openNewCustomerModalFromNewOrder, + closeNewCustomerModal: closeNewCustomerModal, + saveNewCustomer: saveNewCustomer, }; })(); diff --git a/pos/templates/workshop.html b/pos/templates/workshop.html index b682f6a..b36c508 100644 --- a/pos/templates/workshop.html +++ b/pos/templates/workshop.html @@ -15,7 +15,7 @@ - +