2 Commits

3 changed files with 449 additions and 207 deletions

View File

@@ -1,11 +1,41 @@
<template> <template>
<v-container> <v-container fluid class="pa-4 pa-md-6">
<v-form ref="purchase" v-model="valid" @change="onFormChange"> <v-form ref="purchase" v-model="valid" @change="onFormChange">
<!-- Encabezado -->
<v-sheet class="page-header d-flex align-center pa-4 pa-md-6 mb-4 rounded-lg">
<v-icon start size="40" color="white" class="mr-3">mdi-cart-plus</v-icon>
<div>
<h1 class="text-h5 text-md-h4 font-weight-bold text-white mb-0">Nueva Compra</h1>
<p class="text-body-2 text-white text-medium-emphasis mb-0">Registra una nueva venta en el sistema</p>
</div>
</v-sheet>
<!-- Loading -->
<template v-if="loading">
<v-skeleton-loader
class="mb-4 rounded-lg"
type="card-heading, list-item-two-line, list-item-two-line"
></v-skeleton-loader>
</template>
<template v-else>
<!-- Card: Información del Cliente -->
<v-card class="mb-4 rounded-lg" elevation="2">
<v-card-item>
<template #prepend>
<v-icon color="primary" size="36">mdi-account</v-icon>
</template>
<v-card-title class="font-weight-bold text-h6">Información del Cliente</v-card-title>
<v-card-subtitle>Datos básicos de la compra</v-card-subtitle>
</v-card-item>
<v-divider></v-divider>
<v-card-text>
<v-row> <v-row>
<v-col> <v-col cols="12" md="6">
<v-autocomplete <v-autocomplete
v-model="purchase.customer" v-model="purchase.customer"
:items="filteredClients" :items="clients"
:search="client_search" :search="client_search"
no-data-text="No se hallaron clientes" no-data-text="No se hallaron clientes"
item-title="name" item-title="name"
@@ -14,12 +44,14 @@
label="Cliente" label="Cliente"
:rules="[rules.required]" :rules="[rules.required]"
required required
class="mr-4" clearable
variant="outlined"
density="comfortable"
hide-details="auto"
></v-autocomplete> ></v-autocomplete>
<!--<v-btn color="primary" @click="openModal">Agregar Cliente</v-btn>--> <CreateCustomerModal ref="customerModal" @customerCreated="handleNewCustomer" />
<CreateCustomerModal ref="customerModal" @customerCreated="handleNewCustomer"/>
</v-col> </v-col>
<v-col lg="4"> <v-col cols="12" md="3">
<v-text-field <v-text-field
v-model="purchase.date" v-model="purchase.date"
label="Fecha" label="Fecha"
@@ -27,55 +59,108 @@
:rules="[rules.required]" :rules="[rules.required]"
required required
readonly readonly
variant="outlined"
density="comfortable"
hide-details="auto"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="3">
<v-btn
variant="tonal"
color="primary"
size="small"
class="mt-1"
prepend-icon="mdi-plus"
@click="openModal"
>
Nuevo Cliente
</v-btn>
</v-col>
</v-row> </v-row>
<v-row class="mt-0">
<v-col cols="12">
<v-textarea <v-textarea
v-model="purchase.notes" v-model="purchase.notes"
label="Notas" label="Notas"
rows="2" rows="2"
variant="outlined"
density="comfortable"
hide-details="auto"
></v-textarea> ></v-textarea>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Card: Productos -->
<v-card class="mb-4 rounded-lg" elevation="2">
<v-card-item>
<template #prepend>
<v-icon color="green-darken-1" size="36">mdi-package-variant-closed</v-icon>
</template>
<v-card-title class="font-weight-bold text-h6">Productos</v-card-title>
<v-card-subtitle>Agrega los productos de la compra</v-card-subtitle>
</v-card-item>
<v-divider></v-divider> <v-divider></v-divider>
<v-container>
<v-toolbar> <!-- Encabezados de columnas (solo desktop) -->
<v-toolbar-title secondary>Productos</v-toolbar-title> <div class="d-none d-md-flex px-6 pt-3 text-caption font-weight-bold text-grey">
</v-toolbar> <div class="flex-grow-1 flex-shrink-1" style="min-width:0">Producto</div>
<v-container v-for="(line, index) in purchase.saleline_set" :key="line.id"> <div class="mx-2 text-center" style="width:100px; flex-shrink:0">Cantidad</div>
<v-row> <div class="mx-2 text-end" style="width:110px; flex-shrink:0">Precio</div>
<v-col <div class="mx-2 text-end" style="width:110px; flex-shrink:0">Subtotal</div>
lg="9"> <div style="width:40px; flex-shrink:0"></div>
</div>
<v-card-text class="pa-0 pa-md-4">
<div
v-for="(line, index) in purchase.saleline_set"
:key="line.id"
class="product-line pa-3 pa-md-2"
>
<v-row no-gutters align="start" class="flex-md-nowrap">
<!-- Producto -->
<v-col cols="12" md class="mb-2 mb-md-0">
<v-autocomplete <v-autocomplete
v-model="line.product" v-model="line.product"
:items="filteredProducts" :items="products"
:search="product_search" :search="product_search"
@update:modelValue="onProductChange(index)" @update:modelValue="onProductChange(index)"
no-data-text="No se hallaron productos" no-data-text="No se hallaron productos"
item-title="name" item-title="name"
item-value="id" item-value="id"
item-subtitle="Price"
label="Producto" label="Producto"
:rules="[rules.required]" :rules="[rules.required]"
required required
variant="outlined"
density="compact"
hide-details="auto"
class="product-select"
> >
<template v-slot:item="{ props, item }"> <template v-slot:item="{ props, item }">
<v-list-item v-bind="props" :title="item.raw.name" :subtitle="formatPrice(item.raw.price)"></v-list-item> <v-list-item v-bind="props" :title="item.raw.name" :subtitle="formatPrice(item.raw.price)"></v-list-item>
</template> </template>
</v-autocomplete> </v-autocomplete>
</v-col> </v-col>
<v-col
lg="2" <!-- Cantidad -->
> <v-col cols="4" md="auto" class="pe-1">
<v-text-field <v-text-field
v-model.number="line.quantity" v-model.number="line.quantity"
label="Cantidad" label="Cant."
type="number" type="number"
:rules="[rules.required,rules.positive]" :rules="[rules.required, rules.positive]"
required required
variant="outlined"
density="compact"
hide-details="auto"
min="0"
class="quantity-field"
></v-text-field> ></v-text-field>
</v-col> </v-col>
</v-row>
<v-row> <!-- Precio -->
<v-col> <v-col cols="4" md="auto" class="px-1">
<v-text-field <v-text-field
v-model.number="line.unit_price" v-model.number="line.unit_price"
label="Precio" label="Precio"
@@ -84,64 +169,148 @@
prefix="$" prefix="$"
required required
readonly readonly
variant="outlined"
density="compact"
hide-details="auto"
class="price-field"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col>
<v-text-field <!-- Subtotal + UdM -->
v-model="line.measuring_unit" <v-col cols="4" md="auto" class="ps-1">
label="UdM"
persistent-placeholder="true"
readonly
></v-text-field>
</v-col>
<v-col>
<v-text-field <v-text-field
type="number" type="number"
:value="calculateSubtotal(line)" :value="calculateSubtotal(line)"
label="Subtotal" label="Subtotal"
prefix="$" prefix="$"
readonly readonly
disable variant="outlined"
persistent-placeholder="true" density="compact"
hide-details="auto"
class="subtotal-field"
></v-text-field> ></v-text-field>
<div class="text-caption text-grey text-center mt-1 d-md-none">{{ line.measuring_unit || 'Ud' }}</div>
</v-col> </v-col>
<v-col>
<v-btn @click="removeLine(index)" color="red">Eliminar</v-btn> <!-- Eliminar + UdM desktop -->
<v-col cols="12" md="auto" class="d-flex align-center mt-2 mt-md-0 ps-md-2">
<v-btn
@click="removeLine(index)"
color="error"
variant="text"
icon="mdi-delete"
size="small"
density="comfortable"
class="flex-shrink-0"
></v-btn>
<span class="text-caption text-grey ms-2 d-none d-md-inline">{{ line.measuring_unit || 'Ud' }}</span>
</v-col> </v-col>
</v-row> </v-row>
<v-alert type="warning" :duration="2000" closable v-model="show_alert_lines"> </div>
<v-alert
type="warning"
closable
v-model="show_alert_lines"
density="compact"
variant="tonal"
class="ma-3"
>
No se puede eliminar la única línea. No se puede eliminar la única línea.
</v-alert> </v-alert>
</v-container>
<v-btn @click="addLine" color="blue">Agregar</v-btn> <v-divider class="my-2"></v-divider>
</v-container>
<v-btn
@click="addLine"
color="primary"
variant="tonal"
prepend-icon="mdi-plus"
class="mt-2"
>
Agregar Producto
</v-btn>
</v-card-text>
</v-card>
<!-- Card: Resumen y Pago -->
<v-card v-if="calculateTotal > 0" class="mb-4 rounded-lg" elevation="2">
<v-card-item>
<template #prepend>
<v-icon color="green-darken-2" size="36">mdi-credit-card</v-icon>
</template>
<v-card-title class="font-weight-bold text-h6">Resumen y Pago</v-card-title>
</v-card-item>
<v-divider></v-divider> <v-divider></v-divider>
<v-text-field <v-card-text>
:value="calculateTotal" <v-row align="center">
label="Total" <v-col cols="12" md="6">
prefix="$"
readonly
persistent-placeholder="true"
></v-text-field>
<v-container v-if="calculateTotal > 0">
<v-select <v-select
:items="payment_methods" :items="payment_methods"
v-model="purchase.payment_method" v-model="purchase.payment_method"
item-title="text" item-title="text"
item-value="value" item-value="value"
label="Pago en" label="Método de Pago"
:rules="[rules.required]" :rules="[rules.required]"
required required
variant="outlined"
density="comfortable"
hide-details="auto"
></v-select> ></v-select>
<v-btn @click="openCasherModal" v-if="purchase.payment_method === 'CASH'">Calcular Devuelta</v-btn> </v-col>
<CasherModal :total_purchase="calculateTotal" ref="casherModal"</CasherModal> <v-col cols="12" md="6" class="text-center text-md-end">
</v-container> <div class="text-body-2 text-grey mb-1">Total de la compra</div>
<v-btn @click="submit" color="green">Comprar</v-btn> <div class="total-amount">{{ formatPrice(calculateTotal) }}</div>
<v-alert type="error" :duration="2000" closable v-model="show_alert_purchase"> <v-btn
v-if="purchase.payment_method === 'CASH'"
@click="openCasherModal"
color="orange-darken-2"
variant="tonal"
prepend-icon="mdi-cash"
size="small"
class="mt-1"
>
Calcular Devuelta
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Botón Comprar -->
<div class="d-flex flex-column flex-sm-row justify-space-between align-center ga-4 mt-6">
<div class="text-caption text-grey">
<v-icon start size="small" color="grey">mdi-information</v-icon>
Todos los campos marcados con * son obligatorios
</div>
<v-btn
@click="submit"
color="green-darken-1"
size="x-large"
prepend-icon="mdi-cart-check"
variant="elevated"
class="px-8 submit-btn"
:disabled="calculateTotal <= 0"
>
Comprar
</v-btn>
</div>
<v-alert
type="error"
closable
v-model="show_alert_purchase"
density="compact"
variant="tonal"
class="mt-4"
>
Verifique los campos obligatorios. Verifique los campos obligatorios.
</v-alert> </v-alert>
<CasherModal :total_purchase="calculateTotal" ref="casherModal" />
</template>
</v-form> </v-form>
</v-container> </v-container>
</template> </template>
<script> <script>
@@ -161,6 +330,7 @@
data() { data() {
return { return {
api: inject('api'), api: inject('api'),
loading: true,
valid: false, valid: false,
form_changed: false, form_changed: false,
show_alert_lines: false, show_alert_lines: false,
@@ -179,10 +349,6 @@
required: value => !!value || 'Requerido.', required: value => !!value || 'Requerido.',
positive: value => value > 0 || 'La cantidad debe ser mayor que 0.', positive: value => value > 0 || 'La cantidad debe ser mayor que 0.',
}, },
menuItems: [
{ title: 'Inicio', route: '/'},
{ title: 'Compras', route:'/compras'},
],
clients: [], clients: [],
products: [], products: [],
}; };
@@ -192,41 +358,12 @@
this.fetchProducts(); this.fetchProducts();
this.fetchPaymentMethods(); this.fetchPaymentMethods();
}, },
watch: {
group () {
this.drawer = false
},
},
beforeMount() {
window.addEventListener('beforeunload', this.confirmLeave);
},
beforeDestroy() {
window.removeEventListener('beforeunload', this.confirmLeave);
},
computed: { computed: {
calculateTotal() { calculateTotal() {
return this.purchase.saleline_set.reduce((total, saleline) => { return this.purchase.saleline_set.reduce((total, saleline) => {
return total + this.calculateSubtotal(saleline); return total + this.calculateSubtotal(saleline);
}, 0); }, 0);
}, },
filteredClients() {
return this.clients.filter(client => {
if (this.client_search === '') {
return [];
} else {
return client.name.toLowerCase().includes(this.client_search.toLowerCase());
}
});
},
filteredProducts() {
return this.products.filter(product => {
if (this.product_search === '') {
return [];
} else {
return product.name.toLowerCase().includes(this.product_search.toLowerCase());
}
});
},
}, },
methods: { methods: {
openModal() { openModal() {
@@ -250,7 +387,6 @@
const today = new Date(); const today = new Date();
const gmtOffSet = -5; const gmtOffSet = -5;
const localDate = new Date(today.getTime() + (gmtOffSet * 60 * 60 * 1000)); const localDate = new Date(today.getTime() + (gmtOffSet * 60 * 60 * 1000));
// Formatear la fecha y hora en el formato YYYY-MM-DDTHH:MM
const formattedDate = localDate.toISOString().slice(0,16); const formattedDate = localDate.toISOString().slice(0,16);
return formattedDate; return formattedDate;
}, },
@@ -267,6 +403,9 @@
}) })
.catch(error => { .catch(error => {
console.error(error); console.error(error);
})
.finally(() => {
this.loading = false;
}); });
}, },
handleNewCustomer(newCustomer){ handleNewCustomer(newCustomer){
@@ -330,7 +469,7 @@
this.$router.push(route); this.$router.push(route);
}, },
formatPrice(price) { formatPrice(price) {
return new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'COP' }).format(price); return new Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP', minimumFractionDigits: 0 }).format(price);
}, },
}, },
mounted() { mounted() {
@@ -339,4 +478,28 @@
}; };
</script> </script>
<style> <style>
.page-header {
background: linear-gradient(135deg, #1565C0 0%, #0D47A1 100%);
}
.product-line:nth-child(odd) {
background-color: rgba(0, 0, 0, 0.02);
}
.total-amount {
font-size: 2rem;
font-weight: 700;
color: rgb(46, 125, 50);
line-height: 1.2;
}
.submit-btn {
min-width: 220px;
}
@media (max-width: 599px) {
.total-amount {
font-size: 1.6rem;
}
}
</style> </style>

View File

@@ -1,34 +1,112 @@
<template> <template>
<v-container > <v-container fluid class="pa-0">
<v-responsive> <v-sheet
<v-toolbar> class="hero-section d-flex align-center justify-center text-center pa-8"
<v-toolbar-title>Don Confiao te atiende</v-toolbar-title> >
</v-toolbar> <div>
<v-card> <v-img
<v-card-title>Hacer parte de la tienda la ilusión</v-card-title> :src="logo"
<v-card-text> alt="Don Confiao"
Recuerda que participando de esta tienda le apuestas a la economía solidaria, al mercado justo, a la alimentación sana, al campesinado colombiano y a un mundo mejor. max-width="180"
</v-card-text> class="mx-auto mb-4"
</v-card> />
<h1 class="text-h4 font-weight-bold text-white mb-2">
<v-card> Don Confiao te atiende
<v-card-title>En desarrollo</v-card-title> </h1>
<v-card-text> <p class="text-subtitle-1 font-italic font-weight-bold text-white">
Don confiao apenas esta entendiendo como funciona esta tienda y por ahora <ResaltedText>solo puede atender las compras de contado</ResaltedText>, ya sea en efectivo o consignación. Economía solidaria, mercado justo, alimentación sana
</p>
<v-alert type="warning">
Si no vas a pagar tu compra recuerda que debes hacerlo en la planilla manual</v-alert>
</v-card-text>
</v-card>
<v-card>
<v-card-title>A comprar</v-card-title>
<v-card-text>
El siguiente botón te permitirá registrar tu compra. Cuando finalices te pedimos que ingrese el número de la compra, la fecha y el valor en la planilla física.
<div class="text-center">
<v-btn :to="{ path: 'comprar' }" color="green">Ir a Comprar</v-btn>
</div> </div>
</v-sheet>
<v-container class="py-6">
<v-row>
<v-col cols="12" md="4">
<v-card class="h-100" elevation="2">
<v-card-item>
<template #prepend>
<v-icon color="green" size="48">mdi-hand-heart</v-icon>
</template>
<v-card-title class="font-weight-bold">Nuestra Tienda</v-card-title>
</v-card-item>
<v-card-text>
Hacer parte de la tienda la ilusión. Participando de esta tienda le apuestas a la economía solidaria, al mercado justo, a la alimentación sana, al campesinado colombiano y a un mundo mejor.
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-responsive> </v-col>
<v-col cols="12" md="4">
<v-card class="h-100" elevation="2">
<v-card-item>
<template #prepend>
<v-icon color="orange-darken-2" size="48">mdi-progress-wrench</v-icon>
</template>
<v-card-title class="font-weight-bold">En Desarrollo</v-card-title>
</v-card-item>
<v-card-text>
Don Confiao apenas está entendiendo cómo funciona esta tienda y por ahora <ResaltedText>solo puede atender las compras de contado</ResaltedText>, ya sea en efectivo o consignación.
<v-alert type="warning" class="mt-3" density="compact">
Si no vas a pagar tu compra recuerda que debes hacerlo en la planilla manual
</v-alert>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card class="h-100" elevation="2">
<v-card-item>
<template #prepend>
<v-icon color="blue" size="48">mdi-store</v-icon>
</template>
<v-card-title class="font-weight-bold">Catálogo</v-card-title>
</v-card-item>
<v-card-text>
Explora nuestro catálogo de productos disponibles. Encuentra todo lo que necesitas y arma tu pedido fácilmente.
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row class="mt-4">
<v-col cols="12" class="text-center">
<h2 class="text-h5 font-weight-bold mb-4">¿Qué deseas hacer?</h2>
<div class="d-flex flex-wrap justify-center ga-4">
<v-btn
:to="{ path: 'comprar' }"
color="green"
size="x-large"
prepend-icon="mdi-cart"
variant="elevated"
class="px-8"
>
Ir a Comprar
</v-btn>
<v-btn
:to="{ path: 'catalog' }"
color="primary"
size="x-large"
prepend-icon="mdi-store"
variant="elevated"
class="px-8"
>
Ver Catálogo
</v-btn>
</div>
</v-col>
</v-row>
</v-container>
</v-container> </v-container>
</template> </template>
<script setup>
import ResaltedText from '@/components/ResaltedText.vue'
import logo from '@/assets/logo.png'
</script>
<style scoped>
.hero-section {
min-height: 320px;
background: linear-gradient(135deg, #1B5E20 0%, #2E7D32 30%, #E65100 100%) !important;
color: white;
}
</style>

View File

@@ -3,4 +3,5 @@
</template> </template>
<script setup> <script setup>
import Wellcome from '@/components/Wellcome.vue'
</script> </script>