feat: contrato soporta múltiples servicios (many2many) con precio sugerido
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
<thead class="text-left border-b border-gray-200 bg-gray-50">
|
||||
<tr>
|
||||
<th class="py-2 px-3">Cliente</th>
|
||||
<th class="py-2 px-3">Servicio</th>
|
||||
<th class="py-2 px-3">Servicios</th>
|
||||
<th class="py-2 px-3">Vencimiento</th>
|
||||
<th class="py-2 px-3">Días</th>
|
||||
<th class="py-2 px-3">Precio</th>
|
||||
@@ -43,14 +43,19 @@
|
||||
<div class="font-medium" x-text="d.cliente?.nombre||'—'"></div>
|
||||
<div class="text-xs text-gray-400" x-text="d.cliente?.empresa||''"></div>
|
||||
</td>
|
||||
<td class="py-2 px-3" x-text="d.servicio?.nombre||'—'"></td>
|
||||
<td class="py-2 px-3">
|
||||
<template x-for="s in (d.servicios||[])" :key="s.ID">
|
||||
<span class="inline-block text-xs bg-gray-100 rounded px-1.5 py-0.5 mr-1 mb-0.5" x-text="s.nombre"></span>
|
||||
</template>
|
||||
<span x-show="!d.servicios||d.servicios.length===0" class="text-gray-400">—</span>
|
||||
</td>
|
||||
<td class="py-2 px-3" x-text="fmtDate(d.fecha_vencimiento)"></td>
|
||||
<td class="py-2 px-3">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-semibold"
|
||||
:class="d.urgencia==='rojo' ? 'bg-red-100 text-red-700' : d.urgencia==='amarillo' ? 'bg-yellow-100 text-yellow-700' : 'bg-green-100 text-green-700'"
|
||||
x-text="d.dias_restantes <= 0 ? 'Vencido' : d.dias_restantes+' días'"></span>
|
||||
</td>
|
||||
<td class="py-2 px-3" x-text="(d.servicio?.moneda||'') +' '+ Number(d.precio_acordado).toFixed(2)"></td>
|
||||
<td class="py-2 px-3" x-text="Number(d.precio_acordado).toFixed(2)"></td>
|
||||
<td class="py-2 px-3">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium"
|
||||
:class="{
|
||||
@@ -111,14 +116,20 @@
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-span-2" x-show="!editModal">
|
||||
<label class="text-xs font-medium text-gray-600">Servicio *</label>
|
||||
<select x-model="form.servicio_id" required class="mt-1 w-full border rounded px-3 py-2 text-sm">
|
||||
<option value="">Seleccionar...</option>
|
||||
<div class="col-span-2">
|
||||
<label class="text-xs font-medium text-gray-600">Servicios <span class="font-normal text-gray-400">(uno o varios)</span></label>
|
||||
<div class="mt-1 border rounded max-h-36 overflow-y-auto divide-y text-sm">
|
||||
<template x-for="s in servicios" :key="s.ID">
|
||||
<option :value="s.ID" x-text="s.nombre"></option>
|
||||
<label class="flex items-center gap-2 px-3 py-1.5 hover:bg-gray-50 cursor-pointer">
|
||||
<input type="checkbox" :value="s.ID"
|
||||
:checked="form.servicio_ids.includes(s.ID)"
|
||||
@change="toggleServicio(s.ID)" />
|
||||
<span class="flex-1" x-text="s.nombre"></span>
|
||||
<span class="text-xs text-gray-400" x-text="s.moneda+' '+Number(s.precio).toFixed(2)"></span>
|
||||
</label>
|
||||
</template>
|
||||
</select>
|
||||
<div x-show="servicios.length===0" class="text-xs text-gray-400 px-3 py-2">Sin servicios disponibles</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-600">Fecha inicio *</label>
|
||||
@@ -131,6 +142,9 @@
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-600">Precio acordado</label>
|
||||
<input x-model="form.precio_acordado" type="number" step="0.01" class="mt-1 w-full border rounded px-3 py-2 text-sm" />
|
||||
<p class="text-xs text-gray-400 mt-0.5" x-show="precioSugerido > 0">
|
||||
Sugerido: <span class="font-medium text-gray-600" x-text="precioSugerido.toFixed(2)"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div x-show="editModal">
|
||||
<label class="text-xs font-medium text-gray-600">Estado</label>
|
||||
@@ -188,7 +202,7 @@ document.addEventListener('alpine:init', () => {
|
||||
addModal: false, editModal: false, deleteModal: false,
|
||||
clientes: [], servicios: [],
|
||||
selectedId: null,
|
||||
form: { cliente_id:'', servicio_id:'', fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, estado:'activo', auto_renovar:false, notas:'' },
|
||||
form: { cliente_id:'', servicio_ids:[], fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, estado:'activo', auto_renovar:false, notas:'' },
|
||||
toast: { show: false, msg: '', type: 'ok' },
|
||||
|
||||
async init() {
|
||||
@@ -205,16 +219,37 @@ document.addEventListener('alpine:init', () => {
|
||||
},
|
||||
|
||||
async loadSelects() {
|
||||
const [c, s] = await Promise.all([
|
||||
axios.get('/app/api/clientes/select'),
|
||||
axios.get('/app/api/servicios/select'),
|
||||
]);
|
||||
this.clientes = c.data.registros || [];
|
||||
this.servicios = s.data.registros || [];
|
||||
try {
|
||||
const [c, s] = await Promise.all([
|
||||
axios.get('/app/api/clientes/select'),
|
||||
axios.get('/app/api/servicios/select'),
|
||||
]);
|
||||
this.clientes = Array.isArray(c.data) ? c.data : (c.data.registros || []);
|
||||
this.servicios = Array.isArray(s.data) ? s.data : (s.data.registros || []);
|
||||
} catch(e) {
|
||||
this.showToast('Error cargando listas: ' + (e.response?.data?.error || e.message), 'error');
|
||||
}
|
||||
},
|
||||
|
||||
get precioSugerido() {
|
||||
return this.servicios
|
||||
.filter(s => (this.form.servicio_ids || []).includes(s.ID))
|
||||
.reduce((acc, s) => acc + (parseFloat(s.precio) || 0), 0);
|
||||
},
|
||||
toggleServicio(id) {
|
||||
const idx = (this.form.servicio_ids || []).indexOf(id);
|
||||
if (idx === -1) {
|
||||
this.form.servicio_ids.push(id);
|
||||
} else {
|
||||
this.form.servicio_ids.splice(idx, 1);
|
||||
}
|
||||
this.form.precio_acordado = this.precioSugerido;
|
||||
},
|
||||
|
||||
openEdit(d) {
|
||||
this.form = {
|
||||
cliente_id: d.cliente_id,
|
||||
servicio_ids: (d.servicios || []).map(s => s.ID),
|
||||
fecha_inicio: d.fecha_inicio ? d.fecha_inicio.substring(0,10) : '',
|
||||
fecha_vencimiento: d.fecha_vencimiento ? d.fecha_vencimiento.substring(0,10) : '',
|
||||
precio_acordado: d.precio_acordado,
|
||||
@@ -229,7 +264,7 @@ document.addEventListener('alpine:init', () => {
|
||||
closeModals() {
|
||||
this.addModal = this.editModal = this.deleteModal = false;
|
||||
this.selectedId = null;
|
||||
this.form = { cliente_id:'', servicio_id:'', fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, estado:'activo', auto_renovar:false, notas:'' };
|
||||
this.form = { cliente_id:'', servicio_ids:[], fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, estado:'activo', auto_renovar:false, notas:'' };
|
||||
},
|
||||
|
||||
async save() {
|
||||
|
||||
Reference in New Issue
Block a user