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.
This commit is contained in:
@@ -3,6 +3,8 @@
|
|||||||
Prefix: /pos/api/service-orders
|
Prefix: /pos/api/service-orders
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
from flask import Blueprint, g, jsonify, request
|
from flask import Blueprint, g, jsonify, request
|
||||||
from middleware import require_auth
|
from middleware import require_auth
|
||||||
from services.service_order_engine import (
|
from services.service_order_engine import (
|
||||||
@@ -374,6 +376,40 @@ def assign_mechanic_endpoint(so_id):
|
|||||||
conn.close()
|
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'])
|
@service_order_bp.route('/vehicles', methods=['POST'])
|
||||||
@require_auth('workshop.edit')
|
@require_auth('workshop.edit')
|
||||||
def create_vehicle_for_workshop():
|
def create_vehicle_for_workshop():
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ var Workshop = (function() {
|
|||||||
var vehicleSearchTimer = null;
|
var vehicleSearchTimer = null;
|
||||||
var selectedEditVehicle = null;
|
var selectedEditVehicle = null;
|
||||||
var newVehicleContext = null;
|
var newVehicleContext = null;
|
||||||
|
var newCustomerContext = null;
|
||||||
var currentView = 'list';
|
var currentView = 'list';
|
||||||
var currentPage = 1;
|
var currentPage = 1;
|
||||||
var perPage = 25;
|
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 ───
|
// ─── Actions ───
|
||||||
|
|
||||||
function changeStatus() {
|
function changeStatus() {
|
||||||
@@ -1181,6 +1241,10 @@ var Workshop = (function() {
|
|||||||
openNewVehicleModalFromNewOrder: openNewVehicleModalFromNewOrder,
|
openNewVehicleModalFromNewOrder: openNewVehicleModalFromNewOrder,
|
||||||
closeNewVehicleModal: closeNewVehicleModal,
|
closeNewVehicleModal: closeNewVehicleModal,
|
||||||
saveNewVehicle: saveNewVehicle,
|
saveNewVehicle: saveNewVehicle,
|
||||||
|
openNewCustomerModal: openNewCustomerModal,
|
||||||
|
openNewCustomerModalFromNewOrder: openNewCustomerModalFromNewOrder,
|
||||||
|
closeNewCustomerModal: closeNewCustomerModal,
|
||||||
|
saveNewCustomer: saveNewCustomer,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<meta name="theme-color" content="#F5A623" />
|
<meta name="theme-color" content="#F5A623" />
|
||||||
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
|
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="/pos/static/css/workshop.css?v=38">
|
<link rel="stylesheet" href="/pos/static/css/workshop.css?v=39">
|
||||||
<style>
|
<style>
|
||||||
.so-notes-grid { display: grid; grid-template-columns: 120px 1fr; gap: var(--space-2); align-items: start; }
|
.so-notes-grid { display: grid; grid-template-columns: 120px 1fr; gap: var(--space-2); align-items: start; }
|
||||||
.so-notes-grid .form-label { margin: 0; padding-top: var(--space-2); }
|
.so-notes-grid .form-label { margin: 0; padding-top: var(--space-2); }
|
||||||
@@ -186,7 +186,10 @@
|
|||||||
<form id="newOrderForm" class="form-grid">
|
<form id="newOrderForm" class="form-grid">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label" for="noCustomer">Cliente</label>
|
<label class="form-label" for="noCustomer">Cliente</label>
|
||||||
<select class="form-input" id="noCustomer" required></select>
|
<div style="display:flex;gap:var(--space-2);">
|
||||||
|
<select class="form-input" id="noCustomer" required style="flex:1;"></select>
|
||||||
|
<button class="btn btn--secondary" type="button" onclick="Workshop.openNewCustomerModalFromNewOrder()">+ Nuevo</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label" for="noVehicle">Vehículo</label>
|
<label class="form-label" for="noVehicle">Vehículo</label>
|
||||||
@@ -376,6 +379,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- New customer modal -->
|
||||||
|
<div class="modal-overlay" id="newCustomerModal">
|
||||||
|
<div class="modal modal--sm">
|
||||||
|
<div class="modal__header">
|
||||||
|
<h2 class="modal__title">Nuevo cliente</h2>
|
||||||
|
<button class="modal__close" onclick="Workshop.closeNewCustomerModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal__body">
|
||||||
|
<form class="form-grid" id="newCustomerForm" onsubmit="return false;">
|
||||||
|
<div class="form-field form-field--span2">
|
||||||
|
<label class="form-label" for="ncName">Nombre *</label>
|
||||||
|
<input class="form-input" id="ncName" placeholder="Nombre completo" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="form-label" for="ncPhone">Teléfono</label>
|
||||||
|
<input class="form-input" id="ncPhone" placeholder="3312345678" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="form-label" for="ncEmail">Correo</label>
|
||||||
|
<input class="form-input" id="ncEmail" placeholder="cliente@ejemplo.com" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="form-label" for="ncRfc">RFC</label>
|
||||||
|
<input class="form-input" id="ncRfc" placeholder="XAXX010101000" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="form-label" for="ncPriceTier">Lista de precios</label>
|
||||||
|
<select class="form-input" id="ncPriceTier">
|
||||||
|
<option value="1">Mostrador</option>
|
||||||
|
<option value="2">Taller</option>
|
||||||
|
<option value="3">Mayoreo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-field form-field--span2">
|
||||||
|
<label class="form-label" for="ncAddress">Dirección</label>
|
||||||
|
<input class="form-input" id="ncAddress" placeholder="Calle, número, colonia" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal__footer">
|
||||||
|
<button class="btn btn--ghost" onclick="Workshop.closeNewCustomerModal()">Cancelar</button>
|
||||||
|
<button class="btn btn--primary" onclick="Workshop.saveNewCustomer()">Guardar cliente</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Catalog modal -->
|
<!-- Catalog modal -->
|
||||||
<div class="modal-overlay" id="catalogModal">
|
<div class="modal-overlay" id="catalogModal">
|
||||||
<div class="modal modal--lg">
|
<div class="modal modal--lg">
|
||||||
@@ -432,7 +481,7 @@
|
|||||||
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
|
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
|
||||||
<script src="/pos/static/js/sidebar.js?v=36" defer></script>
|
<script src="/pos/static/js/sidebar.js?v=36" defer></script>
|
||||||
<script src="/pos/static/js/offline-banner.js" defer></script>
|
<script src="/pos/static/js/offline-banner.js" defer></script>
|
||||||
<script src="/pos/static/js/workshop.js?v=38" defer></script>
|
<script src="/pos/static/js/workshop.js?v=39" defer></script>
|
||||||
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
|
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
|
||||||
<script src="/pos/static/js/pwa-install.js" defer></script>
|
<script src="/pos/static/js/pwa-install.js" defer></script>
|
||||||
<script src="/pos/static/js/chat.js" defer></script>
|
<script src="/pos/static/js/chat.js" defer></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user