Files
Autoparts-DB/pos/static/js/workshop.js
consultoria-as 670db75fcd
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled
fix(workshop): show autorizada for workshop/mechanic and hide delivery statuses
2026-07-02 14:45:25 +00:00

1492 lines
72 KiB
JavaScript

/**
* 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';
// Remove statuses that workshop/mechanic cannot see from the status filter.
var statusSel = document.getElementById('filterStatus');
if (statusSel) {
var hiddenStatuses = isMechanic
? ['cotizada','por_autorizar','autorizacion_parcial','por_facturar','facturada','por_entregar','entregado','por_enviar','enviado','por_recolectar']
: ['por_entregar','entregado','por_enviar','enviado','por_recolectar'];
Array.from(statusSel.options).forEach(function(opt) {
if (opt.value && hiddenStatuses.indexOf(opt.value) !== -1) opt.remove();
});
statusSel.value = '';
}
}
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: hide delivery/logistics card; show work-in-progress summary.
cards.forEach(function(card, idx) {
card.style.display = (idx === 2) ? 'none' : ''; // hide "Por entregar"
});
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) +
(d.autorizada || 0) + (d.autorizacion_parcial || 0)
);
document.getElementById('statReady').textContent = '0';
document.getElementById('statOverdue').textContent = fmt(d.overdue || 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 = '<tr><td colspan="7" style="text-align:center;padding:var(--space-4);">Error cargando órdenes</td></tr>';
});
}
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 = '<tr><td colspan="' + cols + '" style="text-align:center;padding:var(--space-4);">No se encontraron órdenes</td></tr>';
return;
}
body.innerHTML = orders.map(function(o) {
var statusCell = '<td><span class="badge badge--' + esc(o.status) + '">' + esc(STATUS_LABELS[o.status] || o.status) + '</span></td>';
var actionCell = '<td><button class="btn btn--sm btn--secondary" onclick="Workshop.openDetail(' + o.id + ')">Ver</button></td>';
var base = '<td>' + esc(o.branch_name || '—') + '</td>' +
'<td><strong>' + esc(o.order_number) + '</strong></td>' +
statusCell +
actionCell;
if (isRestricted) {
return '<tr>' + base + '</tr>';
}
var vehicle = esc(o.vehicle_description || (o.vehicle_plate ? (o.vehicle_plate + ' ' + (o.vehicle_make || '') + ' ' + (o.vehicle_model || '')).trim() : '—'));
return '<tr>' +
'<td>' + esc(o.branch_name || '—') + '</td>' +
'<td><strong>' + esc(o.order_number) + '</strong></td>' +
'<td>' + esc(o.customer_name || 'Cliente general') + '</td>' +
'<td>' + esc(o.workshop_name || '—') + '</td>' +
'<td>' + vehicle + '</td>' +
statusCell +
'<td class="price-col" style="text-align:right;">' + fmtMoney(o.total) + '</td>' +
actionCell +
'</tr>';
}).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 = '<button class="btn btn--sm btn--ghost" ' + (currentPage === 1 ? 'disabled' : '') + ' onclick="Workshop.goPage(' + (currentPage - 1) + ')">Anterior</button>' +
'<span class="pagination-info">Página ' + currentPage + ' de ' + totalPages + '</span>' +
'<button class="btn btn--sm btn--ghost" ' + (currentPage === totalPages ? 'disabled' : '') + ' onclick="Workshop.goPage(' + (currentPage + 1) + ')">Siguiente</button>';
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 (isMechanic) {
// Mechanic sees everything except commercial and delivery/logistics statuses.
hiddenCols = ['cotizada','por_autorizar','autorizacion_parcial','por_facturar','facturada','por_entregar','entregado','por_enviar','enviado','por_recolectar'];
} else if (role === 'workshop') {
// Workshop sees everything except delivery/logistics statuses.
hiddenCols = ['por_entregar','entregado','por_enviar','enviado','por_recolectar'];
}
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 =
'<div class="kanban-column__header">' +
' <span>' + esc(col.label) + '</span>' +
' <span class="kanban-column__count">' + colOrders.length + '</span>' +
'</div>' +
'<div class="kanban-column__body" id="col-' + col.key + '"></div>';
board.appendChild(colEl);
var body = colEl.querySelector('.kanban-column__body');
if (!colOrders.length) {
body.innerHTML = '<div class="empty-state" style="padding:var(--space-4);"><div class="empty-state__subtitle">Sin órdenes</div></div>';
} 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 ? '' : '<span>' + fmtMoney(o.estimated_cost || o.total) + '</span>';
if (isRestricted) {
card.innerHTML =
'<div class="kanban-card__header">' +
' <span class="kanban-card__id">' + esc(o.order_number) + '</span>' +
' <span class="kanban-card__priority badge badge--' + esc(o.status) + '">' + esc(STATUS_LABELS[o.status] || o.status) + '</span>' +
'</div>' +
'<div class="kanban-card__branch">' + esc(o.branch_name || '—') + '</div>';
return card;
}
card.innerHTML =
'<div class="kanban-card__header">' +
' <span class="kanban-card__id">' + esc(o.order_number) + '</span>' +
' <span class="kanban-card__priority badge badge--' + esc(o.status) + '">' + esc(STATUS_LABELS[o.status] || o.status) + '</span>' +
'</div>' +
'<div class="kanban-card__customer">' + esc(o.customer_name || 'Cliente general') + '</div>' +
'<div class="kanban-card__vehicle">' + esc(o.vehicle_description || o.vehicle_plate || 'Sin vehículo') + '</div>' +
'<div class="kanban-card__meta">' +
' <span class="kanban-card__mechanic">🔧 ' + esc(o.employee_name || 'Sin asignar') + '</span>' +
priceHtml +
'</div>';
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 ?
' <div class="so-detail-header__row"><span class="so-detail__label">Mecánico asignado:</span> ' +
'<input id="mechanicNameInput" class="form-input" style="width:auto;min-width:180px;" value="' + esc(o.mechanic_name || '') + '" placeholder="Nombre del mecánico" />' +
' <button class="btn btn--sm btn--secondary" onclick="Workshop.saveMechanicName()">Guardar</button></div>' :
' <div class="so-detail-header__row"><span class="so-detail__label">Mecánico asignado:</span> ' + esc(o.mechanic_name || o.employee_name || 'Sin asignar') + '</div>';
var html =
'<div class="so-detail-header">' +
' <div class="so-detail-header__row"><span class="so-detail__label">Estatus:</span> <span class="badge badge--' + esc(o.status) + '">' + esc(STATUS_LABELS[o.status] || o.status) + '</span></div>' +
' <div class="so-detail-header__row"><span class="so-detail__label">Fecha:</span> ' + fmtDate(o.created_at) + '</div>' +
' <div class="so-detail-header__row"><span class="so-detail__label">Registrado por:</span> ' + esc(o.created_by_name || '—') + '</div>' +
' <div class="so-detail-header__row"><span class="so-detail__label">Sucursal:</span> ' + esc(o.branch_name || '—') + '</div>' +
mechanicAssignHtml +
'</div>' +
(isRestricted ? '' :
'<div class="so-detail-info">' +
' <div class="so-detail__field"><span class="so-detail__label">Cliente</span><span class="so-detail__value">' + esc(o.customer_name || '—') + '</span></div>' +
' <div class="so-detail__field"><span class="so-detail__label">Taller</span><span class="so-detail__value">' + esc(o.workshop_name || '—') + '</span></div>' +
' <div class="so-detail__field"><span class="so-detail__label">Dirección</span><span class="so-detail__value">' + esc(o.customer_address || '—') + '</span></div>' +
' <div class="so-detail__field"><span class="so-detail__label">Teléfono</span><span class="so-detail__value">' + esc(o.customer_phone || '—') + '</span></div>' +
' <div class="so-detail__field"><span class="so-detail__label">Vehículo</span><span class="so-detail__value">' + esc(o.vehicle_description || (o.vehicle_plate ? (o.vehicle_plate + ' ' + (o.vehicle_make || '') + ' ' + (o.vehicle_model || '')).trim() : '—')) + '</span></div>' +
' <div class="so-detail__field"><span class="so-detail__label">Vía de entrega</span><span class="so-detail__value">' + esc(DELIVERY_LABELS[o.delivery_method] || o.delivery_method || '—') + '</span></div>' +
' <div class="so-detail__field"><span class="so-detail__label">Motociclista</span><span class="so-detail__value">' + esc(o.courier_name || '—') + '</span></div>' +
' <div class="so-detail__field"><span class="so-detail__label">Factura</span><span class="so-detail__value">' + (o.requires_invoice ? 'Sí requiere' : 'No requiere') + '</span></div>' +
' <div class="so-detail__field"><span class="so-detail__label">Mecánico</span><span class="so-detail__value">' + esc(o.employee_name || 'Sin asignar') + '</span></div>' +
(hidePrices ? '' : '<div class="so-detail__field"><span class="so-detail__label">Presupuesto</span><span class="so-detail__value">' + fmtMoney(o.estimated_cost) + '</span></div>') +
(hidePrices ? '' : '<div class="so-detail__field"><span class="so-detail__label">Total</span><span class="so-detail__value">' + fmtMoney(o.total) + '</span></div>') +
'</div>') +
(isRestricted ? '' :
'<div class="so-tabs">' +
' <button class="so-tabs__btn ' + (selectedTab === 'service' ? 'is-active' : '') + '" data-tab="service" onclick="Workshop.switchTab(\'service\')">Orden de servicio</button>' +
' <button class="so-tabs__btn ' + (selectedTab === 'articles' ? 'is-active' : '') + '" data-tab="articles" onclick="Workshop.switchTab(\'articles\')">Artículos</button>' +
'</div>') +
'<div class="so-tab-panel" id="tab-service" ' + (selectedTab === 'service' || isRestricted ? '' : 'style="display:none;"') + '>' +
' <div class="so-detail__section">' +
' <h3>Notas</h3>' +
' <div class="so-notes-grid">' +
' <label class="form-label">Recepción</label>' +
' <textarea id="noteReception" class="form-input" rows="2" ' + (canEdit ? '' : 'readonly') + '>' + esc(o.reception_notes || '') + '</textarea>' +
' <label class="form-label">Diagnóstico</label>' +
' <textarea id="noteDiagnosis" class="form-input" rows="2" ' + (canEdit || isMechanic ? '' : 'readonly') + '>' + esc(o.diagnosis_notes || '') + '</textarea>' +
' <label class="form-label">Reparación</label>' +
' <textarea id="noteRepair" class="form-input" rows="2" ' + (canEdit || isMechanic ? '' : 'readonly') + '>' + esc(o.repair_notes || '') + '</textarea>' +
' <label class="form-label">Entrega</label>' +
' <textarea id="noteDelivery" class="form-input" rows="2" ' + (canEdit ? '' : 'readonly') + '>' + esc(o.delivery_notes || '') + '</textarea>' +
' </div>' +
(canEdit || isMechanic ? ' <button class="btn btn--secondary" style="margin-top:var(--space-2);" onclick="Workshop.saveNotes()">Guardar notas</button>' : '') +
' </div>' +
' <div class="so-detail__section">' +
' <h3>Bitácora</h3>' +
renderBitacora(o.status_history || []) +
' </div>' +
'</div>' +
(isRestricted ? '' : '<div class="so-tab-panel" id="tab-articles" ' + (selectedTab === 'articles' ? '' : 'style="display:none;"') + '>' +
renderArticles(o) +
'</div>');
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 ?
'<div class="so-detail__actions" style="margin-right:auto;">' +
' <select class="form-input" id="statusSelect" style="width:auto;">' +
'<option value="' + esc(o.status) + '" selected>' + esc(STATUS_LABELS[o.status] || o.status) + '</option>' +
allowedNext.map(function(s) { return '<option value="' + s + '">' + esc(STATUS_LABELS[s] || s) + '</option>'; }).join('') +
' </select>' +
' <button class="btn btn--primary" onclick="Workshop.changeStatus()">Actualizar estado</button>' +
'</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>' : '') +
(isRestricted ? '' : '<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>' +
'Imprimir orden</button>') +
(canEdit && canSell && (o.status === 'por_entregar' || o.status === 'entregado') && !o.sale_id ? '<button class="btn btn--primary" onclick="Workshop.convertToSale()">Convertir a venta</button>' : '') +
(canEdit && canSell && !o.sale_id && o.status !== 'cancelled' ? '<button class="btn btn--secondary" onclick="Workshop.convertToRemission()">Generar nota de remisión</button>' : '') +
(o.sale_id ? '<a class="btn btn--secondary" href="/pos/invoicing?sale_id=' + o.sale_id + '">Ver venta #' + o.sale_id + '</a>' : '');
}
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 '<p style="color:var(--color-text-muted);">Sin movimientos</p>';
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 '<tr>' +
'<td><span class="badge badge--' + esc(h.new_status) + '">' + esc(STATUS_LABELS[h.new_status] || h.new_status) + '</span></td>' +
'<td>' + fmtDate(h.created_at) + '</td>' +
'<td>' + esc(h.changed_by_name || '—') + '</td>' +
(isMechanic && vehicle ? '<td>' + esc(vehicle) + '</td>' : '') +
'<td>' + esc(h.notes || '—') + '</td>' +
'</tr>';
}).join('');
var vehicleTh = isMechanic && vehicle ? '<th>Vehículo</th>' : '';
return '<table class="data-table bitacora-table"><thead><tr><th>Estatus</th><th>Fecha</th><th>Usuario</th>' + vehicleTh + '<th>Observaciones</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
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 = '<option value="">— Sin asignar —</option>' +
employees
.filter(function(e) { return e.is_active && (e.role === 'mechanic' || e.role === 'workshop'); })
.map(function(e) {
return '<option value="' + e.id + '"' + (e.id === selectedId ? ' selected' : '') + '>' + esc(e.name) + '</option>';
}).join('');
});
}
function renderArticles(o) {
var colCount = (hidePrices ? 5 : 6) - (isRestricted ? 1 : 0);
var partsRows = (o.items || []).map(function(it) {
var priceCells = hidePrices ? '' : '<td>' + fmtMoney(it.unit_price) + '</td>';
var mechanicCells = isRestricted ? '' : '<td>' + esc(mechanicName(it.mechanic_id)) + '</td>';
var actionCell = canEdit && it.status !== 'cancelado' ?
'<td><button class="btn btn--sm btn--secondary" onclick="event.stopPropagation();Workshop.editItemInline(' + it.id + ')">Editar</button></td>' : '<td></td>';
return '<tr>' +
'<td>' + esc(it.name) + '<br><small>' + esc(it.part_number || '') + '</small></td>' +
'<td>' + fmt(it.quantity) + '</td>' +
priceCells +
mechanicCells +
'<td><span class="badge ' + statusBadgeClass(it.status) + '">' + esc(ITEM_STATUS_LABELS[it.status] || it.status) + '</span></td>' +
'<td>' + esc(it.observations || '') + '</td>' +
actionCell +
'</tr>';
}).join('');
var partsHeader = '<tr><th>Concepto</th><th>Cant.</th>' + (hidePrices ? '' : '<th>Precio</th>') + (isRestricted ? '' : '<th>Mecánico</th>') + '<th>Estado</th><th>Observaciones</th><th></th></tr>';
var addParts = canEdit ?
'<div style="margin-top:var(--space-3);display:flex;gap:var(--space-2);align-items:flex-start;flex-wrap:wrap;">' +
' <div style="position:relative;flex:1;min-width:160px;">' +
' <input class="form-input" id="newItemSearch" placeholder="Buscar refacción por nombre/número" autocomplete="off" oninput="Workshop.searchItemsForSO()" />' +
' <div id="itemSearchResults" style="display:none;position:absolute;z-index:10;top:100%;left:0;right:0;max-height:220px;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>' +
' </div>' +
' <input class="form-input" id="newItemQty" type="number" value="1" min="1" style="width:70px;" />' +
(hidePrices ? '' : '<input class="form-input" id="newItemPrice" type="number" step="0.01" placeholder="Precio" style="width:90px;" />') +
' <select class="form-input" id="newItemMechanic" style="width:auto;"><option value="">— Mecánico —</option></select>' +
' <select class="form-input" id="newItemStatus" style="width:auto;">' +
Object.keys(ITEM_STATUS_LABELS).map(function(s) { return '<option value="' + s + '">' + esc(ITEM_STATUS_LABELS[s]) + '</option>'; }).join('') +
' </select>' +
' <input class="form-input" id="newItemObs" placeholder="Observaciones" style="min-width:140px;flex:1;" />' +
' <button class="btn btn--secondary" onclick="Workshop.addSelectedItem()">Agregar</button>' +
'</div>' : '';
var html =
'<div class="so-detail__section">' +
' <h3>Artículos</h3>' +
' <table class="data-table"><thead>' + partsHeader + '</thead><tbody>' + (partsRows || '<tr><td colspan="' + colCount + '" style="text-align:center;">Sin artículos</td></tr>') + '</tbody></table>' +
addParts +
'</div>';
// 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 '<option value="' + b.id + '"' + (b.id === o.branch_id ? ' selected' : '') + '>' + esc(b.name) + '</option>';
}).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 '<option value="' + c.id + '"' + (c.id === o.courier_id ? ' selected' : '') + '>' + esc(c.name) + '</option>';
}).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 = '<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() {
// 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 = '<div style="padding:var(--space-2);color:var(--color-text-muted);">Sin resultados</div>';
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 '<div class="so-search-result" data-id="' + it.id + '" data-name="' + esc(it.name) + '" data-part="' + esc(it.part_number || '') + '" data-price="' + price + '" data-cost="' + (it.cost || 0) + '" style="padding:var(--space-2);cursor:pointer;border-bottom:1px solid var(--color-border);" onclick="Workshop.selectInventoryItem(' + it.id + ')">' +
'<div><strong>' + esc(it.name) + '</strong></div>' +
'<small>' + esc(it.part_number || '') + ' · ' + esc(it.brand || '') + ' · Stock: ' + fmt(it.stock) + (hidePrices ? '' : ' · ' + fmtMoney(price)) + '</small>' +
'</div>';
}).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 = '<tr><td colspan="' + (hidePrices ? 3 : 5) + '" style="text-align:center;">Sin conceptos</td></tr>';
return;
}
body.innerHTML = catalog.map(function(c) {
return '<tr>' +
'<td>' + esc(c.name) + (c.description ? '<br><small>' + esc(c.description) + '</small>' : '') + '</td>' +
'<td>' + fmt(c.suggested_hours) + '</td>' +
(hidePrices ? '' : '<td class="price-col">' + fmtMoney(c.suggested_rate) + '</td>' +
'<td class="price-col">' + fmtMoney(c.suggested_hours * c.suggested_rate) + '</td>') +
'<td><button class="btn btn--sm btn--ghost" onclick="Workshop.deleteCatalogItem(' + c.id + ')">Desactivar</button></td>' +
'</tr>';
}).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 = '<option value="">Todas las sucursales</option>';
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' ? '' : '<option value="">—</option>';
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);