feat: mejorar UI con Vuetify 3, POS tipo venta real, dashboard
This commit is contained in:
@@ -1,7 +1,73 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Inicio</h2>
|
||||
<p>Sistema de gestión para el Toldo de CESTA</p>
|
||||
<p>Funciona offline - Datos guardados localmente</p>
|
||||
<h1 class="text-h4 mb-4">Panel de Control</h1>
|
||||
<v-row>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<div class="text-h6">Ventas del día</div>
|
||||
<div class="text-h4">${{ totalVentas }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<div class="text-h6">Gastos del día</div>
|
||||
<div class="text-h4">${{ totalGastos }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<div class="text-h6">Productos</div>
|
||||
<div class="text-h4">{{ productos.length }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<div class="text-h6">Productores</div>
|
||||
<div class="text-h4">{{ productores.length }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="mt-4">
|
||||
<v-col>
|
||||
<v-card>
|
||||
<v-card-title>Acciones Rápidas</v-card-title>
|
||||
<v-card-text>
|
||||
<v-btn to="/ventas" color="primary" class="mr-2">Nueva Venta</v-btn>
|
||||
<v-btn to="/gastos" color="secondary" class="mr-2">Registrar Gasto</v-btn>
|
||||
<v-btn to="/inventario" color="accent">Gestionar Inventario</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { db } from '@toldo/data';
|
||||
import { VentasService, GastosService } from '@toldo/services';
|
||||
|
||||
const ventasService = new VentasService();
|
||||
const gastosService = new GastosService();
|
||||
const totalVentas = ref(0);
|
||||
const totalGastos = ref(0);
|
||||
const productos = ref<any[]>([]);
|
||||
const productores = ref<any[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
productos.value = await db.productos.toArray();
|
||||
productores.value = await db.productores.toArray();
|
||||
const ventas = await ventasService.getVentasDelDia(new Date());
|
||||
const gastos = await gastosService.getGastosDelDia(new Date());
|
||||
totalVentas.value = ventas.reduce((acc, v) => acc + v.total, 0);
|
||||
totalGastos.value = gastos.reduce((acc, g) => acc + g.monto, 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,50 +1,67 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Inventario</h2>
|
||||
|
||||
<section>
|
||||
<h3>Productores</h3>
|
||||
<input v-model="nuevoProductor" placeholder="Nombre del productor" />
|
||||
<button @click="crearProductor">Agregar Productor</button>
|
||||
<ul>
|
||||
<li v-for="p in productores" :key="p.id">{{ p.nombre }}</li>
|
||||
</ul>
|
||||
</section>
|
||||
<h1 class="text-h4 mb-4">Inventario y Productos</h1>
|
||||
|
||||
<section>
|
||||
<h3>Productos (por productor)</h3>
|
||||
<select v-model="productorSel">
|
||||
<option value="">Seleccionar productor</option>
|
||||
<option v-for="p in productores" :key="p.id" :value="p.id">{{ p.nombre }}</option>
|
||||
</select>
|
||||
<input v-model="nombreProd" placeholder="Ej: Café molido 250gr" />
|
||||
<input v-model.number="precioProd" type="number" placeholder="Precio unitario" />
|
||||
<button @click="crearProducto">Agregar Producto</button>
|
||||
<ul>
|
||||
<li v-for="prod in productos" :key="prod.id">{{ prod.nombrePresentacion }} - ${{ prod.precioUnitario }}</li>
|
||||
</ul>
|
||||
</section>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card class="mb-4">
|
||||
<v-card-title>Productores</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="8">
|
||||
<v-text-field v-model="nuevoProductor" label="Nombre del productor" density="compact"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-btn @click="crearProductor" color="primary" block>Agregar</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-list>
|
||||
<v-list-item v-for="p in productores" :key="p.id" :title="p.nombre"></v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<section>
|
||||
<h3>Ingreso Inicial</h3>
|
||||
<select v-model="prodInv">
|
||||
<option value="">Seleccionar producto</option>
|
||||
<option v-for="prod in productos" :key="prod.id" :value="prod.id">{{ prod.nombrePresentacion }}</option>
|
||||
</select>
|
||||
<input v-model.number="cantInv" type="number" placeholder="Cantidad" />
|
||||
<button @click="ingresoInicial">Registrar Ingreso Inicial</button>
|
||||
</section>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card class="mb-4">
|
||||
<v-card-title>Productos</v-card-title>
|
||||
<v-card-text>
|
||||
<v-select v-model="productorSel" :items="productores" item-title="nombre" item-value="id" label="Productor" density="compact" class="mb-2"></v-select>
|
||||
<v-text-field v-model="nombreProd" label="Nombre/Presentación (ej: Café molido 250gr)" density="compact" class="mb-2"></v-text-field>
|
||||
<v-text-field v-model.number="precioProd" label="Precio unitario" type="number" density="compact" class="mb-2"></v-text-field>
|
||||
<v-btn @click="crearProducto" color="primary" block>Agregar Producto</v-btn>
|
||||
<v-list class="mt-3">
|
||||
<v-list-item v-for="prod in productos" :key="prod.id" :title="prod.nombrePresentacion" :subtitle="'$' + prod.precioUnitario"></v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<section>
|
||||
<h3>Ingreso entre semana</h3>
|
||||
<select v-model="prodInv2">
|
||||
<option value="">Seleccionar producto</option>
|
||||
<option v-for="prod in productos" :key="prod.id" :value="prod.id">{{ prod.nombrePresentacion }}</option>
|
||||
</select>
|
||||
<input v-model.number="cantInv2" type="number" placeholder="Cantidad" />
|
||||
<input v-model="recibidoPor" placeholder="Recibido por" />
|
||||
<button @click="ingresoSemana">Registrar Ingreso</button>
|
||||
</section>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card>
|
||||
<v-card-title>Ingreso Inicial del Día</v-card-title>
|
||||
<v-card-text>
|
||||
<v-select v-model="prodInv" :items="productos" item-title="nombrePresentacion" item-value="id" label="Producto" density="compact" class="mb-2"></v-select>
|
||||
<v-text-field v-model.number="cantInv" label="Cantidad" type="number" density="compact" class="mb-2"></v-text-field>
|
||||
<v-btn @click="ingresoInicial" color="success" block>Registrar Ingreso Inicial</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card>
|
||||
<v-card-title>Ingreso entre Semana</v-card-title>
|
||||
<v-card-text>
|
||||
<v-select v-model="prodInv2" :items="productos" item-title="nombrePresentacion" item-value="id" label="Producto" density="compact" class="mb-2"></v-select>
|
||||
<v-text-field v-model.number="cantInv2" label="Cantidad" type="number" density="compact" class="mb-2"></v-text-field>
|
||||
<v-text-field v-model="recibidoPor" label="Recibido por" density="compact" class="mb-2"></v-text-field>
|
||||
<v-btn @click="ingresoSemana" color="info" block>Registrar Ingreso</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -73,14 +90,14 @@ async function cargar() {
|
||||
|
||||
async function crearProductor() {
|
||||
if (!nuevoProductor.value) return;
|
||||
await db.productores.add({ id: crypto.randomUUID(), nombre: nuevoProductor.value, activo: true, createdAt: new Date() });
|
||||
await db.productores.add({ id: crypto.randomUUID(), nombre: nuevoProductor.value, activo: true, createdAt: new Date() } as any);
|
||||
nuevoProductor.value = '';
|
||||
await cargar();
|
||||
}
|
||||
|
||||
async function crearProducto() {
|
||||
if (!productorSel.value || !nombreProd.value) return;
|
||||
await db.productos.add({ id: crypto.randomUUID(), productorId: productorSel.value, nombrePresentacion: nombreProd.value, precioUnitario: precioProd.value, activo: true, createdAt: new Date() });
|
||||
await db.productos.add({ id: crypto.randomUUID(), productorId: productorSel.value, nombrePresentacion: nombreProd.value, precioUnitario: Number(precioProd.value), activo: true, createdAt: new Date() } as any);
|
||||
nombreProd.value = '';
|
||||
precioProd.value = 0;
|
||||
await cargar();
|
||||
@@ -88,14 +105,14 @@ async function crearProducto() {
|
||||
|
||||
async function ingresoInicial() {
|
||||
if (!prodInv.value || cantInv.value <= 0) return;
|
||||
await invService.registrarIngresoInicial(prodInv.value, cantInv.value, new Date());
|
||||
await invService.registrarIngresoInicial(prodInv.value, Number(cantInv.value), new Date());
|
||||
cantInv.value = 0;
|
||||
alert('Ingreso inicial registrado');
|
||||
}
|
||||
|
||||
async function ingresoSemana() {
|
||||
if (!prodInv2.value || cantInv2.value <= 0 || !recibidoPor.value) return;
|
||||
await invService.registrarIngresoSemana(prodInv2.value, cantInv2.value, new Date(), recibidoPor.value);
|
||||
await invService.registrarIngresoSemana(prodInv2.value, Number(cantInv2.value), new Date(), recibidoPor.value);
|
||||
cantInv2.value = 0;
|
||||
recibidoPor.value = '';
|
||||
alert('Ingreso entre semana registrado');
|
||||
|
||||
@@ -1,38 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Ventas</h2>
|
||||
|
||||
<section>
|
||||
<h3>Nueva Venta</h3>
|
||||
<select v-model="productoSel">
|
||||
<option value="">Seleccionar producto</option>
|
||||
<option v-for="prod in productos" :key="prod.id" :value="prod.id">{{ prod.nombrePresentacion }} - ${{ prod.precioUnitario }}</option>
|
||||
</select>
|
||||
<input v-model.number="cantidad" type="number" placeholder="Cantidad" />
|
||||
<select v-model="formaPago">
|
||||
<option value="EFECTIVO">Efectivo</option>
|
||||
<option value="QR">QR</option>
|
||||
</select>
|
||||
<button @click="agregarLinea">Agregar Línea</button>
|
||||
</section>
|
||||
<h1 class="text-h4 mb-4">Punto de Venta (POS)</h1>
|
||||
<v-row>
|
||||
<v-col cols="12" md="8">
|
||||
<v-card>
|
||||
<v-card-title>Productos Disponibles</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-for="prod in productos" :key="prod.id">
|
||||
<v-card @click="agregarProducto(prod)" class="cursor-pointer" hover>
|
||||
<v-card-text>
|
||||
<div class="text-subtitle-1 font-weight-bold">{{ prod.nombrePresentacion }}</div>
|
||||
<div class="text-h6 text-primary">${{ prod.precioUnitario }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<section>
|
||||
<h3>Carrito</h3>
|
||||
<table v-if="lineas.length">
|
||||
<thead><tr><th>Producto</th><th>Cant</th><th>Precio</th><th>Subtotal</th><th>Forma</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="(l, i) in lineas" :key="i">
|
||||
<td>{{ getProdNombre(l.productoId) }}</td>
|
||||
<td>{{ l.cantidad }}</td>
|
||||
<td>${{ l.precioUnitario }}</td>
|
||||
<td>${{ l.subtotal }}</td>
|
||||
<td>{{ l.formaPago }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-if="lineas.length"><strong>Total: ${{ total }}</strong></p>
|
||||
<button v-if="lineas.length" @click="registrarVenta">Registrar Venta</button>
|
||||
</section>
|
||||
<v-col cols="12" md="4">
|
||||
<v-card>
|
||||
<v-card-title>Carrito de Compras</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list v-if="lineas.length">
|
||||
<v-list-item v-for="(l, i) in lineas" :key="i">
|
||||
<v-list-item-title>{{ getProdNombre(l.productoId) }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-row align="center" class="mt-1">
|
||||
<v-col cols="4">
|
||||
<v-text-field v-model.number="l.cantidad" type="number" density="compact" hide-details @update:model-value="actualizarLinea(i)"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="4">${{ l.precioUnitario }}</v-col>
|
||||
<v-col cols="4">${{ l.subtotal }}</v-col>
|
||||
</v-row>
|
||||
</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-btn icon="mdi-delete" size="small" @click="eliminarLinea(i)"></v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-divider class="my-3"></v-divider>
|
||||
<div class="text-h5">Total: ${{ total }}</div>
|
||||
<v-select v-model="formaPago" :items="['EFECTIVO', 'QR']" label="Forma de Pago" class="mt-3"></v-select>
|
||||
<v-btn @click="registrarVenta" color="success" block size="large" :disabled="lineas.length === 0" class="mt-2">COBRAR</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -43,10 +59,8 @@ import { VentasService } from '@toldo/services';
|
||||
|
||||
const ventasService = new VentasService();
|
||||
const productos = ref<any[]>([]);
|
||||
const productoSel = ref('');
|
||||
const cantidad = ref(1);
|
||||
const formaPago = ref('EFECTIVO');
|
||||
const lineas = ref<any[]>([]);
|
||||
const formaPago = ref('EFECTIVO');
|
||||
|
||||
async function cargar() {
|
||||
productos.value = await db.productos.toArray();
|
||||
@@ -56,39 +70,49 @@ function getProdNombre(id: string) {
|
||||
return productos.value.find(p => p.id === id)?.nombrePresentacion || id;
|
||||
}
|
||||
|
||||
function agregarLinea() {
|
||||
if (!productoSel.value || cantidad.value <= 0) return;
|
||||
const prod = productos.value.find(p => p.id === productoSel.value);
|
||||
if (!prod) return;
|
||||
lineas.value.push({
|
||||
id: crypto.randomUUID(),
|
||||
ventaId: '',
|
||||
productoId: prod.id,
|
||||
cantidad: cantidad.value,
|
||||
precioUnitario: prod.precioUnitario,
|
||||
subtotal: prod.precioUnitario * cantidad.value,
|
||||
formaPago: formaPago.value
|
||||
});
|
||||
productoSel.value = '';
|
||||
cantidad.value = 1;
|
||||
function agregarProducto(prod: any) {
|
||||
const existente = lineas.value.findIndex(l => l.productoId === prod.id);
|
||||
if (existente >= 0) {
|
||||
lineas.value[existente].cantidad++;
|
||||
lineas.value[existente].subtotal = lineas.value[existente].cantidad * lineas.value[existente].precioUnitario;
|
||||
} else {
|
||||
lineas.value.push({
|
||||
id: crypto.randomUUID(),
|
||||
ventaId: '',
|
||||
productoId: prod.id,
|
||||
cantidad: 1,
|
||||
precioUnitario: prod.precioUnitario,
|
||||
subtotal: prod.precioUnitario,
|
||||
formaPago: formaPago.value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function actualizarLinea(i: number) {
|
||||
lineas.value[i].subtotal = lineas.value[i].cantidad * lineas.value[i].precioUnitario;
|
||||
}
|
||||
|
||||
function eliminarLinea(i: number) {
|
||||
lineas.value.splice(i, 1);
|
||||
}
|
||||
|
||||
const total = computed(() => lineas.value.reduce((acc, l) => acc + l.subtotal, 0));
|
||||
|
||||
async function registrarVenta() {
|
||||
if (lineas.value.length === 0) return;
|
||||
const ventaId = crypto.randomUUID();
|
||||
const lineasVenta = lineas.value.map(l => ({ ...l, ventaId, formaPago: formaPago.value }));
|
||||
const venta = {
|
||||
id: ventaId,
|
||||
fecha: new Date(),
|
||||
lineas: lineas.value.map(l => ({ ...l, ventaId })),
|
||||
lineas: lineasVenta,
|
||||
subtotal: total.value,
|
||||
total: total.value,
|
||||
createdAt: new Date()
|
||||
};
|
||||
const lineasVenta = lineas.value.map(l => ({ ...l, ventaId }));
|
||||
await ventasService.registrarVenta(venta, lineasVenta);
|
||||
lineas.value = [];
|
||||
alert('Venta registrada');
|
||||
alert(`Venta registrada por $${total.value} - ${formaPago.value}`);
|
||||
}
|
||||
|
||||
onMounted(cargar);
|
||||
|
||||
Reference in New Issue
Block a user