feat(workshop): remove quality_check and add edit order modal
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

- Removed 'Control de calidad' from service order pipeline (VALID_TRANSITIONS, status labels, Kanban columns).

- Added editable fields for customer, vehicle, branch, mechanic, priority, mileage, fuel level, estimated completion/cost and reception notes.

- Added customer/vehicle autocomplete in the edit order modal.
This commit is contained in:
2026-06-30 19:30:51 +00:00
parent b539c92daf
commit ff600ed7df
3 changed files with 271 additions and 10 deletions

View File

@@ -12,8 +12,7 @@ VALID_TRANSITIONS = {
'received': ['diagnosis', 'cancelled'],
'diagnosis': ['waiting_parts', 'repair', 'cancelled'],
'waiting_parts': ['repair', 'cancelled'],
'repair': ['quality_check', 'cancelled'],
'quality_check': ['ready', 'cancelled'],
'repair': ['ready', 'cancelled'],
'ready': ['delivered', 'cancelled'],
'delivered': [],
'cancelled': [],
@@ -455,9 +454,10 @@ def remove_labor(conn, labor_id):
def update_service_order(conn, so_id, data):
"""Update general service order fields."""
cur = conn.cursor()
allowed = ['priority', 'reception_notes', 'diagnosis_notes', 'repair_notes',
allowed = ['customer_id', 'vehicle_id', 'branch_id', 'priority',
'reception_notes', 'diagnosis_notes', 'repair_notes',
'delivery_notes', 'estimated_cost', 'estimated_completion',
'employee_id', 'mileage_out', 'fuel_level', 'final_cost',
'employee_id', 'mileage_in', 'mileage_out', 'fuel_level', 'final_cost',
'delivery_method', 'courier_id', 'is_direct']
sets = []
vals = []

View File

@@ -18,6 +18,10 @@ var Workshop = (function() {
var currentOrder = null;
var selectedInventoryItem = null;
var itemSearchTimer = null;
var customerSearchTimer = null;
var selectedEditCustomer = null;
var vehicleSearchTimer = null;
var selectedEditVehicle = null;
var currentView = 'list';
var currentPage = 1;
var perPage = 25;
@@ -35,7 +39,6 @@ var Workshop = (function() {
{key: 'diagnosis', label: 'Diagnóstico'},
{key: 'waiting_parts', label: 'Espera refacciones'},
{key: 'repair', label: 'En reparación'},
{key: 'quality_check', label: 'Control calidad'},
{key: 'ready', label: 'Listo'},
{key: 'delivered', label: 'Entregado'},
];
@@ -45,7 +48,6 @@ var Workshop = (function() {
diagnosis: 'Diagnóstico',
waiting_parts: 'Espera refacciones',
repair: 'En reparación',
quality_check: 'Control calidad',
ready: 'Listo',
delivered: 'Entregado',
cancelled: 'Cancelado'
@@ -55,8 +57,7 @@ var Workshop = (function() {
received: ['diagnosis', 'cancelled'],
diagnosis: ['waiting_parts', 'repair', 'cancelled'],
waiting_parts: ['repair', 'cancelled'],
repair: ['quality_check', 'cancelled'],
quality_check: ['ready', 'cancelled'],
repair: ['ready', 'cancelled'],
ready: ['delivered', 'cancelled'],
delivered: [],
cancelled: []
@@ -100,6 +101,11 @@ var Workshop = (function() {
return el.innerHTML;
}
function escJs(s) {
if (s == null) return '';
return String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"');
}
function api(method, url, body) {
var opts = {method: method, headers: headers()};
if (body) opts.body = JSON.stringify(body);
@@ -414,6 +420,7 @@ var Workshop = (function() {
'</div>' : '';
footer.innerHTML = statusHtml +
'<button class="btn btn--ghost" onclick="Workshop.closeDetailModal()">Cerrar</button>' +
(canEdit ? '<button class="btn btn--secondary" onclick="Workshop.openEditOrderModal()">Editar orden</button>' : '') +
(canDelete ? '<button class="btn btn--danger" onclick="Workshop.deleteOrder()">Eliminar orden</button>' : '') +
'<button class="btn btn--secondary" onclick="Workshop.printOrder()">' +
'<svg viewBox="0 0 24 24"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>' +
@@ -528,6 +535,175 @@ var Workshop = (function() {
currentOrder = null;
}
function openEditOrderModal() {
if (!currentOrder) return;
populateEditOrderModal();
document.getElementById('editOrderModal').classList.add('is-open');
}
function closeEditOrderModal() {
document.getElementById('editOrderModal').classList.remove('is-open');
selectedEditCustomer = null;
selectedEditVehicle = null;
}
function toDatetimeLocal(d) {
if (!d) return '';
var date = new Date(d);
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
return date.toISOString().slice(0, 16);
}
async function populateEditOrderModal() {
var o = currentOrder;
selectedEditCustomer = o.customer_id ? {id: o.customer_id, name: o.customer_name || ''} : null;
selectedEditVehicle = o.vehicle_id ? {id: o.vehicle_id, label: (o.vehicle_plate || '') + ' ' + (o.vehicle_make || '') + ' ' + (o.vehicle_model || '')} : null;
document.getElementById('eoCustomerSearch').value = selectedEditCustomer ? selectedEditCustomer.name : '';
document.getElementById('eoCustomerId').value = selectedEditCustomer ? selectedEditCustomer.id : '';
document.getElementById('eoVehicleSearch').value = selectedEditVehicle ? selectedEditVehicle.label.trim() : '';
document.getElementById('eoVehicleId').value = selectedEditVehicle ? selectedEditVehicle.id : '';
// Branches
var branchSel = document.getElementById('eoBranch');
branchSel.innerHTML = branches.map(function(b) {
return '<option value="' + b.id + '"' + (b.id === o.branch_id ? ' selected' : '') + '>' + esc(b.name) + '</option>';
}).join('');
// Mechanics/employees
var mechSel = document.getElementById('eoMechanic');
mechSel.innerHTML = '<option value="">— Ninguno —</option>';
try {
var res = await fetch('/pos/api/config/employees', {headers: headers()});
var json = await res.json();
(json.data || []).forEach(function(e) {
if (!e.is_active) return;
mechSel.innerHTML += '<option value="' + e.id + '"' + (e.id === o.employee_id ? ' selected' : '') + '>' + esc(e.name) + '</option>';
});
} catch (e) {}
document.getElementById('eoPriority').value = o.priority || 'normal';
document.getElementById('eoFuelLevel').value = o.fuel_level || '';
document.getElementById('eoMileageIn').value = o.mileage_in != null ? o.mileage_in : '';
document.getElementById('eoMileageOut').value = o.mileage_out != null ? o.mileage_out : '';
document.getElementById('eoEstimatedCompletion').value = toDatetimeLocal(o.estimated_completion);
document.getElementById('eoEstimatedCost').value = o.estimated_cost != null ? o.estimated_cost : '';
document.getElementById('eoNotes').value = o.reception_notes || '';
}
function hideEditSearchResults(type) {
var box = document.getElementById(type === 'customer' ? 'eoCustomerResults' : 'eoVehicleResults');
if (box) box.style.display = 'none';
}
function searchCustomersForSO() {
var input = document.getElementById('eoCustomerSearch');
var box = document.getElementById('eoCustomerResults');
var q = input.value.trim();
selectedEditCustomer = null;
document.getElementById('eoCustomerId').value = '';
if (!q || q.length < 2) {
box.style.display = 'none'; box.innerHTML = '';
return;
}
clearTimeout(customerSearchTimer);
customerSearchTimer = setTimeout(function() {
fetch('/pos/api/customers?q=' + encodeURIComponent(q) + '&per_page=20', {headers: headers()})
.then(function(r) { return r.json(); })
.then(function(d) {
var items = d.data || [];
if (!items.length) {
box.innerHTML = '<div style="padding:var(--space-2);color:var(--color-text-muted);">Sin resultados</div>';
box.style.display = 'block'; return;
}
box.innerHTML = items.map(function(c) {
return '<div class="so-search-result" style="padding:var(--space-2);cursor:pointer;border-bottom:1px solid var(--color-border);" onclick="Workshop.selectEditCustomer(' + c.id + ', \'' + escJs(c.name) + '\')">' +
'<strong>' + esc(c.name) + '</strong>' +
'<small>' + esc(c.phone || '') + ' · ' + esc(c.rfc || '') + '</small>' +
'</div>';
}).join('');
box.style.display = 'block';
})
.catch(function() { box.style.display = 'none'; });
}, 250);
}
function selectEditCustomer(id, name) {
selectedEditCustomer = {id: id, name: name};
document.getElementById('eoCustomerSearch').value = name;
document.getElementById('eoCustomerId').value = id;
hideEditSearchResults('customer');
}
function searchVehiclesForSO() {
var input = document.getElementById('eoVehicleSearch');
var box = document.getElementById('eoVehicleResults');
var q = input.value.trim();
selectedEditVehicle = null;
document.getElementById('eoVehicleId').value = '';
if (!q || q.length < 2) {
box.style.display = 'none'; box.innerHTML = '';
return;
}
clearTimeout(vehicleSearchTimer);
vehicleSearchTimer = setTimeout(function() {
fetch('/pos/api/fleet/vehicles?q=' + encodeURIComponent(q) + '&per_page=20&active_only=true', {headers: headers()})
.then(function(r) { return r.json(); })
.then(function(d) {
var items = d.data || [];
if (!items.length) {
box.innerHTML = '<div style="padding:var(--space-2);color:var(--color-text-muted);">Sin resultados</div>';
box.style.display = 'block'; return;
}
box.innerHTML = items.map(function(v) {
var label = (v.plate || '') + ' · ' + (v.make || '') + ' ' + (v.model || '');
return '<div class="so-search-result" style="padding:var(--space-2);cursor:pointer;border-bottom:1px solid var(--color-border);" onclick="Workshop.selectEditVehicle(' + v.id + ', \'' + escJs(label) + '\')">' +
'<strong>' + esc(v.plate || '') + '</strong>' +
'<small>' + esc(v.make || '') + ' ' + esc(v.model || '') + ' · ' + esc(v.owner_name || '') + '</small>' +
'</div>';
}).join('');
box.style.display = 'block';
})
.catch(function() { box.style.display = 'none'; });
}, 250);
}
function selectEditVehicle(id, label) {
selectedEditVehicle = {id: id, label: label};
document.getElementById('eoVehicleSearch').value = label;
document.getElementById('eoVehicleId').value = id;
hideEditSearchResults('vehicle');
}
async function saveOrderChanges() {
if (!currentOrderId) return;
var customerId = selectedEditCustomer ? selectedEditCustomer.id : parseInt(document.getElementById('eoCustomerId').value, 10) || null;
var vehicleId = selectedEditVehicle ? selectedEditVehicle.id : parseInt(document.getElementById('eoVehicleId').value, 10) || null;
var payload = {
customer_id: customerId,
vehicle_id: vehicleId,
branch_id: parseInt(document.getElementById('eoBranch').value, 10) || null,
employee_id: parseInt(document.getElementById('eoMechanic').value, 10) || null,
priority: document.getElementById('eoPriority').value,
fuel_level: document.getElementById('eoFuelLevel').value || null,
mileage_in: document.getElementById('eoMileageIn').value ? parseInt(document.getElementById('eoMileageIn').value, 10) : null,
mileage_out: document.getElementById('eoMileageOut').value ? parseInt(document.getElementById('eoMileageOut').value, 10) : null,
estimated_completion: document.getElementById('eoEstimatedCompletion').value || null,
estimated_cost: document.getElementById('eoEstimatedCost').value ? parseFloat(document.getElementById('eoEstimatedCost').value) : null,
reception_notes: document.getElementById('eoNotes').value
};
try {
await api('PUT', '/' + currentOrderId, payload);
toast('Orden actualizada');
closeEditOrderModal();
closeDetailModal();
loadSummary();
loadOrders();
} catch (e) {
alert('Error: ' + e.message);
}
}
// ─── Actions ───
function changeStatus() {
@@ -930,6 +1106,13 @@ var Workshop = (function() {
closeCatalogModal: closeCatalogModal,
addCatalogItem: addCatalogItem,
deleteCatalogItem: deleteCatalogItem,
openEditOrderModal: openEditOrderModal,
closeEditOrderModal: closeEditOrderModal,
saveOrderChanges: saveOrderChanges,
selectEditCustomer: selectEditCustomer,
selectEditVehicle: selectEditVehicle,
searchCustomersForSO: searchCustomersForSO,
searchVehiclesForSO: searchVehiclesForSO,
};
})();

View File

@@ -15,7 +15,7 @@
<meta name="theme-color" content="#F5A623" />
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
<link rel="stylesheet" href="/pos/static/css/workshop.css?v=35">
<link rel="stylesheet" href="/pos/static/css/workshop.css?v=36">
<style>
.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); }
@@ -244,6 +244,84 @@
</div>
</div>
<!-- Edit order modal -->
<div class="modal-overlay" id="editOrderModal">
<div class="modal modal--md">
<div class="modal__header">
<h2 class="modal__title">Editar orden</h2>
<button class="modal__close" onclick="Workshop.closeEditOrderModal()">&times;</button>
</div>
<div class="modal__body">
<form class="form-grid" id="editOrderForm" onsubmit="return false;">
<div class="form-field form-field--span2" style="position:relative;">
<label class="form-label" for="eoCustomerSearch">Cliente</label>
<input class="form-input" id="eoCustomerSearch" autocomplete="off" placeholder="Buscar cliente..." oninput="Workshop.searchCustomersForSO()" />
<div id="eoCustomerResults" style="display:none;position:absolute;z-index:10;top:100%;left:0;right:0;max-height:180px;overflow-y:auto;background:#fff;border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:0 4px 12px rgba(0,0,0,.15);"></div>
<input type="hidden" id="eoCustomerId" />
</div>
<div class="form-field form-field--span2" style="position:relative;">
<label class="form-label" for="eoVehicleSearch">Veh&iacute;culo</label>
<input class="form-input" id="eoVehicleSearch" autocomplete="off" placeholder="Buscar por placa..." oninput="Workshop.searchVehiclesForSO()" />
<div id="eoVehicleResults" style="display:none;position:absolute;z-index:10;top:100%;left:0;right:0;max-height:180px;overflow-y:auto;background:#fff;border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:0 4px 12px rgba(0,0,0,.15);"></div>
<input type="hidden" id="eoVehicleId" />
</div>
<div class="form-field">
<label class="form-label" for="eoBranch">Sucursal</label>
<select class="form-input" id="eoBranch"></select>
</div>
<div class="form-field">
<label class="form-label" for="eoMechanic">Mec&aacute;nico asignado</label>
<select class="form-input" id="eoMechanic"><option value="">— Ninguno —</option></select>
</div>
<div class="form-field">
<label class="form-label" for="eoPriority">Prioridad</label>
<select class="form-input" id="eoPriority">
<option value="low">Baja</option>
<option value="normal">Normal</option>
<option value="high">Alta</option>
<option value="urgent">Urgente</option>
</select>
</div>
<div class="form-field">
<label class="form-label" for="eoFuelLevel">Nivel de combustible</label>
<select class="form-input" id="eoFuelLevel">
<option value=""></option>
<option value="empty">Vac&iacute;o</option>
<option value="quarter">1/4</option>
<option value="half">1/2</option>
<option value="three_quarters">3/4</option>
<option value="full">Lleno</option>
</select>
</div>
<div class="form-field">
<label class="form-label" for="eoMileageIn">Kilometraje entrada</label>
<input class="form-input" type="number" id="eoMileageIn" />
</div>
<div class="form-field">
<label class="form-label" for="eoMileageOut">Kilometraje salida</label>
<input class="form-input" type="number" id="eoMileageOut" />
</div>
<div class="form-field">
<label class="form-label" for="eoEstimatedCompletion">Entrega estimada</label>
<input class="form-input" type="datetime-local" id="eoEstimatedCompletion" />
</div>
<div class="form-field">
<label class="form-label" for="eoEstimatedCost">Costo estimado</label>
<input class="form-input" type="number" id="eoEstimatedCost" step="0.01" />
</div>
<div class="form-field form-field--span2">
<label class="form-label" for="eoNotes">Notas de recepci&oacute;n</label>
<textarea class="form-input" id="eoNotes" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal__footer">
<button class="btn btn--ghost" onclick="Workshop.closeEditOrderModal()">Cancelar</button>
<button class="btn btn--primary" onclick="Workshop.saveOrderChanges()">Guardar cambios</button>
</div>
</div>
</div>
<!-- Catalog modal -->
<div class="modal-overlay" id="catalogModal">
<div class="modal modal--lg">
@@ -300,7 +378,7 @@
<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/offline-banner.js" defer></script>
<script src="/pos/static/js/workshop.js?v=35" defer></script>
<script src="/pos/static/js/workshop.js?v=36" defer></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/chat.js" defer></script>