feat: cashier/counter reports, service-order & remission flows, Rached migration utils
- Add "Mis cortes de caja" report for cashiers/counters with sales detail. - Cash register history scoped to own cuts for non-admin roles; new /register/<id>/sales endpoint. - Remove dashboard from cashier menu; add Reports to cashier/counter. - Service orders: assign mechanic, budget field, invoice flag, counter/cashier can add items/remissions, convert to remission. - Remission notes module (UI, CSS, courier, counter remissions). - Customer hard-delete and vehicle/customer linkage in workshop. - POS: always show search results, compact payment grid, credit validation, tier pricing (5%/10%), ticket with customer/folio. - Inventory: CSV template with sku_secondary, alias import. - Rached migration scripts and DB migrations. - Version-bump cached JS/CSS query strings. Excludes local Rached session tokens/captures (rached_*.json / rached_*.txt).
This commit is contained in:
@@ -30,47 +30,99 @@ var Workshop = (function() {
|
||||
|
||||
var user = window.POS_USER || {};
|
||||
var role = (user.role || '').toLowerCase();
|
||||
var hidePrices = role === 'workshop' || role === 'mechanic';
|
||||
var isRestricted = role === 'workshop' || role === 'mechanic';
|
||||
var isMechanic = role === 'mechanic';
|
||||
var hidePrices = isRestricted;
|
||||
var perms = user.permissions || [];
|
||||
var canEdit = role === 'owner' || role === 'admin' || perms.indexOf('workshop.edit') !== -1;
|
||||
// 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' || perms.indexOf('pos.sell') !== -1;
|
||||
var canSell = role === 'owner' || role === 'admin' || role === 'counter' || role === 'cashier' || perms.indexOf('pos.sell') !== -1;
|
||||
var canChangeStatus = canEdit || isMechanic;
|
||||
|
||||
var COLUMNS = [
|
||||
{key: 'received', label: 'Recibido'},
|
||||
{key: 'diagnosis', label: 'Diagnóstico'},
|
||||
{key: 'waiting_parts', label: 'Espera refacciones'},
|
||||
{key: 'repair', label: 'En reparación'},
|
||||
{key: 'ready', label: 'Listo'},
|
||||
{key: 'delivered', label: 'Entregado'},
|
||||
{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 = {
|
||||
received: 'Recibido',
|
||||
diagnosis: 'Diagnóstico',
|
||||
waiting_parts: 'Espera refacciones',
|
||||
repair: 'En reparación',
|
||||
ready: 'Listo',
|
||||
delivered: 'Entregado',
|
||||
cancelled: 'Cancelado'
|
||||
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 = {
|
||||
received: ['diagnosis', 'cancelled'],
|
||||
diagnosis: ['waiting_parts', 'repair', 'cancelled'],
|
||||
waiting_parts: ['repair', 'cancelled'],
|
||||
repair: ['ready', 'cancelled'],
|
||||
ready: ['delivered', 'cancelled'],
|
||||
delivered: [],
|
||||
cancelled: []
|
||||
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: 'Pasa cliente',
|
||||
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',
|
||||
@@ -88,6 +140,16 @@ var Workshop = (function() {
|
||||
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);
|
||||
@@ -126,6 +188,15 @@ var Workshop = (function() {
|
||||
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';
|
||||
}
|
||||
if (hidePrices) {
|
||||
var btnCatalog = document.getElementById('btnCatalog');
|
||||
if (btnCatalog) btnCatalog.style.display = 'none';
|
||||
@@ -151,11 +222,19 @@ var Workshop = (function() {
|
||||
|
||||
function bindDeliverySelect() {
|
||||
var sel = document.getElementById('noDelivery');
|
||||
if (!sel) return;
|
||||
sel.addEventListener('change', function() {
|
||||
var cf = document.getElementById('courierField');
|
||||
if (cf) cf.style.display = sel.value === 'courier' ? 'block' : 'none';
|
||||
});
|
||||
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) {
|
||||
@@ -172,9 +251,9 @@ var Workshop = (function() {
|
||||
fetch(API + '/kanban/summary', {headers: headers()})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
document.getElementById('statReceived').textContent = fmt(d.received || 0);
|
||||
document.getElementById('statRepair').textContent = fmt((d.repair || 0) + (d.diagnosis || 0) + (d.waiting_parts || 0) + (d.quality_check || 0));
|
||||
document.getElementById('statReady').textContent = fmt(d.ready || 0);
|
||||
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() {});
|
||||
@@ -243,20 +322,33 @@ var Workshop = (function() {
|
||||
|
||||
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="7" style="text-align:center;padding:var(--space-4);">No se encontraron órdenes</td></tr>';
|
||||
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 vehicle = esc((o.vehicle_plate || '—') + ' ' + (o.vehicle_make || '') + ' ' + (o.vehicle_model || ''));
|
||||
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>' +
|
||||
'<td><span class="badge badge--' + esc(o.status) + '">' + esc(STATUS_LABELS[o.status] || o.status) + '</span></td>' +
|
||||
statusCell +
|
||||
'<td class="price-col" style="text-align:right;">' + fmtMoney(o.total) + '</td>' +
|
||||
'<td><button class="btn btn--sm btn--secondary" onclick="Workshop.openDetail(' + o.id + ')">Ver</button></td>' +
|
||||
actionCell +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
if (hidePrices) {
|
||||
@@ -287,7 +379,8 @@ var Workshop = (function() {
|
||||
function renderKanban() {
|
||||
var board = document.getElementById('kanbanBoard');
|
||||
board.innerHTML = '';
|
||||
COLUMNS.forEach(function(col) {
|
||||
var hiddenCols = isMechanic ? ['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';
|
||||
@@ -322,13 +415,22 @@ var Workshop = (function() {
|
||||
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.priority) + '">' + esc(priorityLabel(o.priority)) + '</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_plate || 'Sin vehículo') + '</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 +
|
||||
@@ -355,47 +457,56 @@ var Workshop = (function() {
|
||||
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_plate || '—') + ' ' + (o.vehicle_make || '') + ' ' + (o.vehicle_model || '')) + '</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>' +
|
||||
' <div class="so-detail__field"><span class="so-detail__label">Entrega estimada</span><span class="so-detail__value">' + fmtDate(o.estimated_completion) + '</span></div>' +
|
||||
' <div class="so-detail__field"><span class="so-detail__label">Kilometraje entrada</span><span class="so-detail__value">' + fmt(o.mileage_in) + '</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>' +
|
||||
'</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>') +
|
||||
|
||||
'<div class="so-tab-panel" id="tab-service" ' + (selectedTab === 'service' ? '' : 'style="display:none;"') + '>' +
|
||||
'<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 ? '' : 'readonly') + '>' + esc(o.diagnosis_notes || '') + '</textarea>' +
|
||||
' <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 ? '' : 'readonly') + '>' + esc(o.repair_notes || '') + '</textarea>' +
|
||||
' <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 ? ' <button class="btn btn--secondary" style="margin-top:var(--space-2);" onclick="Workshop.saveNotes()">Guardar notas</button>' : '') +
|
||||
(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>' +
|
||||
@@ -403,16 +514,18 @@ var Workshop = (function() {
|
||||
' </div>' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="so-tab-panel" id="tab-articles" ' + (selectedTab === 'articles' ? '' : 'style="display:none;"') + '>' +
|
||||
(isRestricted ? '' : '<div class="so-tab-panel" id="tab-articles" ' + (selectedTab === 'articles' ? '' : 'style="display:none;"') + '>' +
|
||||
renderArticles(o) +
|
||||
'</div>';
|
||||
'</div>');
|
||||
|
||||
document.getElementById('detailBody').innerHTML = html;
|
||||
|
||||
// Footer actions
|
||||
var footer = document.getElementById('detailFooter');
|
||||
var allowedNext = VALID_NEXT[o.status] || [];
|
||||
var statusHtml = canEdit && allowedNext.length ?
|
||||
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>' +
|
||||
@@ -424,10 +537,11 @@ var Workshop = (function() {
|
||||
'<button class="btn btn--ghost" onclick="Workshop.closeDetailModal()">Cerrar</button>' +
|
||||
(canEdit ? '<button class="btn btn--secondary" onclick="Workshop.openEditOrderModal()">Editar orden</button>' : '') +
|
||||
(canDelete ? '<button class="btn btn--danger" onclick="Workshop.deleteOrder()">Eliminar orden</button>' : '') +
|
||||
'<button class="btn btn--secondary" onclick="Workshop.printOrder()">' +
|
||||
(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 === 'ready' && !o.sale_id ? '<button class="btn btn--primary" onclick="Workshop.convertToSale()">Convertir a venta</button>' : '') +
|
||||
'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>' : '');
|
||||
}
|
||||
|
||||
@@ -439,93 +553,105 @@ var Workshop = (function() {
|
||||
|
||||
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('');
|
||||
return '<table class="data-table bitacora-table"><thead><tr><th>Estatus</th><th>Fecha</th><th>Usuario</th><th>Observaciones</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
||||
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 actionCell = canEdit && it.status !== 'cancelled' ?
|
||||
'<td><button class="btn btn--sm btn--secondary" onclick="event.stopPropagation();Workshop.reserveItem(' + it.id + ')">Reservar</button></td>' : '<td></td>';
|
||||
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 +
|
||||
'<td><span class="badge ' + statusBadgeClass(it.status) + '">' + esc(STATUS_LABELS[it.status] || it.status) + '</span></td>' +
|
||||
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>') + '<th>Estado</th><th></th></tr>';
|
||||
|
||||
var laborRows = (o.labor || []).map(function(l) {
|
||||
var priceCells = hidePrices ? '' : '<td>' + fmtMoney(l.hourly_rate) + '</td><td>' + fmtMoney(l.total_cost) + '</td>';
|
||||
return '<tr>' +
|
||||
'<td>' + esc(l.description) + '</td>' +
|
||||
'<td>' + fmt(l.hours) + '</td>' +
|
||||
priceCells +
|
||||
'<td><span class="badge ' + statusBadgeClass(l.status) + '">' + esc(STATUS_LABELS[l.status] || l.status) + '</span></td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
var laborHeader = '<tr><th>Concepto</th><th>Horas</th>' + (hidePrices ? '' : '<th>Precio/hr</th><th>Total</th>') + '<th>Estado</th></tr>';
|
||||
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:200px;">' +
|
||||
' <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:80px;" />' +
|
||||
' <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 addLabor = canEdit ?
|
||||
'<div style="margin-top:var(--space-3);display:flex;gap:var(--space-2);flex-wrap:wrap;">' +
|
||||
' <select class="form-input" id="laborCatalogSelect"><option value="">Concepto manual</option></select>' +
|
||||
' <input class="form-input" id="laborDesc" placeholder="Descripción" style="flex:1;min-width:160px;" />' +
|
||||
' <input class="form-input" id="laborHours" type="number" step="0.1" placeholder="Hrs" style="width:80px;" />' +
|
||||
(hidePrices ? '' : '<input class="form-input" id="laborRate" type="number" step="0.01" placeholder="$/hr" style="width:100px;" />') +
|
||||
' <button class="btn btn--secondary" onclick="Workshop.addLabor()">Agregar</button>' +
|
||||
'</div>' : '';
|
||||
|
||||
var html =
|
||||
'<div class="so-detail__section">' +
|
||||
' <h3>Refacciones</h3>' +
|
||||
' <table class="data-table"><thead>' + partsHeader + '</thead><tbody>' + (partsRows || '<tr><td colspan="' + (hidePrices ? 4 : 5) + '" style="text-align:center;">Sin refacciones</td></tr>') + '</tbody></table>' +
|
||||
' <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>' +
|
||||
'<div class="so-detail__section">' +
|
||||
' <h3>Mano de obra</h3>' +
|
||||
' <table class="data-table"><thead>' + laborHeader + '</thead><tbody>' + (laborRows || '<tr><td colspan="' + (hidePrices ? 3 : 5) + '" style="text-align:center;">Sin mano de obra</td></tr>') + '</tbody></table>' +
|
||||
addLabor +
|
||||
'</div>';
|
||||
|
||||
// schedule labor catalog select population after DOM insertion
|
||||
// populate mechanic select after DOM insertion
|
||||
setTimeout(function() {
|
||||
var sel = document.getElementById('laborCatalogSelect');
|
||||
if (!sel || sel.dataset.populated) return;
|
||||
catalog.forEach(function(c) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = JSON.stringify(c);
|
||||
opt.textContent = c.name + (hidePrices ? '' : ' ($' + fmtMoney(c.suggested_hours * c.suggested_rate).replace('$', '') + ')');
|
||||
sel.appendChild(opt);
|
||||
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';
|
||||
});
|
||||
sel.dataset.populated = '1';
|
||||
sel.onchange = function() {
|
||||
if (!sel.value) return;
|
||||
var c = JSON.parse(sel.value);
|
||||
document.getElementById('laborDesc').value = c.name;
|
||||
document.getElementById('laborHours').value = c.suggested_hours;
|
||||
if (!hidePrices) document.getElementById('laborRate').value = c.suggested_rate;
|
||||
};
|
||||
}, 0);
|
||||
|
||||
return html;
|
||||
@@ -559,12 +685,9 @@ var Workshop = (function() {
|
||||
async function populateEditOrderModal() {
|
||||
var o = currentOrder;
|
||||
selectedEditCustomer = o.customer_id ? {id: o.customer_id, name: o.customer_name || ''} : null;
|
||||
selectedEditVehicle = o.vehicle_id ? {id: o.vehicle_id, label: (o.vehicle_plate || '') + ' ' + (o.vehicle_make || '') + ' ' + (o.vehicle_model || '')} : null;
|
||||
|
||||
document.getElementById('eoCustomerSearch').value = selectedEditCustomer ? selectedEditCustomer.name : '';
|
||||
document.getElementById('eoCustomerId').value = selectedEditCustomer ? selectedEditCustomer.id : '';
|
||||
document.getElementById('eoVehicleSearch').value = selectedEditVehicle ? selectedEditVehicle.label.trim() : '';
|
||||
document.getElementById('eoVehicleId').value = selectedEditVehicle ? selectedEditVehicle.id : '';
|
||||
|
||||
// Branches
|
||||
var branchSel = document.getElementById('eoBranch');
|
||||
@@ -572,25 +695,26 @@ var Workshop = (function() {
|
||||
return '<option value="' + b.id + '"' + (b.id === o.branch_id ? ' selected' : '') + '>' + esc(b.name) + '</option>';
|
||||
}).join('');
|
||||
|
||||
// Mechanics/employees
|
||||
var mechSel = document.getElementById('eoMechanic');
|
||||
mechSel.innerHTML = '<option value="">— Ninguno —</option>';
|
||||
try {
|
||||
var res = await fetch('/pos/api/config/employees', {headers: headers()});
|
||||
var json = await res.json();
|
||||
(json.data || []).forEach(function(e) {
|
||||
if (!e.is_active) return;
|
||||
mechSel.innerHTML += '<option value="' + e.id + '"' + (e.id === o.employee_id ? ' selected' : '') + '>' + esc(e.name) + '</option>';
|
||||
});
|
||||
} catch (e) {}
|
||||
document.getElementById('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('eoPriority').value = o.priority || 'normal';
|
||||
document.getElementById('eoFuelLevel').value = o.fuel_level || '';
|
||||
document.getElementById('eoMileageIn').value = o.mileage_in != null ? o.mileage_in : '';
|
||||
document.getElementById('eoMileageOut').value = o.mileage_out != null ? o.mileage_out : '';
|
||||
document.getElementById('eoEstimatedCompletion').value = toDatetimeLocal(o.estimated_completion);
|
||||
document.getElementById('eoEstimatedCost').value = o.estimated_cost != null ? o.estimated_cost : '';
|
||||
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) {
|
||||
@@ -638,36 +762,9 @@ var Workshop = (function() {
|
||||
}
|
||||
|
||||
function searchVehiclesForSO() {
|
||||
var input = document.getElementById('eoVehicleSearch');
|
||||
// Fleet module removed; vehicle is free-text in the taller flow.
|
||||
var box = document.getElementById('eoVehicleResults');
|
||||
var q = input.value.trim();
|
||||
selectedEditVehicle = null;
|
||||
document.getElementById('eoVehicleId').value = '';
|
||||
if (!q || q.length < 2) {
|
||||
box.style.display = 'none'; box.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
clearTimeout(vehicleSearchTimer);
|
||||
vehicleSearchTimer = setTimeout(function() {
|
||||
fetch('/pos/api/fleet/vehicles?q=' + encodeURIComponent(q) + '&per_page=20&active_only=true', {headers: headers()})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
var items = d.data || [];
|
||||
if (!items.length) {
|
||||
box.innerHTML = '<div style="padding:var(--space-2);color:var(--color-text-muted);">Sin resultados</div>';
|
||||
box.style.display = 'block'; return;
|
||||
}
|
||||
box.innerHTML = items.map(function(v) {
|
||||
var label = (v.plate || '') + ' · ' + (v.make || '') + ' ' + (v.model || '');
|
||||
return '<div class="so-search-result" style="padding:var(--space-2);cursor:pointer;border-bottom:1px solid var(--color-border);" onclick="Workshop.selectEditVehicle(' + v.id + ', \'' + escJs(label) + '\')">' +
|
||||
'<strong>' + esc(v.plate || '') + '</strong>' +
|
||||
'<small>' + esc(v.make || '') + ' ' + esc(v.model || '') + ' · ' + esc(v.owner_name || '') + '</small>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
box.style.display = 'block';
|
||||
})
|
||||
.catch(function() { box.style.display = 'none'; });
|
||||
}, 250);
|
||||
if (box) { box.style.display = 'none'; box.innerHTML = ''; }
|
||||
}
|
||||
|
||||
function selectEditVehicle(id, label) {
|
||||
@@ -680,19 +777,22 @@ var Workshop = (function() {
|
||||
async function saveOrderChanges() {
|
||||
if (!currentOrderId) return;
|
||||
var customerId = selectedEditCustomer ? selectedEditCustomer.id : parseInt(document.getElementById('eoCustomerId').value, 10) || null;
|
||||
var vehicleId = selectedEditVehicle ? selectedEditVehicle.id : parseInt(document.getElementById('eoVehicleId').value, 10) || null;
|
||||
var delivery = document.getElementById('eoDelivery').value;
|
||||
var mechanicVal = document.getElementById('eoMechanic').value;
|
||||
var payload = {
|
||||
customer_id: customerId,
|
||||
vehicle_id: vehicleId,
|
||||
branch_id: parseInt(document.getElementById('eoBranch').value, 10) || null,
|
||||
employee_id: parseInt(document.getElementById('eoMechanic').value, 10) || null,
|
||||
priority: document.getElementById('eoPriority').value,
|
||||
fuel_level: document.getElementById('eoFuelLevel').value || null,
|
||||
mileage_in: document.getElementById('eoMileageIn').value ? parseInt(document.getElementById('eoMileageIn').value, 10) : null,
|
||||
mileage_out: document.getElementById('eoMileageOut').value ? parseInt(document.getElementById('eoMileageOut').value, 10) : null,
|
||||
estimated_completion: document.getElementById('eoEstimatedCompletion').value || null,
|
||||
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,
|
||||
reception_notes: document.getElementById('eoNotes').value
|
||||
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);
|
||||
@@ -832,8 +932,11 @@ var Workshop = (function() {
|
||||
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});
|
||||
customers.push({id: res.id, name: name, phone: payload.phone, rfc: payload.rfc, address: payload.address});
|
||||
}
|
||||
} else {
|
||||
selectedEditCustomer = {id: res.id, name: name};
|
||||
@@ -867,25 +970,46 @@ var Workshop = (function() {
|
||||
|
||||
function saveNotes() {
|
||||
if (!currentOrderId) return;
|
||||
var 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
|
||||
};
|
||||
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) {
|
||||
currentOrder.reception_notes = payload.reception_notes;
|
||||
currentOrder.diagnosis_notes = payload.diagnosis_notes;
|
||||
currentOrder.repair_notes = payload.repair_notes;
|
||||
currentOrder.delivery_notes = payload.delivery_notes;
|
||||
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() {
|
||||
@@ -923,10 +1047,12 @@ var Workshop = (function() {
|
||||
box.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
var orderTier = currentOrder ? (currentOrder.customer_price_tier || 1) : 1;
|
||||
box.innerHTML = items.map(function(it) {
|
||||
return '<div class="so-search-result" data-id="' + it.id + '" data-name="' + esc(it.name) + '" data-part="' + esc(it.part_number || '') + '" data-price="' + (it.price_1 || 0) + '" data-cost="' + (it.cost || 0) + '" style="padding:var(--space-2);cursor:pointer;border-bottom:1px solid var(--color-border);" onclick="Workshop.selectInventoryItem(' + it.id + ')">' +
|
||||
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(it.price_1)) + '</small>' +
|
||||
'<small>' + esc(it.part_number || '') + ' · ' + esc(it.brand || '') + ' · Stock: ' + fmt(it.stock) + (hidePrices ? '' : ' · ' + fmtMoney(price)) + '</small>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
box.style.display = 'block';
|
||||
@@ -954,20 +1080,28 @@ var Workshop = (function() {
|
||||
|
||||
function addSelectedItem() {
|
||||
if (!currentOrderId) return;
|
||||
var qty = parseInt(document.getElementById('newItemQty').value, 10) || 1;
|
||||
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: selectedInventoryItem.unit_price,
|
||||
status: 'pending'
|
||||
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;
|
||||
@@ -975,19 +1109,36 @@ var Workshop = (function() {
|
||||
// Fallback: manual placeholder (no inventory link)
|
||||
var name = document.getElementById('newItemSearch').value.trim();
|
||||
if (!name) return;
|
||||
if (!confirm('No seleccionaste una refacción del inventario. ¿Agregar como concepto manual? No podrá reservarse.')) return;
|
||||
var manualPrice = hidePrices ? 0 : (parseFloat(document.getElementById('newItemPrice').value) || 0);
|
||||
api('POST', '/' + currentOrderId + '/items', {
|
||||
name: name,
|
||||
quantity: qty,
|
||||
unit_price: 0,
|
||||
status: 'pending'
|
||||
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;
|
||||
@@ -1020,6 +1171,18 @@ var Workshop = (function() {
|
||||
}).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;
|
||||
@@ -1058,10 +1221,22 @@ var Workshop = (function() {
|
||||
// ─── 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('noVehicle', vehicles, function(v) { return {value: v.id, text: v.plate + ' ' + v.make + ' ' + v.model}; });
|
||||
populateSelect('noMechanic', employees, function(e) { return {value: e.id, text: e.name}; });
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -1074,19 +1249,27 @@ var Workshop = (function() {
|
||||
|
||||
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),
|
||||
vehicle_id: parseInt(document.getElementById('noVehicle').value, 10) || null,
|
||||
employee_id: parseInt(document.getElementById('noMechanic').value, 10) || null,
|
||||
priority: document.getElementById('noPriority').value,
|
||||
estimated_completion: document.getElementById('noEstimatedCompletion').value || null,
|
||||
mileage_in: parseInt(document.getElementById('noMileage').value, 10) || null,
|
||||
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 === 'courier' ? (parseInt(document.getElementById('noCourier').value, 10) || null) : null,
|
||||
is_direct: document.getElementById('noDirect').checked
|
||||
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();
|
||||
@@ -1168,18 +1351,13 @@ var Workshop = (function() {
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) { customers = (d.data || d.customers || []); })
|
||||
.catch(function() {});
|
||||
// Vehicles
|
||||
fetch('/pos/api/fleet/vehicles?per_page=500', {headers: headers()})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) { vehicles = (d.data || []); })
|
||||
.catch(function() { vehicles = []; });
|
||||
// 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/couriers?per_page=500', {headers: headers()})
|
||||
fetch('/pos/api/logistics/couriers?per_page=500', {headers: headers()})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
couriers = (d.data || d.couriers || []);
|
||||
@@ -1192,6 +1370,11 @@ var Workshop = (function() {
|
||||
.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 = []; });
|
||||
}
|
||||
@@ -1235,11 +1418,14 @@ var Workshop = (function() {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user