feat(workshop): remove quality_check and add edit order modal
- 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:
@@ -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 = []
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
@@ -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()">×</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í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á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í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ó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>
|
||||
|
||||
Reference in New Issue
Block a user