/** * workshop.js — Taller / Service Orders for Nexus POS * Supports list + kanban views, filters, bitacora and role-based price hiding. */ var Workshop = (function() { 'use strict'; var API = '/pos/api/service-orders'; var token = localStorage.getItem('pos_token'); var orders = []; var catalog = []; var customers = []; var vehicles = []; var employees = []; var couriers = []; var branches = []; var currentOrderId = null; var currentOrder = null; var selectedInventoryItem = null; var itemSearchTimer = null; var customerSearchTimer = null; var selectedEditCustomer = null; var vehicleSearchTimer = null; var selectedEditVehicle = null; var newVehicleContext = null; var newCustomerContext = null; var currentView = 'list'; var currentPage = 1; var perPage = 25; var user = window.POS_USER || {}; var role = (user.role || '').toLowerCase(); var isRestricted = role === 'workshop' || role === 'mechanic'; var isMechanic = role === 'mechanic'; var hidePrices = isRestricted; var perms = user.permissions || []; // Owner/admin/counter/cashier can create/edit/delete service orders. // Mechanics have limited access: view allowed statuses, change status, edit diagnosis/repair notes. var canEdit = role === 'owner' || role === 'admin' || role === 'counter' || role === 'cashier'; var canCreate = canEdit; var canDelete = role === 'owner' || role === 'admin'; var canSell = role === 'owner' || role === 'admin' || role === 'counter' || role === 'cashier' || perms.indexOf('pos.sell') !== -1; var canChangeStatus = canEdit || isMechanic; var COLUMNS = [ {key: 'por_revisar', label: 'Por revisar'}, {key: 'en_revision', label: 'En revisión'}, {key: 'revisada', label: 'Revisada'}, {key: 'cotizada', label: 'Cotizada'}, {key: 'por_autorizar', label: 'Por autorizar'}, {key: 'autorizada', label: 'Autorizada'}, {key: 'autorizacion_parcial', label: 'Autorización parcial'}, {key: 'en_reparacion', label: 'En reparación'}, {key: 'reparada', label: 'Reparada'}, {key: 'por_entregar', label: 'Por entregar'}, {key: 'entregado', label: 'Entregado'}, {key: 'por_enviar', label: 'Por enviar'}, {key: 'enviado', label: 'Enviado'}, {key: 'por_facturar', label: 'Por facturar'}, {key: 'facturada', label: 'Facturada'}, {key: 'por_recolectar', label: 'Por recolectar'}, ]; var STATUS_LABELS = { por_revisar: 'Por revisar', en_revision: 'En revisión', revisada: 'Revisada', cotizada: 'Cotizada', por_autorizar: 'Por autorizar', autorizada: 'Autorizada', autorizacion_parcial: 'Autorización parcial', en_reparacion: 'En reparación', reparada: 'Reparada', por_entregar: 'Por entregar', entregado: 'Entregado', por_enviar: 'Por enviar', enviado: 'Enviado', por_facturar: 'Por facturar', facturada: 'Facturada', por_recolectar: 'Por recolectar', cancelada: 'Cancelada' }; var VALID_NEXT = { por_revisar: ['en_revision', 'cancelada'], en_revision: ['revisada', 'por_revisar', 'cancelada'], revisada: ['cotizada', 'en_revision', 'cancelada'], cotizada: ['por_autorizar', 'revisada', 'cancelada'], por_autorizar: ['autorizada', 'autorizacion_parcial', 'cotizada', 'cancelada'], autorizada: ['en_reparacion', 'por_autorizar', 'cancelada'], autorizacion_parcial: ['en_reparacion', 'por_autorizar', 'cancelada'], en_reparacion: ['reparada', 'por_autorizar', 'cancelada'], reparada: ['por_entregar', 'en_reparacion', 'cancelada'], por_entregar: ['entregado', 'por_enviar', 'reparada', 'cancelada'], por_enviar: ['enviado', 'por_entregar', 'cancelada'], enviado: ['entregado', 'por_enviar', 'cancelada'], por_recolectar: ['por_revisar', 'cancelada'], por_facturar: ['facturada', 'cancelada'], entregado: ['por_facturar'], facturada: [], cancelada: [] }; var DELIVERY_LABELS = { pickup: 'Mostrador', delivery: 'Envío a domicilio', courier: 'Motociclista' }; var ITEM_STATUS_LABELS = { por_revisar: 'Por revisar', revisando: 'Revisando', revisado: 'Revisado', cotizado: 'Cotizado', por_autorizar: 'Por autorizar', autorizado: 'Autorizado', en_reparacion: 'En reparación', reparado: 'Reparado', por_entregar: 'Por entregar', entregado: 'Entregado', por_enviar: 'Por enviar', enviado: 'Enviado', cancelado: 'Cancelado' }; function headers() { return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }; } function fmt(n) { if (n == null) return '0'; return parseFloat(n).toLocaleString('es-MX'); } function fmtMoney(n) { if (n == null) return '$0.00'; return '$' + parseFloat(n).toLocaleString('es-MX', {minimumFractionDigits: 2, maximumFractionDigits: 2}); } // Price based on customer tier: base price (price_1) with tier discount. // Tier 2 = Taller (5% off), Tier 3 = Mayoreo (10% off), Tier 1 = base. function priceForTier(basePrice, tier) { var p = parseFloat(basePrice) || 0; var t = parseInt(tier, 10) || 1; if (t === 2) return Math.round(p * 0.95 * 100) / 100; if (t === 3) return Math.round(p * 0.90 * 100) / 100; return p; } function fmtDate(d) { if (!d) return '—'; var dt = new Date(d); if (isNaN(dt.getTime())) return d; return dt.toLocaleDateString('es-MX', {day: '2-digit', month: '2-digit', year: 'numeric'}) + ' ' + dt.toLocaleTimeString('es-MX', {hour: '2-digit', minute: '2-digit', hour12: false}); } function esc(s) { if (s == null) return ''; var el = document.createElement('div'); el.textContent = s; 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); return fetch(API + url, opts).then(function(r) { return r.json().then(function(data) { if (!r.ok) throw new Error(data.error || r.statusText); return data; }); }); } // ─── Init ─── function init() { var savedView = localStorage.getItem('workshop_view'); if (savedView) currentView = savedView; bindFilters(); bindDeliverySelect(); if (!canCreate) { var btnNewOrder = document.getElementById('btnNewOrder'); if (btnNewOrder) btnNewOrder.style.display = 'none'; } if (isRestricted) { document.querySelectorAll('.restricted-hide').forEach(function(el) { el.style.display = 'none'; }); var searchInput = document.getElementById('filterSearch'); if (searchInput) searchInput.placeholder = 'Buscar orden'; // Limit status filter to the only status taller accounts can see. var statusSel = document.getElementById('filterStatus'); if (statusSel) { Array.from(statusSel.options).forEach(function(opt) { if (opt.value && opt.value !== 'autorizada') opt.remove(); }); statusSel.value = 'autorizada'; } } if (hidePrices) { var btnCatalog = document.getElementById('btnCatalog'); if (btnCatalog) btnCatalog.style.display = 'none'; document.querySelectorAll('.price-col').forEach(function(el) { el.style.display = 'none'; }); } loadReferenceData(); loadSummary(); setView(currentView); loadOrders(); loadCatalog(); } function bindFilters() { ['filterBranch','filterStatus','filterDelivery','filterDirect','filterSearch'].forEach(function(id) { var el = document.getElementById(id); if (!el) return; el.addEventListener('change', function() { currentPage = 1; loadOrders(); }); if (el.tagName === 'INPUT' && id === 'filterSearch') { el.addEventListener('keyup', debounce(function() { currentPage = 1; loadOrders(); }, 350)); } }); } function bindDeliverySelect() { var sel = document.getElementById('noDelivery'); if (sel) { sel.addEventListener('change', function() { var cf = document.getElementById('courierField'); if (cf) cf.style.display = (sel.value === 'delivery' || sel.value === 'courier') ? 'block' : 'none'; }); } var eoSel = document.getElementById('eoDelivery'); if (eoSel) { eoSel.addEventListener('change', function() { var cf = document.getElementById('eoCourierField'); if (cf) cf.style.display = (eoSel.value === 'delivery' || eoSel.value === 'courier') ? 'block' : 'none'; }); } } function debounce(fn, ms) { var t; return function() { clearTimeout(t); t = setTimeout(fn, ms); }; } // ─── Summary ─── function loadSummary() { fetch(API + '/kanban/summary', {headers: headers()}) .then(function(r) { return r.json(); }) .then(function(d) { var cards = document.querySelectorAll('#statsRow .summary-card'); if (isRestricted) { // Taller accounts only see authorized orders. cards.forEach(function(card, idx) { if (idx === 0) { card.style.display = ''; var label = card.querySelector('.summary-card__label'); if (label) label.textContent = 'Autorizadas'; } else { card.style.display = 'none'; } }); document.getElementById('statReceived').textContent = fmt(d.autorizada || 0); return; } cards.forEach(function(card) { card.style.display = ''; }); document.getElementById('statReceived').textContent = fmt(d.por_revisar || 0); document.getElementById('statRepair').textContent = fmt((d.en_reparacion || 0) + (d.en_revision || 0) + (d.revisada || 0) + (d.cotizada || 0) + (d.por_autorizar || 0)); document.getElementById('statReady').textContent = fmt(d.por_entregar || 0); document.getElementById('statOverdue').textContent = fmt(d.overdue || 0); }) .catch(function() {}); } // ─── Orders loading ─── function getFilterQuery() { var params = []; var status = document.getElementById('filterStatus').value; var delivery = document.getElementById('filterDelivery').value; var branch = document.getElementById('filterBranch').value; var direct = document.getElementById('filterDirect').checked; var q = document.getElementById('filterSearch').value.trim(); if (status) params.push('status=' + encodeURIComponent(status)); if (delivery) params.push('delivery_method=' + encodeURIComponent(delivery)); if (branch) params.push('branch_id=' + encodeURIComponent(branch)); if (direct) params.push('is_direct=true'); if (q) params.push('q=' + encodeURIComponent(q)); params.push('page=' + currentPage); params.push('per_page=' + perPage); return '?' + params.join('&'); } function loadOrders() { fetch(API + getFilterQuery(), {headers: headers()}) .then(function(r) { return r.json(); }) .then(function(d) { orders = d.data || []; var pagination = d.pagination || {}; totalPages = pagination.total_pages || 1; if (currentView === 'list') { renderList(); renderPagination(); } else { renderKanban(); } }) .catch(function(e) { console.error(e); document.getElementById('listBody').innerHTML = 'Error cargando órdenes'; }); } var totalPages = 1; // ─── View switching ─── function setView(view) { currentView = view; localStorage.setItem('workshop_view', view); document.querySelectorAll('.view-switch__btn').forEach(function(b) { b.classList.toggle('is-active', b.dataset.view === view); }); document.getElementById('listView').style.display = view === 'list' ? 'block' : 'none'; document.getElementById('kanbanBoard').style.display = view === 'kanban' ? 'flex' : 'none'; if (view === 'list') { renderList(); renderPagination(); } else { renderKanban(); } } // ─── List view ─── function renderList() { var body = document.getElementById('listBody'); var fullCols = 8; var restrictedCols = 4; var cols = isRestricted ? restrictedCols : fullCols; if (!orders.length) { body.innerHTML = 'No se encontraron órdenes'; return; } body.innerHTML = orders.map(function(o) { var statusCell = '' + esc(STATUS_LABELS[o.status] || o.status) + ''; var actionCell = ''; var base = '' + esc(o.branch_name || '—') + '' + '' + esc(o.order_number) + '' + statusCell + actionCell; if (isRestricted) { return '' + base + ''; } var vehicle = esc(o.vehicle_description || (o.vehicle_plate ? (o.vehicle_plate + ' ' + (o.vehicle_make || '') + ' ' + (o.vehicle_model || '')).trim() : '—')); return '' + '' + esc(o.branch_name || '—') + '' + '' + esc(o.order_number) + '' + '' + esc(o.customer_name || 'Cliente general') + '' + '' + esc(o.workshop_name || '—') + '' + '' + vehicle + '' + statusCell + '' + fmtMoney(o.total) + '' + actionCell + ''; }).join(''); if (hidePrices) { document.querySelectorAll('.price-col').forEach(function(el) { el.style.display = 'none'; }); } } function renderPagination() { var el = document.getElementById('listPagination'); if (totalPages <= 1) { el.innerHTML = ''; return; } var html = '' + 'Página ' + currentPage + ' de ' + totalPages + '' + ''; el.innerHTML = html; } function goPage(p) { if (p < 1 || p > totalPages) return; currentPage = p; loadOrders(); } // ─── Kanban view ─── function renderKanban() { var board = document.getElementById('kanbanBoard'); board.innerHTML = ''; var hiddenCols = []; if (isRestricted) { // Taller accounts only see the "autorizada" column. hiddenCols = COLUMNS.filter(function(c) { return c.key !== 'autorizada'; }).map(function(c) { return c.key; }); } else if (isMechanic) { hiddenCols = ['cotizada','por_autorizar','autorizada','autorizacion_parcial','por_facturar','facturada']; } COLUMNS.filter(function(col) { return hiddenCols.indexOf(col.key) === -1; }).forEach(function(col) { var colOrders = orders.filter(function(o) { return o.status === col.key; }); var colEl = document.createElement('div'); colEl.className = 'kanban-column'; colEl.innerHTML = '
' + ' ' + esc(col.label) + '' + ' ' + colOrders.length + '' + '
' + '
'; board.appendChild(colEl); var body = colEl.querySelector('.kanban-column__body'); if (!colOrders.length) { body.innerHTML = '
Sin órdenes
'; } else { colOrders.forEach(function(o) { body.appendChild(renderCard(o)); }); } }); } function priorityLabel(p) { var map = {normal: 'Normal', high: 'Alta', urgent: 'Urgente'}; return map[p] || p; } function statusBadgeClass(status) { return 'badge--' + (status || 'pending'); } function renderCard(o) { var card = document.createElement('div'); card.className = 'kanban-card'; card.onclick = function() { openDetail(o.id); }; var priceHtml = hidePrices ? '' : '' + fmtMoney(o.estimated_cost || o.total) + ''; if (isRestricted) { card.innerHTML = '
' + ' ' + esc(o.order_number) + '' + ' ' + esc(STATUS_LABELS[o.status] || o.status) + '' + '
' + '
' + esc(o.branch_name || '—') + '
'; return card; } card.innerHTML = '
' + ' ' + esc(o.order_number) + '' + ' ' + esc(STATUS_LABELS[o.status] || o.status) + '' + '
' + '
' + esc(o.customer_name || 'Cliente general') + '
' + '
' + esc(o.vehicle_description || o.vehicle_plate || 'Sin vehículo') + '
' + '
' + ' 🔧 ' + esc(o.employee_name || 'Sin asignar') + '' + priceHtml + '
'; return card; } // ─── Detail modal ─── function openDetail(id) { currentOrderId = id; fetch(API + '/' + id, {headers: headers()}) .then(function(r) { return r.json(); }) .then(function(o) { currentOrder = o; document.getElementById('detailTitle').textContent = 'Bitácora Orden: ' + esc(o.order_number); renderDetailBody(o); document.getElementById('detailModal').classList.add('is-open'); }) .catch(function(e) { alert('Error: ' + e.message); }); } function renderDetailBody(o) { var activeTab = document.querySelector('.so-tabs__btn.is-active'); var selectedTab = activeTab ? activeTab.dataset.tab : 'service'; var mechanicAssignHtml = isMechanic ? '
Mecánico asignado: ' + '' + '
' : '
Mecánico asignado: ' + esc(o.mechanic_name || o.employee_name || 'Sin asignar') + '
'; var html = '
' + '
Estatus: ' + esc(STATUS_LABELS[o.status] || o.status) + '
' + '
Fecha: ' + fmtDate(o.created_at) + '
' + '
Registrado por: ' + esc(o.created_by_name || '—') + '
' + '
Sucursal: ' + esc(o.branch_name || '—') + '
' + mechanicAssignHtml + '
' + (isRestricted ? '' : '
' + '
Cliente' + esc(o.customer_name || '—') + '
' + '
Taller' + esc(o.workshop_name || '—') + '
' + '
Dirección' + esc(o.customer_address || '—') + '
' + '
Teléfono' + esc(o.customer_phone || '—') + '
' + '
Vehículo' + esc(o.vehicle_description || (o.vehicle_plate ? (o.vehicle_plate + ' ' + (o.vehicle_make || '') + ' ' + (o.vehicle_model || '')).trim() : '—')) + '
' + '
Vía de entrega' + esc(DELIVERY_LABELS[o.delivery_method] || o.delivery_method || '—') + '
' + '
Motociclista' + esc(o.courier_name || '—') + '
' + '
Factura' + (o.requires_invoice ? 'Sí requiere' : 'No requiere') + '
' + '
Mecánico' + esc(o.employee_name || 'Sin asignar') + '
' + (hidePrices ? '' : '
Presupuesto' + fmtMoney(o.estimated_cost) + '
') + (hidePrices ? '' : '
Total' + fmtMoney(o.total) + '
') + '
') + (isRestricted ? '' : '
' + ' ' + ' ' + '
') + '
' + '
' + '

Notas

' + '
' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '
' + (canEdit || isMechanic ? ' ' : '') + '
' + '
' + '

Bitácora

' + renderBitacora(o.status_history || []) + '
' + '
' + (isRestricted ? '' : '
' + renderArticles(o) + '
'); document.getElementById('detailBody').innerHTML = html; // Footer actions var footer = document.getElementById('detailFooter'); var allowedNext = (VALID_NEXT[o.status] || []).filter(function(s) { return !isMechanic || ['cotizada','por_autorizar','autorizada','autorizacion_parcial','por_facturar','facturada'].indexOf(s) === -1; }); var statusHtml = canChangeStatus && allowedNext.length ? '
' + ' ' + ' ' + '
' : ''; footer.innerHTML = statusHtml + '' + (canEdit ? '' : '') + (canDelete ? '' : '') + (isRestricted ? '' : '') + (canEdit && canSell && (o.status === 'por_entregar' || o.status === 'entregado') && !o.sale_id ? '' : '') + (canEdit && canSell && !o.sale_id && o.status !== 'cancelled' ? '' : '') + (o.sale_id ? 'Ver venta #' + o.sale_id + '' : ''); } function switchTab(tab) { document.querySelectorAll('.so-tabs__btn').forEach(function(b) { b.classList.toggle('is-active', b.dataset.tab === tab); }); document.getElementById('tab-service').style.display = tab === 'service' ? 'block' : 'none'; document.getElementById('tab-articles').style.display = tab === 'articles' ? 'block' : 'none'; } function renderBitacora(history) { if (!history.length) return '

Sin movimientos

'; var vehicle = currentOrder ? (currentOrder.vehicle_description || (currentOrder.vehicle_plate ? currentOrder.vehicle_plate + ' ' + (currentOrder.vehicle_make || '') + ' ' + (currentOrder.vehicle_model || '') : null)) : null; var rows = history.map(function(h) { return '' + '' + esc(STATUS_LABELS[h.new_status] || h.new_status) + '' + '' + fmtDate(h.created_at) + '' + '' + esc(h.changed_by_name || '—') + '' + (isMechanic && vehicle ? '' + esc(vehicle) + '' : '') + '' + esc(h.notes || '—') + '' + ''; }).join(''); var vehicleTh = isMechanic && vehicle ? 'Vehículo' : ''; return '' + vehicleTh + '' + rows + '
EstatusFechaUsuarioObservaciones
'; } function mechanicName(id) { var e = employees.find(function(x) { return x.id === id; }); return e ? e.name : '—'; } function ensureEmployees() { if (employees && employees.length) return Promise.resolve(employees); return fetch('/pos/api/config/employees?per_page=500', {headers: headers()}) .then(function(r) { return r.json(); }) .then(function(d) { employees = (d.data || d.employees || []); return employees; }) .catch(function() { employees = []; return employees; }); } function populateMechanicSelect(selectId, selectedId) { var sel = document.getElementById(selectId); if (!sel) return; ensureEmployees().then(function() { sel.innerHTML = '' + employees .filter(function(e) { return e.is_active && (e.role === 'mechanic' || e.role === 'workshop'); }) .map(function(e) { return ''; }).join(''); }); } function renderArticles(o) { var colCount = (hidePrices ? 5 : 6) - (isRestricted ? 1 : 0); var partsRows = (o.items || []).map(function(it) { var priceCells = hidePrices ? '' : '' + fmtMoney(it.unit_price) + ''; var mechanicCells = isRestricted ? '' : '' + esc(mechanicName(it.mechanic_id)) + ''; var actionCell = canEdit && it.status !== 'cancelado' ? '' : ''; return '' + '' + esc(it.name) + '
' + esc(it.part_number || '') + '' + '' + fmt(it.quantity) + '' + priceCells + mechanicCells + '' + esc(ITEM_STATUS_LABELS[it.status] || it.status) + '' + '' + esc(it.observations || '') + '' + actionCell + ''; }).join(''); var partsHeader = 'ConceptoCant.' + (hidePrices ? '' : 'Precio') + (isRestricted ? '' : 'Mecánico') + 'EstadoObservaciones'; var addParts = canEdit ? '
' + '
' + ' ' + ' ' + '
' + ' ' + (hidePrices ? '' : '') + ' ' + ' ' + ' ' + ' ' + '
' : ''; var html = '
' + '

Artículos

' + ' ' + partsHeader + '' + (partsRows || '') + '
Sin artículos
' + addParts + '
'; // populate mechanic select after DOM insertion setTimeout(function() { ensureEmployees().then(function() { var sel = document.getElementById('newItemMechanic'); if (!sel || sel.dataset.populated) return; employees.forEach(function(e) { if (!e.is_active) return; var opt = document.createElement('option'); opt.value = e.id; opt.textContent = e.name; sel.appendChild(opt); }); sel.dataset.populated = '1'; }); }, 0); return html; } function closeDetailModal() { document.getElementById('detailModal').classList.remove('is-open'); currentOrderId = null; 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; document.getElementById('eoCustomerSearch').value = selectedEditCustomer ? selectedEditCustomer.name : ''; document.getElementById('eoCustomerId').value = selectedEditCustomer ? selectedEditCustomer.id : ''; // Branches var branchSel = document.getElementById('eoBranch'); branchSel.innerHTML = branches.map(function(b) { return ''; }).join(''); document.getElementById('eoWorkshopName').value = o.workshop_name || ''; document.getElementById('eoCustomerAddress').value = o.customer_address || ''; document.getElementById('eoCustomerPhone').value = o.customer_phone || ''; document.getElementById('eoVehicleDescription').value = o.vehicle_description || ''; // Delivery / courier var deliverySel = document.getElementById('eoDelivery'); deliverySel.value = o.delivery_method || ''; var courierField = document.getElementById('eoCourierField'); var courierSel = document.getElementById('eoCourier'); courierSel.innerHTML = couriers.map(function(c) { return ''; }).join(''); courierField.style.display = (o.delivery_method === 'delivery' || o.delivery_method === 'courier') ? 'block' : 'none'; document.getElementById('eoEstimatedCost').value = o.estimated_cost != null ? o.estimated_cost : ''; populateMechanicSelect('eoMechanic', o.employee_id || null); document.getElementById('eoMechanicName').value = o.mechanic_name || ''; document.getElementById('eoNotes').value = o.reception_notes || ''; document.getElementById('eoRequiresInvoice').checked = !!o.requires_invoice; } 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 = '
Sin resultados
'; box.style.display = 'block'; return; } box.innerHTML = items.map(function(c) { return '
' + '' + esc(c.name) + '' + '' + esc(c.phone || '') + ' · ' + esc(c.rfc || '') + '' + '
'; }).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() { // Fleet module removed; vehicle is free-text in the taller flow. var box = document.getElementById('eoVehicleResults'); if (box) { box.style.display = 'none'; box.innerHTML = ''; } } 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 delivery = document.getElementById('eoDelivery').value; var mechanicVal = document.getElementById('eoMechanic').value; var payload = { customer_id: customerId, branch_id: parseInt(document.getElementById('eoBranch').value, 10) || null, workshop_name: document.getElementById('eoWorkshopName').value.trim() || null, customer_address: document.getElementById('eoCustomerAddress').value.trim() || null, customer_phone: document.getElementById('eoCustomerPhone').value.trim() || null, vehicle_description: document.getElementById('eoVehicleDescription').value.trim() || null, delivery_method: delivery || null, courier_id: (delivery === 'delivery' || delivery === 'courier') ? (parseInt(document.getElementById('eoCourier').value, 10) || null) : null, estimated_cost: document.getElementById('eoEstimatedCost').value ? parseFloat(document.getElementById('eoEstimatedCost').value) : null, employee_id: mechanicVal ? parseInt(mechanicVal, 10) : null, mechanic_name: document.getElementById('eoMechanicName').value.trim() || null, reception_notes: document.getElementById('eoNotes').value, requires_invoice: document.getElementById('eoRequiresInvoice').checked }; try { await api('PUT', '/' + currentOrderId, payload); toast('Orden actualizada'); closeEditOrderModal(); closeDetailModal(); loadSummary(); loadOrders(); } catch (e) { alert('Error: ' + e.message); } } function openNewVehicleModal(context) { newVehicleContext = context || 'edit'; var customerId = null; var customerName = ''; if (newVehicleContext === 'new') { var sel = document.getElementById('noCustomer'); customerId = sel ? parseInt(sel.value, 10) || null : null; customerName = sel && sel.selectedIndex >= 0 ? sel.options[sel.selectedIndex].text : ''; } else { customerId = currentOrder ? currentOrder.customer_id : null; customerName = currentOrder ? (currentOrder.customer_name || '') : ''; } if (!customerId) { alert('Primero selecciona un cliente para poder crear el vehículo.'); return; } document.getElementById('nvCustomerId').value = customerId; document.getElementById('nvCustomerName').value = customerName; document.getElementById('newVehicleModal').classList.add('is-open'); document.getElementById('nvPlate').value = ''; document.getElementById('nvMake').value = ''; document.getElementById('nvModel').value = ''; document.getElementById('nvYear').value = ''; document.getElementById('nvColor').value = ''; } function openNewVehicleModalFromNewOrder() { openNewVehicleModal('new'); } function closeNewVehicleModal() { document.getElementById('newVehicleModal').classList.remove('is-open'); } async function saveNewVehicle() { var plate = document.getElementById('nvPlate').value.trim(); var make = document.getElementById('nvMake').value.trim(); var model = document.getElementById('nvModel').value.trim(); var customerId = parseInt(document.getElementById('nvCustomerId').value, 10) || null; if (!make || !model) { alert('Marca y modelo son obligatorios'); return; } if (!customerId) { alert('El vehículo debe estar asignado a un cliente'); return; } var payload = { plate: plate, customer_id: customerId, make: make, model: model, year: document.getElementById('nvYear').value ? parseInt(document.getElementById('nvYear').value, 10) : null, color: document.getElementById('nvColor').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', '/vehicles', payload); var label = [payload.make, payload.model, payload.year, plate].filter(Boolean).join(' · '); if (newVehicleContext === 'new') { var sel = document.getElementById('noVehicle'); var opt = document.createElement('option'); opt.value = res.id; opt.textContent = label.trim(); sel.appendChild(opt); sel.value = res.id; // also add to cached vehicles array if present if (typeof vehicles !== 'undefined') { vehicles.push({id: res.id, plate: plate, vin: vin, make: payload.make, model: payload.model, year: payload.year, color: payload.color, owner_name: payload.owner_name}); } } else { selectedEditVehicle = {id: res.id, label: label.trim()}; document.getElementById('eoVehicleSearch').value = selectedEditVehicle.label; document.getElementById('eoVehicleId').value = res.id; } toast('Vehículo creado'); closeNewVehicleModal(); } catch (e) { alert('Error: ' + e.message); } } 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; document.getElementById('noWorkshopName').value = name; document.getElementById('noCustomerPhone').value = payload.phone || ''; document.getElementById('noCustomerAddress').value = payload.address || ''; if (typeof customers !== 'undefined') { customers.push({id: res.id, name: name, phone: payload.phone, rfc: payload.rfc, address: payload.address}); } } 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() { if (!currentOrderId) return; var newStatus = document.getElementById('statusSelect').value; if (currentOrder && newStatus === currentOrder.status) { alert('Selecciona un estado diferente al actual'); return; } api('PUT', '/' + currentOrderId + '/status', {status: newStatus}) .then(function() { closeDetailModal(); loadSummary(); loadOrders(); }) .catch(function(e) { alert('Error: ' + e.message); }); } function saveNotes() { if (!currentOrderId) return; var payload; if (isMechanic) { payload = { diagnosis_notes: document.getElementById('noteDiagnosis').value, repair_notes: document.getElementById('noteRepair').value }; } else { payload = { reception_notes: document.getElementById('noteReception').value, diagnosis_notes: document.getElementById('noteDiagnosis').value, repair_notes: document.getElementById('noteRepair').value, delivery_notes: document.getElementById('noteDelivery').value }; } api('PUT', '/' + currentOrderId, payload) .then(function() { alert('Notas guardadas'); if (currentOrder) { if ('reception_notes' in payload) currentOrder.reception_notes = payload.reception_notes; if ('diagnosis_notes' in payload) currentOrder.diagnosis_notes = payload.diagnosis_notes; if ('repair_notes' in payload) currentOrder.repair_notes = payload.repair_notes; if ('delivery_notes' in payload) currentOrder.delivery_notes = payload.delivery_notes; } }) .catch(function(e) { alert('Error: ' + e.message); }); } function saveMechanicName() { if (!currentOrderId) return; var input = document.getElementById('mechanicNameInput'); if (!input) return; var payload = { mechanic_name: input.value.trim() || null }; api('PUT', '/' + currentOrderId, payload) .then(function() { alert('Mecánico asignado guardado'); if (currentOrder) currentOrder.mechanic_name = payload.mechanic_name; }) .catch(function(e) { alert('Error: ' + e.message); }); } function reserveItem(itemId) { api('POST', '/' + currentOrderId + '/items/' + itemId + '/reserve', {}) .then(function() { alert('Refacción reservada'); openDetail(currentOrderId); loadSummary(); }) .catch(function(e) { alert('Error: ' + e.message); }); } function hideItemSearchResults() { var box = document.getElementById('itemSearchResults'); if (box) box.style.display = 'none'; } function searchItemsForSO() { var input = document.getElementById('newItemSearch'); var box = document.getElementById('itemSearchResults'); var q = input.value.trim(); selectedInventoryItem = null; if (!q || q.length < 2) { box.style.display = 'none'; box.innerHTML = ''; return; } clearTimeout(itemSearchTimer); itemSearchTimer = setTimeout(function() { var branchId = currentOrder && currentOrder.branch_id ? currentOrder.branch_id : ''; fetch('/pos/api/service-orders/inventory-search?q=' + encodeURIComponent(q) + '&branch_id=' + encodeURIComponent(branchId), {headers: headers()}) .then(function(r) { return r.json(); }) .then(function(d) { var items = d.data || []; if (!items.length) { box.innerHTML = '
Sin resultados
'; box.style.display = 'block'; return; } var orderTier = currentOrder ? (currentOrder.customer_price_tier || 1) : 1; box.innerHTML = items.map(function(it) { var price = priceForTier(it.price_1, orderTier); return '
' + '
' + esc(it.name) + '
' + '' + esc(it.part_number || '') + ' · ' + esc(it.brand || '') + ' · Stock: ' + fmt(it.stock) + (hidePrices ? '' : ' · ' + fmtMoney(price)) + '' + '
'; }).join(''); box.style.display = 'block'; }) .catch(function() { box.style.display = 'none'; }); }, 250); } function selectInventoryItem(id) { var box = document.getElementById('itemSearchResults'); var div = box.querySelector('[data-id="' + id + '"]'); if (!div) return; selectedInventoryItem = { id: id, name: div.dataset.name || '', part_number: div.dataset.part || '', unit_price: parseFloat(div.dataset.price) || 0, unit_cost: parseFloat(div.dataset.cost) || 0 }; document.getElementById('newItemSearch').value = selectedInventoryItem.name; box.style.display = 'none'; } function addSelectedItem() { if (!currentOrderId) return; var qty = parseFloat(document.getElementById('newItemQty').value) || 1; var status = document.getElementById('newItemStatus').value || 'por_revisar'; var mechanicId = parseInt(document.getElementById('newItemMechanic').value, 10) || null; var observations = document.getElementById('newItemObs').value.trim(); if (selectedInventoryItem) { var price = hidePrices ? selectedInventoryItem.unit_price : (parseFloat(document.getElementById('newItemPrice').value) || selectedInventoryItem.unit_price || 0); api('POST', '/' + currentOrderId + '/items', { inventory_id: selectedInventoryItem.id, part_number: selectedInventoryItem.part_number, name: selectedInventoryItem.name, quantity: qty, unit_cost: selectedInventoryItem.unit_cost, unit_price: price, status: status, mechanic_id: mechanicId, observations: observations }).then(function() { selectedInventoryItem = null; document.getElementById('newItemSearch').value = ''; document.getElementById('newItemQty').value = '1'; if (!hidePrices) document.getElementById('newItemPrice').value = ''; document.getElementById('newItemObs').value = ''; openDetail(currentOrderId); }).catch(function(e) { alert('Error: ' + e.message); }); return; } // Fallback: manual placeholder (no inventory link) var name = document.getElementById('newItemSearch').value.trim(); if (!name) return; var manualPrice = hidePrices ? 0 : (parseFloat(document.getElementById('newItemPrice').value) || 0); api('POST', '/' + currentOrderId + '/items', { name: name, quantity: qty, unit_price: manualPrice, status: status, mechanic_id: mechanicId, observations: observations }).then(function() { document.getElementById('newItemSearch').value = ''; document.getElementById('newItemQty').value = '1'; if (!hidePrices) document.getElementById('newItemPrice').value = ''; document.getElementById('newItemObs').value = ''; openDetail(currentOrderId); }).catch(function(e) { alert('Error: ' + e.message); }); } function editItemInline(itemId) { if (!currentOrder || !currentOrder.items) return; var it = currentOrder.items.find(function(x) { return x.id === itemId; }); if (!it) return; var newStatus = prompt('Nuevo estado (' + Object.keys(ITEM_STATUS_LABELS).join(', ') + '):', it.status); if (!newStatus || !ITEM_STATUS_LABELS[newStatus]) return; var newObs = prompt('Observaciones:', it.observations || ''); var payload = {status: newStatus, observations: newObs != null ? newObs : it.observations}; api('PUT', '/items/' + itemId, payload) .then(function() { openDetail(currentOrderId); }) .catch(function(e) { alert('Error: ' + e.message); }); } function addLabor() { var desc = document.getElementById('laborDesc').value.trim(); var hours = parseFloat(document.getElementById('laborHours').value) || 0; var rate = hidePrices ? 0 : parseFloat(document.getElementById('laborRate').value) || 0; if (!desc) return alert('Escribe una descripción'); api('POST', '/' + currentOrderId + '/labor', { description: desc, hours: hours, hourly_rate: rate, status: 'pending' }).then(function() { document.getElementById('laborDesc').value = ''; document.getElementById('laborHours').value = ''; if (!hidePrices) document.getElementById('laborRate').value = ''; openDetail(currentOrderId); }).catch(function(e) { alert('Error: ' + e.message); }); } function convertToSale() { if (!currentOrderId) return; if (!confirm('¿Convertir esta orden en una venta? Se descontarán las refacciones reservadas del inventario.')) return; api('POST', '/' + currentOrderId + '/convert-to-sale', { payment_method: 'efectivo', sale_type: 'cash' }).then(function(r) { alert('Venta creada: #' + r.sale_id + ' Total: ' + fmtMoney(r.total)); closeDetailModal(); loadSummary(); loadOrders(); }).catch(function(e) { alert('Error: ' + e.message); }); } function convertToRemission() { if (!currentOrderId) return; if (!confirm('¿Generar nota de remisión desde esta orden? Se reservarán las refacciones y quedará pendiente de cobro.')) return; api('POST', '/' + currentOrderId + '/convert-to-remission', {}) .then(function(r) { alert('Nota de remisión creada: #' + r.sale_id + ' Total: ' + fmtMoney(r.total)); closeDetailModal(); loadSummary(); loadOrders(); }).catch(function(e) { alert('Error: ' + e.message); }); } function deleteOrder() { if (!currentOrderId) return; if (!confirm('¿Eliminar esta orden de servicio? Se ocultará del taller pero las reservas de inventario y la venta asociada (si existe) no se verán afectadas.')) return; api('DELETE', '/' + currentOrderId, {}) .then(function() { closeDetailModal(); loadSummary(); loadOrders(); }) .catch(function(e) { alert('Error: ' + e.message); }); } function printOrder() { if (!currentOrderId) return; if (!window.NexusPrinter || !window.NexusPrinter.isConnected()) { var connect = confirm('No hay impresora conectada. ¿Conectar ahora?'); if (connect) { window.NexusPrinter.connect().then(function(r) { if (r.ok) doPrint(); }); } return; } doPrint(); function doPrint() { window.NexusPrinter.printServiceOrder(currentOrderId, 80) .then(function(ok) { if (ok) alert('Orden enviada a la impresora'); else alert('No se pudo imprimir'); }) .catch(function(e) { alert('Error: ' + e.message); }); } } // ─── New order ─── function openNewOrderModal() { populateSelect('noBranch', branches, function(b) { return {value: b.id, text: b.name}; }); populateSelect('noCustomer', customers, function(c) { return {value: c.id, text: c.name + ' (' + (c.phone || '') + ')'}; }); populateSelect('noCourier', couriers, function(c) { return {value: c.id, text: c.name}; }); populateMechanicSelect('noMechanic', null); document.getElementById('noMechanicName').value = ''; document.getElementById('noEstimatedCost').value = ''; var noCustomer = document.getElementById('noCustomer'); noCustomer.onchange = function() { var cid = parseInt(noCustomer.value, 10); var c = customers.find(function(x) { return x.id === cid; }); if (c) { document.getElementById('noWorkshopName').value = c.name || ''; document.getElementById('noCustomerPhone').value = c.phone || ''; document.getElementById('noCustomerAddress').value = c.address || ''; } }; document.getElementById('newOrderModal').classList.add('is-open'); } function closeNewOrderModal() { document.getElementById('newOrderModal').classList.remove('is-open'); document.getElementById('newOrderForm').reset(); var cf = document.getElementById('courierField'); if (cf) cf.style.display = 'none'; } function submitNewOrder() { var customerId = document.getElementById('noCustomer').value; var branchId = document.getElementById('noBranch').value; if (!customerId) return alert('Selecciona un cliente'); if (!branchId) return alert('Selecciona una sucursal'); var delivery = document.getElementById('noDelivery').value; var mechanicVal = document.getElementById('noMechanic').value; var estimatedCostVal = document.getElementById('noEstimatedCost').value; var payload = { branch_id: parseInt(branchId, 10), customer_id: parseInt(customerId, 10), workshop_name: document.getElementById('noWorkshopName').value.trim() || null, customer_address: document.getElementById('noCustomerAddress').value.trim() || null, customer_phone: document.getElementById('noCustomerPhone').value.trim() || null, vehicle_description: document.getElementById('noVehicleDescription').value.trim() || null, reception_notes: document.getElementById('noNotes').value, delivery_method: delivery || null, courier_id: (delivery === 'delivery' || delivery === 'courier') ? (parseInt(document.getElementById('noCourier').value, 10) || null) : null, employee_id: mechanicVal ? parseInt(mechanicVal, 10) : null, mechanic_name: document.getElementById('noMechanicName').value.trim() || null, estimated_cost: estimatedCostVal ? parseFloat(estimatedCostVal) : null, is_direct: document.getElementById('noDirect').checked, requires_invoice: document.getElementById('noRequiresInvoice').checked }; api('POST', '', payload).then(function() { closeNewOrderModal(); loadSummary(); loadOrders(); }).catch(function(e) { alert('Error: ' + e.message); }); } // ─── Catalog ─── function openCatalogModal() { document.getElementById('catalogModal').classList.add('is-open'); renderCatalog(); } function closeCatalogModal() { document.getElementById('catalogModal').classList.remove('is-open'); } function loadCatalog() { fetch(API + '/service-catalog?active_only=true', {headers: headers()}) .then(function(r) { return r.json(); }) .then(function(d) { catalog = d.data || []; }) .catch(function() {}); } function renderCatalog() { var body = document.getElementById('catalogBody'); if (!catalog.length) { body.innerHTML = 'Sin conceptos'; return; } body.innerHTML = catalog.map(function(c) { return '' + '' + esc(c.name) + (c.description ? '
' + esc(c.description) + '' : '') + '' + '' + fmt(c.suggested_hours) + '' + (hidePrices ? '' : '' + fmtMoney(c.suggested_rate) + '' + '' + fmtMoney(c.suggested_hours * c.suggested_rate) + '') + '' + ''; }).join(''); } function addCatalogItem() { var name = document.getElementById('catName').value.trim(); if (!name) return alert('Escribe un nombre'); api('POST', '/service-catalog', { name: name, description: document.getElementById('catDesc').value, suggested_hours: parseFloat(document.getElementById('catHours').value) || 0, suggested_rate: parseFloat(document.getElementById('catRate').value) || 0 }).then(function() { document.getElementById('catName').value = ''; document.getElementById('catDesc').value = ''; document.getElementById('catHours').value = ''; document.getElementById('catRate').value = ''; loadCatalog(); setTimeout(renderCatalog, 200); }).catch(function(e) { alert('Error: ' + e.message); }); } function deleteCatalogItem(id) { if (!confirm('¿Desactivar este concepto?')) return; api('DELETE', '/service-catalog/' + id, {}) .then(function() { loadCatalog(); setTimeout(renderCatalog, 200); }) .catch(function(e) { alert('Error: ' + e.message); }); } // ─── Reference data ─── function loadReferenceData() { // Customers fetch('/pos/api/customers?per_page=500', {headers: headers()}) .then(function(r) { return r.json(); }) .then(function(d) { customers = (d.data || d.customers || []); }) .catch(function() {}); // Employees fetch('/pos/api/config/employees?per_page=500', {headers: headers()}) .then(function(r) { return r.json(); }) .then(function(d) { employees = (d.data || d.employees || []); }) .catch(function() { employees = []; }); // Couriers fetch('/pos/api/logistics/couriers?per_page=500', {headers: headers()}) .then(function(r) { return r.json(); }) .then(function(d) { couriers = (d.data || d.couriers || []); populateSelect('noCourier', couriers, function(c) { return {value: c.id, text: c.name}; }); }) .catch(function() { couriers = []; }); // Branches fetch('/pos/api/config/branches', {headers: headers()}) .then(function(r) { return r.json(); }) .then(function(d) { branches = (d.data || []); populateBranchFilter(); var noBranch = document.getElementById('noBranch'); var userBranch = (window.POS_USER || {}).branch_id; if (noBranch && userBranch && branches.some(function(b) { return b.id == userBranch; })) { noBranch.value = userBranch; } }) .catch(function() { branches = []; }); } function populateBranchFilter() { var sel = document.getElementById('filterBranch'); if (!sel) return; var current = sel.value; sel.innerHTML = ''; branches.forEach(function(b) { var opt = document.createElement('option'); opt.value = b.id; opt.textContent = b.name; sel.appendChild(opt); }); sel.value = current; } function populateSelect(id, items, mapper) { var sel = document.getElementById(id); if (!sel) return; sel.innerHTML = id === 'noCustomer' || id === 'noCourier' ? '' : ''; items.forEach(function(it) { var opt = mapper(it); var el = document.createElement('option'); el.value = opt.value; el.textContent = opt.text; sel.appendChild(el); }); } // ─── Public API ─── return { init: init, setView: setView, goPage: goPage, openDetail: openDetail, closeDetailModal: closeDetailModal, switchTab: switchTab, changeStatus: changeStatus, reserveItem: reserveItem, saveNotes: saveNotes, saveMechanicName: saveMechanicName, searchItemsForSO: searchItemsForSO, selectInventoryItem: selectInventoryItem, addSelectedItem: addSelectedItem, editItemInline: editItemInline, addLabor: addLabor, convertToSale: convertToSale, convertToRemission: convertToRemission, deleteOrder: deleteOrder, printOrder: printOrder, openNewOrderModal: openNewOrderModal, closeNewOrderModal: closeNewOrderModal, submitNewOrder: submitNewOrder, openCatalogModal: openCatalogModal, closeCatalogModal: closeCatalogModal, addCatalogItem: addCatalogItem, deleteCatalogItem: deleteCatalogItem, openEditOrderModal: openEditOrderModal, closeEditOrderModal: closeEditOrderModal, saveOrderChanges: saveOrderChanges, selectEditCustomer: selectEditCustomer, selectEditVehicle: selectEditVehicle, searchCustomersForSO: searchCustomersForSO, searchVehiclesForSO: searchVehiclesForSO, openNewVehicleModal: openNewVehicleModal, openNewVehicleModalFromNewOrder: openNewVehicleModalFromNewOrder, closeNewVehicleModal: closeNewVehicleModal, saveNewVehicle: saveNewVehicle, openNewCustomerModal: openNewCustomerModal, openNewCustomerModalFromNewOrder: openNewCustomerModalFromNewOrder, closeNewCustomerModal: closeNewCustomerModal, saveNewCustomer: saveNewCustomer, }; })(); document.addEventListener('DOMContentLoaded', Workshop.init);