feat(workshop): add vehicle from service order
- Added POST /pos/api/service-orders/vehicles endpoint for workshop.edit users. - Added 'Nuevo vehiculo' modal in edit order with plate, VIN, make, model, year, color and owner. - After creation the vehicle is automatically selected in the order.
This commit is contained in:
@@ -374,6 +374,39 @@ def assign_mechanic_endpoint(so_id):
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@service_order_bp.route('/vehicles', methods=['POST'])
|
||||||
|
@require_auth('workshop.edit')
|
||||||
|
def create_vehicle_for_workshop():
|
||||||
|
"""Create a fleet vehicle directly from the workshop flow."""
|
||||||
|
data = request.get_json() or {}
|
||||||
|
if not data.get('plate') and not data.get('vin'):
|
||||||
|
return jsonify({'error': 'Se requiere placa o VIN'}), 400
|
||||||
|
|
||||||
|
conn = get_tenant_conn(g.tenant_id)
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO fleet_vehicles
|
||||||
|
(branch_id, plate, vin, make, model, year,
|
||||||
|
current_mileage, fuel_type, color, owner_name, notes)
|
||||||
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||||
|
RETURNING id
|
||||||
|
""", (
|
||||||
|
data.get('branch_id', g.branch_id), data.get('plate'), data.get('vin'),
|
||||||
|
data.get('make'), data.get('model'), data.get('year'),
|
||||||
|
data.get('current_mileage', 0), data.get('fuel_type', 'gasolina'),
|
||||||
|
data.get('color'), data.get('owner_name'), data.get('notes'),
|
||||||
|
))
|
||||||
|
vehicle_id = cur.fetchone()[0]
|
||||||
|
conn.commit()
|
||||||
|
cur.close(); conn.close()
|
||||||
|
return jsonify({'id': vehicle_id, 'message': 'Vehiculo creado'}), 201
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
cur.close(); conn.close()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@service_order_bp.route('/inventory-search', methods=['GET'])
|
@service_order_bp.route('/inventory-search', methods=['GET'])
|
||||||
@require_auth('workshop.view')
|
@require_auth('workshop.view')
|
||||||
def inventory_search():
|
def inventory_search():
|
||||||
|
|||||||
@@ -704,6 +704,51 @@ var Workshop = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openNewVehicleModal() {
|
||||||
|
document.getElementById('newVehicleModal').classList.add('is-open');
|
||||||
|
document.getElementById('nvPlate').value = '';
|
||||||
|
document.getElementById('nvVIN').value = '';
|
||||||
|
document.getElementById('nvMake').value = '';
|
||||||
|
document.getElementById('nvModel').value = '';
|
||||||
|
document.getElementById('nvYear').value = '';
|
||||||
|
document.getElementById('nvColor').value = '';
|
||||||
|
document.getElementById('nvOwner').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeNewVehicleModal() {
|
||||||
|
document.getElementById('newVehicleModal').classList.remove('is-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveNewVehicle() {
|
||||||
|
var plate = document.getElementById('nvPlate').value.trim();
|
||||||
|
var vin = document.getElementById('nvVIN').value.trim();
|
||||||
|
if (!plate && !vin) {
|
||||||
|
alert('Se requiere al menos placa o VIN');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var payload = {
|
||||||
|
plate: plate,
|
||||||
|
vin: vin,
|
||||||
|
make: document.getElementById('nvMake').value.trim() || null,
|
||||||
|
model: document.getElementById('nvModel').value.trim() || null,
|
||||||
|
year: document.getElementById('nvYear').value ? parseInt(document.getElementById('nvYear').value, 10) : null,
|
||||||
|
color: document.getElementById('nvColor').value.trim() || null,
|
||||||
|
owner_name: document.getElementById('nvOwner').value.trim() || null,
|
||||||
|
branch_id: currentOrder ? currentOrder.branch_id : null
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
var res = await api('POST', '/vehicles', payload);
|
||||||
|
var label = plate + (payload.make ? ' · ' + payload.make : '') + (payload.model ? ' ' + payload.model : '');
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Actions ───
|
// ─── Actions ───
|
||||||
|
|
||||||
function changeStatus() {
|
function changeStatus() {
|
||||||
@@ -1113,6 +1158,9 @@ var Workshop = (function() {
|
|||||||
selectEditVehicle: selectEditVehicle,
|
selectEditVehicle: selectEditVehicle,
|
||||||
searchCustomersForSO: searchCustomersForSO,
|
searchCustomersForSO: searchCustomersForSO,
|
||||||
searchVehiclesForSO: searchVehiclesForSO,
|
searchVehiclesForSO: searchVehiclesForSO,
|
||||||
|
openNewVehicleModal: openNewVehicleModal,
|
||||||
|
closeNewVehicleModal: closeNewVehicleModal,
|
||||||
|
saveNewVehicle: saveNewVehicle,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<meta name="theme-color" content="#F5A623" />
|
<meta name="theme-color" content="#F5A623" />
|
||||||
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
|
<link rel="shortcut icon" type="image/png" href="/pos/static/pwa/icon-192.png" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="/pos/static/css/workshop.css?v=36">
|
<link rel="stylesheet" href="/pos/static/css/workshop.css?v=37">
|
||||||
<style>
|
<style>
|
||||||
.so-notes-grid { display: grid; grid-template-columns: 120px 1fr; gap: var(--space-2); align-items: start; }
|
.so-notes-grid { display: grid; grid-template-columns: 120px 1fr; gap: var(--space-2); align-items: start; }
|
||||||
.so-notes-grid .form-label { margin: 0; padding-top: var(--space-2); }
|
.so-notes-grid .form-label { margin: 0; padding-top: var(--space-2); }
|
||||||
@@ -261,8 +261,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-field form-field--span2" style="position:relative;">
|
<div class="form-field form-field--span2" style="position:relative;">
|
||||||
<label class="form-label" for="eoVehicleSearch">Vehículo</label>
|
<label class="form-label" for="eoVehicleSearch">Vehículo</label>
|
||||||
<input class="form-input" id="eoVehicleSearch" autocomplete="off" placeholder="Buscar por placa..." oninput="Workshop.searchVehiclesForSO()" />
|
<div style="display:flex;gap:var(--space-2);">
|
||||||
<div id="eoVehicleResults" style="display:none;position:absolute;z-index:10;top:100%;left:0;right:0;max-height:180px;overflow-y:auto;background:#fff;border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:0 4px 12px rgba(0,0,0,.15);"></div>
|
<div style="position:relative;flex:1;">
|
||||||
|
<input class="form-input" id="eoVehicleSearch" autocomplete="off" placeholder="Buscar por placa..." oninput="Workshop.searchVehiclesForSO()" />
|
||||||
|
<div id="eoVehicleResults" style="display:none;position:absolute;z-index:10;top:100%;left:0;right:0;max-height:180px;overflow-y:auto;background:#fff;border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:0 4px 12px rgba(0,0,0,.15);"></div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn--secondary" type="button" onclick="Workshop.openNewVehicleModal()">+ Nuevo</button>
|
||||||
|
</div>
|
||||||
<input type="hidden" id="eoVehicleId" />
|
<input type="hidden" id="eoVehicleId" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
@@ -322,6 +327,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- New vehicle modal -->
|
||||||
|
<div class="modal-overlay" id="newVehicleModal">
|
||||||
|
<div class="modal modal--sm">
|
||||||
|
<div class="modal__header">
|
||||||
|
<h2 class="modal__title">Nuevo vehículo</h2>
|
||||||
|
<button class="modal__close" onclick="Workshop.closeNewVehicleModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal__body">
|
||||||
|
<form class="form-grid" id="newVehicleForm" onsubmit="return false;">
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="form-label" for="nvPlate">Placa *</label>
|
||||||
|
<input class="form-input" id="nvPlate" placeholder="Ej. ABC-123" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="form-label" for="nvVIN">VIN</label>
|
||||||
|
<input class="form-input" id="nvVIN" placeholder="Número de serie" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="form-label" for="nvMake">Marca</label>
|
||||||
|
<input class="form-input" id="nvMake" placeholder="Ej. Nissan" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="form-label" for="nvModel">Modelo</label>
|
||||||
|
<input class="form-input" id="nvModel" placeholder="Ej. Versa" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="form-label" for="nvYear">Año</label>
|
||||||
|
<input class="form-input" id="nvYear" type="number" placeholder="2020" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field">
|
||||||
|
<label class="form-label" for="nvColor">Color</label>
|
||||||
|
<input class="form-input" id="nvColor" placeholder="Ej. Blanco" />
|
||||||
|
</div>
|
||||||
|
<div class="form-field form-field--span2">
|
||||||
|
<label class="form-label" for="nvOwner">Dueño</label>
|
||||||
|
<input class="form-input" id="nvOwner" placeholder="Nombre del dueño" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal__footer">
|
||||||
|
<button class="btn btn--ghost" onclick="Workshop.closeNewVehicleModal()">Cancelar</button>
|
||||||
|
<button class="btn btn--primary" onclick="Workshop.saveNewVehicle()">Guardar vehículo</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Catalog modal -->
|
<!-- Catalog modal -->
|
||||||
<div class="modal-overlay" id="catalogModal">
|
<div class="modal-overlay" id="catalogModal">
|
||||||
<div class="modal modal--lg">
|
<div class="modal modal--lg">
|
||||||
@@ -378,7 +429,7 @@
|
|||||||
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
|
<script src="/pos/static/js/pos-utils.js?v=33" defer></script>
|
||||||
<script src="/pos/static/js/sidebar.js?v=36" defer></script>
|
<script src="/pos/static/js/sidebar.js?v=36" defer></script>
|
||||||
<script src="/pos/static/js/offline-banner.js" defer></script>
|
<script src="/pos/static/js/offline-banner.js" defer></script>
|
||||||
<script src="/pos/static/js/workshop.js?v=36" defer></script>
|
<script src="/pos/static/js/workshop.js?v=37" defer></script>
|
||||||
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
|
<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/pos/sw.js',{scope:'/pos/'});}</script>
|
||||||
<script src="/pos/static/js/pwa-install.js" defer></script>
|
<script src="/pos/static/js/pwa-install.js" defer></script>
|
||||||
<script src="/pos/static/js/chat.js" defer></script>
|
<script src="/pos/static/js/chat.js" defer></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user