355 lines
9.7 KiB
PHP
355 lines
9.7 KiB
PHP
@extends('layouts.app')
|
||
|
||
@section('content')
|
||
<section class="content py-3">
|
||
<div class="container-fluid">
|
||
|
||
<form method="POST" action="{{ route('cotizaciones.store') }}">
|
||
@csrf
|
||
|
||
<div class="row g-3">
|
||
|
||
{{-- ===================== PRODUCTOS ===================== --}}
|
||
<div class="col-lg-8">
|
||
|
||
<div class="card border-0 shadow-sm h-100">
|
||
<div class="card-body d-flex flex-column">
|
||
|
||
<h6 class="text-muted mb-2">Productos</h6>
|
||
|
||
<input type="text"
|
||
id="search"
|
||
class="form-control mb-3"
|
||
placeholder="Buscar producto por nombre...">
|
||
|
||
<div class="products-scroll">
|
||
@foreach($productos as $producto)
|
||
<div class="product-card"
|
||
data-id="{{ $producto->id }}"
|
||
data-name="{{ $producto->nombre }}"
|
||
data-price="{{ $producto->precio }}">
|
||
|
||
<div class="product-inner text-center">
|
||
|
||
@if($producto->foto)
|
||
<img src="{{ asset('storage/'.$producto->foto) }}"
|
||
class="product-img">
|
||
@else
|
||
<div class="product-img-placeholder">
|
||
<i class="bi bi-box"></i>
|
||
</div>
|
||
@endif
|
||
|
||
<div class="product-name">
|
||
{{ $producto->nombre }}
|
||
</div>
|
||
|
||
<div class="product-price">
|
||
${{ number_format($producto->precio,2) }}
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
{{-- ===================== TOTALES ===================== --}}
|
||
<div class="col-lg-4">
|
||
|
||
<div class="card border-0 shadow-sm total-card">
|
||
<div class="card-body">
|
||
|
||
<h6 class="text-muted mb-3">Resumen</h6>
|
||
|
||
<div class="total-row">
|
||
<span>Subtotal</span>
|
||
<span id="subtotal">$0.00</span>
|
||
</div>
|
||
|
||
<div class="total-row text-danger">
|
||
<span>Descuento</span>
|
||
<span id="total-desc">$0.00</span>
|
||
</div>
|
||
|
||
<div class="total-row">
|
||
<span>IVA (16%)</span>
|
||
<span id="iva">$0.00</span>
|
||
</div>
|
||
|
||
<hr>
|
||
|
||
<div class="total-final">
|
||
<span>Total</span>
|
||
<span id="total">$0.00</span>
|
||
</div>
|
||
|
||
<div>
|
||
<div class="mb-2">
|
||
|
||
<select name="cliente_id" class="form-control select2" required>
|
||
<option value="">Seleccionar cliente</option>
|
||
@foreach($clientes as $cliente)
|
||
<option value="{{ $cliente->id }}">
|
||
{{ $cliente->nombre ?? $cliente->name }}
|
||
</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
|
||
<button class="btn btn-success btn-sm w-100">
|
||
Guardar Cotización
|
||
</button>
|
||
</div>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
{{-- ===================== TABLA ===================== --}}
|
||
<div class="card border-0 shadow-sm mt-3">
|
||
<div class="card-header bg-dark text-white py-2">
|
||
Productos seleccionados
|
||
</div>
|
||
|
||
<div class="table-responsive">
|
||
<table class="table align-middle mb-0">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>Producto</th>
|
||
<th width="170">Cantidad</th>
|
||
<th width="120">Precio</th>
|
||
<th width="170">Desc %</th>
|
||
<th width="120">Total</th>
|
||
<th width="60"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="cart-body"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
</form>
|
||
</div>
|
||
</section>
|
||
@endsection
|
||
|
||
|
||
@section('scripts')
|
||
|
||
<style>
|
||
|
||
/* ========= PRODUCTOS ========= */
|
||
|
||
.products-scroll{
|
||
display:flex;
|
||
gap:15px;
|
||
overflow-x:auto;
|
||
padding-bottom:5px;
|
||
}
|
||
|
||
.product-card{
|
||
min-width:160px;
|
||
cursor:pointer;
|
||
}
|
||
|
||
.product-inner{
|
||
background:#fff;
|
||
border-radius:10px;
|
||
padding:15px;
|
||
border:1px solid #eee;
|
||
transition:.2s;
|
||
}
|
||
|
||
.product-inner:hover{
|
||
transform:translateY(-3px);
|
||
box-shadow:0 6px 14px rgba(0,0,0,.08);
|
||
border-color:#0d6efd;
|
||
}
|
||
|
||
.product-img{
|
||
width:60px;
|
||
height:60px;
|
||
object-fit:cover;
|
||
border-radius:6px;
|
||
margin-bottom:6px;
|
||
}
|
||
|
||
.product-img-placeholder{
|
||
width:60px;
|
||
height:60px;
|
||
background:#f5f5f5;
|
||
border-radius:6px;
|
||
display:flex;
|
||
align-items:center;
|
||
justify-content:center;
|
||
margin:auto;
|
||
margin-bottom:6px;
|
||
}
|
||
|
||
.product-name{
|
||
font-size:14px;
|
||
font-weight:600;
|
||
min-height:34px;
|
||
}
|
||
|
||
.product-price{
|
||
color:#198754;
|
||
font-weight:700;
|
||
}
|
||
|
||
/* ========= TOTALES ========= */
|
||
|
||
.total-card{
|
||
position:sticky;
|
||
top:15px;
|
||
}
|
||
|
||
.total-row{
|
||
display:flex;
|
||
justify-content:space-between;
|
||
margin-bottom:6px;
|
||
font-size:14px;
|
||
}
|
||
|
||
.total-final{
|
||
display:flex;
|
||
justify-content:space-between;
|
||
font-size:22px;
|
||
font-weight:700;
|
||
color:#198754;
|
||
}
|
||
|
||
/* ========= TABLA ========= */
|
||
|
||
.table td{
|
||
vertical-align:middle;
|
||
}
|
||
|
||
</style>
|
||
|
||
|
||
<script>
|
||
const cart = {};
|
||
const IVA = 0.16;
|
||
const tbody = document.getElementById('cart-body');
|
||
|
||
function money(n){
|
||
return new Intl.NumberFormat('es-MX',{
|
||
style:'currency',
|
||
currency:'MXN'
|
||
}).format(n);
|
||
}
|
||
|
||
function render(){
|
||
tbody.innerHTML='';
|
||
let subtotal=0;
|
||
let totalDesc=0;
|
||
let index=0;
|
||
|
||
Object.values(cart).forEach(item=>{
|
||
const bruto=item.cantidad*item.precio;
|
||
const desc=bruto*(item.descuento/100);
|
||
const total=bruto-desc;
|
||
|
||
subtotal+=bruto;
|
||
totalDesc+=desc;
|
||
|
||
const tr=document.createElement('tr');
|
||
tr.innerHTML=`
|
||
<td>
|
||
${item.nombre}
|
||
<input type="hidden" name="productos[${index}][producto_id]" value="${Number(item.id)}">
|
||
<input type="hidden" name="productos[${index}][precio_unitario]" value="${Number(item.precio)}">
|
||
<input type="hidden" name="productos[${index}][cantidad]" value="${Number(item.cantidad)}">
|
||
<input type="hidden" name="productos[${index}][descuento_porcentaje]" value="${Number(item.descuento)}">
|
||
<input type="hidden" name="productos[${index}][descuento_monto]" value="${desc.toFixed(2)}">
|
||
|
||
</td>
|
||
|
||
<td>
|
||
<div class="input-group input-group-sm">
|
||
<button type="button" class="btn btn-outline-secondary" onclick="qty(${item.id},-1)">-</button>
|
||
<input type="number"
|
||
name="productos[${index}][cantidad]"
|
||
class="form-control text-center"
|
||
value="${item.cantidad}" min="1"
|
||
onchange="setQty(${item.id},this.value)">
|
||
<button type="button" class="btn btn-outline-secondary" onclick="qty(${item.id},1)">+</button>
|
||
</div>
|
||
</td>
|
||
|
||
<td>${money(item.precio)}</td>
|
||
|
||
<td>
|
||
<div class="input-group input-group-sm">
|
||
<button type="button" class="btn btn-outline-secondary" onclick="descBtn(${item.id},-1)">-</button>
|
||
<input type="number"
|
||
class="form-control text-center"
|
||
value="${item.descuento}" min="0" max="100"
|
||
onchange="setDesc(${item.id},this.value)">
|
||
<button type="button" class="btn btn-outline-secondary" onclick="descBtn(${item.id},1)">+</button>
|
||
</div>
|
||
</td>
|
||
|
||
<td>${money(total)}</td>
|
||
|
||
<td>
|
||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||
onclick="removeItem(${item.id})">×</button>
|
||
</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
index++;
|
||
});
|
||
|
||
const neto=subtotal-totalDesc;
|
||
|
||
document.getElementById('subtotal').innerText=money(subtotal);
|
||
document.getElementById('total-desc').innerText='- '+money(totalDesc);
|
||
document.getElementById('iva').innerText=money(neto*IVA);
|
||
document.getElementById('total').innerText=money(neto*(1+IVA));
|
||
}
|
||
|
||
function addProduct(el){
|
||
const id=el.dataset.id;
|
||
if(!cart[id]){
|
||
cart[id]={
|
||
id,
|
||
nombre:el.dataset.name,
|
||
precio:parseFloat(el.dataset.price),
|
||
cantidad:1,
|
||
descuento:0
|
||
};
|
||
}else{
|
||
cart[id].cantidad++;
|
||
}
|
||
render();
|
||
}
|
||
|
||
function qty(id,d){ cart[id].cantidad=Math.max(1,cart[id].cantidad+d); render(); }
|
||
function setQty(id,v){ cart[id].cantidad=Math.max(1,parseInt(v)); render(); }
|
||
function descBtn(id,d){ cart[id].descuento=Math.max(0,Math.min(100,cart[id].descuento+d)); render(); }
|
||
function setDesc(id,v){ cart[id].descuento=Math.max(0,Math.min(100,parseFloat(v))); render(); }
|
||
function removeItem(id){ delete cart[id]; render(); }
|
||
|
||
document.querySelectorAll('.product-card').forEach(c=>{
|
||
c.addEventListener('click',()=>addProduct(c));
|
||
});
|
||
|
||
document.getElementById('search').addEventListener('keyup',function(){
|
||
const v=this.value.toLowerCase();
|
||
document.querySelectorAll('.product-card').forEach(c=>{
|
||
c.style.display=c.dataset.name.toLowerCase().includes(v)?'block':'none';
|
||
});
|
||
});
|
||
</script>
|
||
@endsection |