0
1
Fork 0

Merge pull request 'Vistas sección pedidos' (!77) from wbuezas/hedera-web-mindshore:feature/Pedidos into 4922-vueMigration

Reviewed-on: verdnatura/hedera-web#77
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
This commit is contained in:
Javier Segarra 2024-08-16 06:52:22 +00:00
commit 5053a908f7
37 changed files with 1451 additions and 823 deletions

View File

@ -24,7 +24,7 @@ module.exports = configure(function (ctx) {
// app boot file (/src/boot) // app boot file (/src/boot)
// --> boot files are part of "main.js" // --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-webpack/boot-files // https://v2.quasar.dev/quasar-cli-webpack/boot-files
boot: ['i18n', 'axios', 'error-handler', 'app'], boot: ['i18n', 'axios', 'vnDate', 'error-handler', 'app'],
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
css: ['app.scss', 'width.scss', 'responsive.scss'], css: ['app.scss', 'width.scss', 'responsive.scss'],
@ -67,7 +67,7 @@ module.exports = configure(function (ctx) {
// https://v2.quasar.dev/quasar-cli-webpack/handling-webpack // https://v2.quasar.dev/quasar-cli-webpack/handling-webpack
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
chainWebpack(chain) { chainWebpack (chain) {
chain chain
.plugin('eslint-webpack-plugin') .plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js', 'vue'] }]); .use(ESLintPlugin, [{ extensions: ['js', 'vue'] }]);
@ -143,7 +143,7 @@ module.exports = configure(function (ctx) {
maxAge: 1000 * 60 * 60 * 24 * 30, maxAge: 1000 * 60 * 60 * 24 * 30,
// Tell browser when a file from the server should expire from cache (in ms) // Tell browser when a file from the server should expire from cache (in ms)
chainWebpackWebserver(chain) { chainWebpackWebserver (chain) {
chain chain
.plugin('eslint-webpack-plugin') .plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]); .use(ESLintPlugin, [{ extensions: ['js'] }]);
@ -163,7 +163,7 @@ module.exports = configure(function (ctx) {
// for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts]) // for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts])
// if using workbox in InjectManifest mode // if using workbox in InjectManifest mode
chainWebpackCustomSW(chain) { chainWebpackCustomSW (chain) {
chain chain
.plugin('eslint-webpack-plugin') .plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]); .use(ESLintPlugin, [{ extensions: ['js'] }]);
@ -240,13 +240,13 @@ module.exports = configure(function (ctx) {
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
chainWebpackMain(chain) { chainWebpackMain (chain) {
chain chain
.plugin('eslint-webpack-plugin') .plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]); .use(ESLintPlugin, [{ extensions: ['js'] }]);
}, },
chainWebpackPreload(chain) { chainWebpackPreload (chain) {
chain chain
.plugin('eslint-webpack-plugin') .plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]); .use(ESLintPlugin, [{ extensions: ['js'] }]);

View File

@ -1,11 +1,11 @@
<script setup>
import { useAppStore } from 'stores/app';
import { onBeforeMount } from 'vue';
const appStore = useAppStore();
onBeforeMount(() => appStore.init());
</script>
<template> <template>
<router-view /> <router-view />
</template> </template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'App'
});
</script>

View File

@ -1,10 +1,10 @@
import { boot } from 'quasar/wrappers' import { boot } from 'quasar/wrappers';
import { appStore } from 'stores/app' import { useAppStore } from 'stores/app';
import { userStore } from 'stores/user' import { userStore } from 'stores/user';
export default boot(({ app }) => { export default boot(({ app }) => {
const props = app.config.globalProperties const props = app.config.globalProperties;
props.$app = appStore() props.$app = useAppStore();
props.$user = userStore() props.$user = userStore();
props.$actions = document.createElement('div') props.$actions = document.createElement('div');
}) });

20
src/boot/vnDate.js Normal file
View File

@ -0,0 +1,20 @@
import { boot } from 'quasar/wrappers';
export default boot(() => {
Date.vnUTC = () => {
const env = process.env.NODE_ENV;
if (!env || env === 'development') {
return new Date(Date.UTC(2001, 0, 1, 11));
}
return new Date();
};
Date.vnNew = () => {
return new Date(Date.vnUTC());
};
Date.vnNow = () => {
return new Date(Date.vnUTC()).getTime();
};
});

View File

@ -184,7 +184,6 @@ defineExpose({
v-if="!loading" v-if="!loading"
ref="addressFormRef" ref="addressFormRef"
class="column full-width q-gutter-y-xs" class="column full-width q-gutter-y-xs"
@submit="submit()"
> >
<span class="text-h6 text-bold"> <span class="text-h6 text-bold">
{{ title }} {{ title }}
@ -197,7 +196,7 @@ defineExpose({
:class="{ 'q-mt-md': showBottomActions }" :class="{ 'q-mt-md': showBottomActions }"
> >
<QBtn <QBtn
v-if="defaultActions" v-if="defaultActions && showBottomActions"
:label="t('cancel')" :label="t('cancel')"
:icon="showBottomActions ? undefined : 'check'" :icon="showBottomActions ? undefined : 'check'"
rounded rounded
@ -208,12 +207,12 @@ defineExpose({
<QBtn <QBtn
v-if="defaultActions" v-if="defaultActions"
:label="t('save')" :label="t('save')"
type="submit"
:icon="showBottomActions ? undefined : 'check'" :icon="showBottomActions ? undefined : 'check'"
rounded rounded
no-caps no-caps
flat flat
:disabled="!showBottomActions && !updatedColumns.length" :disabled="!showBottomActions && !updatedColumns.length"
@click="submit()"
/> />
<slot name="actions" /> <slot name="actions" />
</component> </component>

View File

@ -9,7 +9,7 @@ const emit = defineEmits([
'remove' 'remove'
]); ]);
const $props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: [String, Number], type: [String, Number],
default: null default: null
@ -33,7 +33,7 @@ const requiredFieldRule = val => !!val || t('globals.fieldRequired');
const vnInputRef = ref(null); const vnInputRef = ref(null);
const value = computed({ const value = computed({
get() { get() {
return $props.modelValue; return props.modelValue;
}, },
set(value) { set(value) {
emit('update:modelValue', value); emit('update:modelValue', value);
@ -41,7 +41,7 @@ const value = computed({
}); });
const hover = ref(false); const hover = ref(false);
const styleAttrs = computed(() => { const styleAttrs = computed(() => {
return $props.isOutlined return props.isOutlined
? { dense: true, outlined: true, rounded: true } ? { dense: true, outlined: true, rounded: true }
: {}; : {};
}); });
@ -88,9 +88,7 @@ const inputRules = [
<template #append> <template #append>
<slot v-if="$slots.append && !$attrs.disabled" name="append" /> <slot v-if="$slots.append && !$attrs.disabled" name="append" />
<QIcon <QIcon
v-if=" v-if="hover && value && !$attrs.disabled && props.clearable"
hover && value && !$attrs.disabled && $props.clearable
"
name="close" name="close"
size="xs" size="xs"
@click=" @click="

View File

@ -0,0 +1,44 @@
<script setup>
const props = defineProps({
clickable: { type: Boolean, default: true },
rounded: { type: Boolean, default: true }
});
const emit = defineEmits(['click']);
const handleClick = () => {
if (props.clickable) {
emit('click');
}
};
</script>
<template>
<QItem
v-bind="$attrs"
v-ripple="clickable"
:clickable="clickable"
class="full-width row items-center justify-between card no-border-radius bg-white"
:class="{ 'cursor-pointer': clickable, 'no-radius': !rounded }"
@click="handleClick()"
>
<QItemSection class="no-padding">
<div class="row no-wrap">
<slot name="prepend" />
<div class="column full-width">
<slot name="content" />
</div>
</div>
</QItemSection>
<QItemSection class="no-padding" side>
<slot name="actions" />
</QItemSection>
</QItem>
</template>
<style lang="scss" scoped>
.card {
border-bottom: 1px solid $gray-light;
padding: 20px;
}
</style>

View File

@ -0,0 +1,152 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { currency, formatDateTitle } from 'src/lib/filters.js';
import VnImg from 'src/components/ui/VnImg.vue';
defineProps({
ticket: {
type: Object,
default: () => ({})
},
rows: {
type: Array,
default: () => []
}
});
const { t } = useI18n();
const lineDiscountSubtotal = line => {
return line.quantity * line.price;
};
const lineSubtotal = line =>
lineDiscountSubtotal(line) * ((100 - line.discount) / 100);
</script>
<template>
<QCard class="vn-w-sm" style="padding: 32px">
<QCardSection class="no-padding q-mb-md">
<div class="text-h6">#{{ ticket.id }}</div>
</QCardSection>
<QCardSection class="no-padding q-mb-md q-gutter-y-xs">
<div class="text-subtitle1 text-bold">
{{ t('shippingInformation') }}
</div>
<div>
{{ t('preparation') }}
{{ formatDateTitle(ticket.shipped) }}
</div>
<div>
{{ t('delivery') }}
{{ formatDateTitle(ticket.landed) }}
</div>
<div>
{{ t(ticket.method != 'PICKUP' ? 'agency' : 'warehouse') }}
{{ ticket.agency }}
</div>
</QCardSection>
<QCardSection class="no-padding q-mb-md q-gutter-y-xs">
<div class="text-subtitle1 text-bold">
{{ t('deliveryAddress') }}
</div>
<div>{{ ticket.nickname }}</div>
<div>{{ ticket.street }}</div>
<div>
{{ ticket.postalCode }} {{ ticket.city }} ({{
ticket.province
}})
</div>
</QCardSection>
<QCardSection
class="no-padding q-mb-md text-subtitle1 text-bold column"
>
<span class="text-right">
{{ t('total') }} {{ currency(ticket.taxBase) }}
</span>
<span class="text-right">
{{ t('totalTax') }} {{ currency(ticket.total) }}
</span>
</QCardSection>
<QSeparator inset />
<QList v-for="row in rows" :key="row.itemFk">
<QItem>
<QItemSection avatar>
<VnImg
storage="catalog"
size="200x200"
:id="row.image"
rounded
/>
</QItemSection>
<QItemSection>
<QItemLabel lines="1">
{{ row.concept }}
</QItemLabel>
<QItemLabel lines="1" caption>
{{ row.value5 }} {{ row.value6 }} {{ row.value7 }}
</QItemLabel>
<QItemLabel lines="1">
{{ row.quantity }} x {{ currency(row.price) }}
</QItemLabel>
</QItemSection>
<QItemSection side class="total">
<QItemLabel>
<span class="discount" v-if="row.discount">
{{ currency(lineDiscountSubtotal(row)) }} -
{{ currency(row.discount) }} =
</span>
{{ currency(lineSubtotal(row)) }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QCard>
</template>
<i18n lang="yaml">
en-US:
shippingInformation: Shipping Information
preparation: Preparation
delivery: Delivery
agency: Agency
warehouse: Store
deliveryAddress: Delivery address
total: Total
totalTax: Total + IVA
es-ES:
shippingInformation: Datos de envío
preparation: Preparación
delivery: Entrega
agency: Agencia
warehouse: Almacén
deliveryAddress: Dirección de entrega
total: Total
totalTax: Total + IVA
ca-ES:
shippingInformation: Dades d'enviament
preparation: Preparació
delivery: Lliurament
agency: Agència
warehouse: Magatzem
deliveryAddress: Adreça de lliurament
total: Total
totalTax: Total + IVA
fr-FR:
shippingInformation: Informations sur la livraison
preparation: Préparation
delivery: Livraison
warehouse: Entrepôt
deliveryAddress: Adresse de livraison
total: Total
totalTax: Total + IVA
pt-PT:
shippingInformation: Dados de envio
preparation: Preparação
delivery: Entrega
agency: Agência
warehouse: Armazém
deliveryAddress: Endereço de entrega
total: Total
totalTax: Total + IVA
</i18n>

View File

@ -1,8 +1,8 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { appStore } from 'stores/app'; import { useAppStore } from 'stores/app';
const $props = defineProps({ const props = defineProps({
baseURL: { baseURL: {
type: String, type: String,
default: null default: null
@ -23,23 +23,27 @@ const $props = defineProps({
id: { id: {
type: Number, type: Number,
required: true required: true
},
rounded: {
type: Boolean,
default: false
} }
}); });
const app = appStore(); const app = useAppStore();
const show = ref(false); const show = ref(false);
const url = computed(() => { const url = computed(() => {
return `${$props.baseURL ?? app.imageUrl}/${$props.storage}/${$props.size}/${$props.id}`; return `${props.baseURL ?? app.imageUrl}/${props.storage}/${props.size}/${props.id}`;
}); });
</script> </script>
<template> <template>
<QImg <QImg
:class="{ zoomIn: $props.zoomSize }" :class="{ zoomIn: props.zoomSize, rounded: props.rounded }"
:src="url" :src="url"
v-bind="$attrs" v-bind="$attrs"
@click="show = !show" @click="show = !show"
spinner-color="primary" spinner-color="primary"
/> />
<QDialog v-model="show" v-if="$props.zoomSize"> <QDialog v-model="show" v-if="props.zoomSize">
<QImg <QImg
:src="url" :src="url"
size="full" size="full"

View File

@ -0,0 +1,41 @@
<script setup>
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
noDataLabel: {
type: String,
default: ''
},
hideBottom: {
type: Boolean,
default: true
},
rowsPerPageOptions: {
type: Array,
default: () => [0]
}
});
</script>
<template>
<QTable
v-bind="$attrs"
:no-data-label="props.noDataLabel || t('noInvoicesFound')"
:hide-bottom="props.hideBottom"
:rows-per-page-options="props.rowsPerPageOptions"
table-header-class="vntable-header-default"
>
<template v-for="(_, slotName) in $slots" v-slot:[slotName]="slotProps">
<slot :name="slotName" v-bind="slotProps" />
</template>
</QTable>
</template>
<style lang="scss">
.vntable-header-default {
background-color: $accent !important;
color: white;
}
</style>

View File

@ -0,0 +1,38 @@
import { userStore as useUserStore } from 'stores/user';
import axios from 'axios';
import { useQuasar } from 'quasar';
export function usePrintService() {
const quasar = useQuasar();
const userStore = useUserStore();
const token = userStore.token;
function sendEmail(path, params) {
return axios.post(path, params).then(() =>
quasar.notify({
message: 'Notification sent',
type: 'positive',
icon: 'check'
})
);
}
function openReport(path, params) {
params = Object.assign(
{
access_token: token
},
params
);
const query = new URLSearchParams(params).toString();
window.open(`api/${path}?${query}`);
}
return {
sendEmail,
openReport
};
}

View File

@ -36,3 +36,9 @@ a.link {
.q-page-sticky.fixed-bottom-right { .q-page-sticky.fixed-bottom-right {
margin: 18px; margin: 18px;
} }
.no-border-radius {
border-radius: 0 !important;
}
.no-padding {
padding: 0 !important;
}

View File

@ -15,7 +15,7 @@
$primary: #1a1a1a; $primary: #1a1a1a;
$secondary: #26a69a; $secondary: #26a69a;
$accent: #8cc63f; $accent: #8cc63f;
$gray-light: #ddd;
$dark: #1d1d1d; $dark: #1d1d1d;
$dark-page: #121212; $dark-page: #121212;

56
src/i18n/ca-ES/index.js Normal file
View File

@ -0,0 +1,56 @@
export default {
date: {
days: [
'Diumenge',
'Dilluns',
'Dimarts',
'Dimecres',
'Dijous',
'Divendres',
'Dissabte'
],
daysShort: ['Dg', 'Dl', 'Dt', 'Dc', 'Dj', 'Dv', 'Ds'],
months: [
'Gener',
'Febrer',
'Març',
'Abril',
'Maig',
'Juny',
'Juliol',
'Agost',
'Setembre',
'Octubre',
'Novembre',
'Desembre'
],
monthsShort: [
'Gen',
'Feb',
'Mar',
'Abr',
'Mai',
'Jun',
'Jul',
'Ago',
'Set',
'Oct',
'Nov',
'Des'
]
},
of: 'de',
// Menu
home: 'Inici',
catalog: 'Catàleg',
pendingOrders: 'Comandes pendents',
confirmedOrders: 'Comandes confirmades',
invoices: 'Factures',
agencyPackages: 'Paquets per agència',
accountConfig: 'Configuració',
addressesList: 'Adreces',
addressDetails: 'Configuració',
checkout: 'Configurar encàrrec',
//
orderLoadedIntoBasket: 'Comanda carregada a la cistella!'
};

View File

@ -37,7 +37,7 @@ export default {
'November', 'November',
'December' 'December'
], ],
shortMonths: [ monthsShort: [
'Jan', 'Jan',
'Feb', 'Feb',
'Mar', 'Mar',
@ -56,6 +56,17 @@ export default {
// menu // menu
home: 'Home', home: 'Home',
catalog: 'Catalog', catalog: 'Catalog',
pendingOrders: 'Pending orders',
confirmedOrders: 'Confirmed orders',
invoices: 'Invoices',
agencyPackages: 'Bundles by agency',
accountConfig: 'Configuration',
addressesList: 'Addresses',
addressDetails: 'Configuration',
checkout: 'Configure order',
//
orderLoadedIntoBasket: 'Order loaded into basket!',
orders: 'Orders', orders: 'Orders',
order: 'Pending order', order: 'Pending order',
ticket: 'Order', ticket: 'Order',
@ -76,5 +87,6 @@ export default {
addressEdit: 'Edit address', addressEdit: 'Edit address',
dataSaved: 'Data saved', dataSaved: 'Data saved',
save: 'Save', save: 'Save',
cancel: 'Cancel' cancel: 'Cancel',
of: 'of'
}; };

View File

@ -46,7 +46,7 @@ export default {
'Noviembre', 'Noviembre',
'Diciembre' 'Diciembre'
], ],
shortMonths: [ monthsShort: [
'Ene', 'Ene',
'Feb', 'Feb',
'Mar', 'Mar',
@ -65,6 +65,17 @@ export default {
// Menu // Menu
home: 'Inicio', home: 'Inicio',
catalog: 'Catálogo', catalog: 'Catálogo',
pendingOrders: 'Pedidos pendientes',
confirmedOrders: 'Pedidos confirmados',
invoices: 'Facturas',
agencyPackages: 'Bultos por agencia',
accountConfig: 'Configuración',
addressesList: 'Direcciones',
addressDetails: 'Configuración',
checkout: 'Configurar pedido',
//
orderLoadedIntoBasket: '¡Pedido cargado en la cesta!',
orders: 'Pedidos', orders: 'Pedidos',
order: 'Pedido pendiente', order: 'Pedido pendiente',
ticket: 'Pedido', ticket: 'Pedido',
@ -94,5 +105,6 @@ export default {
addressEdit: 'Editar dirección', addressEdit: 'Editar dirección',
dataSaved: 'Datos guardados', dataSaved: 'Datos guardados',
save: 'Guardar', save: 'Guardar',
cancel: 'Cancelar' cancel: 'Cancelar',
of: 'de'
}; };

56
src/i18n/fr-FR/index.js Normal file
View File

@ -0,0 +1,56 @@
export default {
date: {
days: [
'Dimanche',
'Lundi',
'Mardi',
'Mercredi',
'Jeudi',
'Vendredi',
'Samedi'
],
daysShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
months: [
'Janvier',
'Février',
'Mars',
'Avril',
'Mai',
'Juin',
'Juillet',
'Août',
'Septembre',
'Octobre',
'Novembre',
'Décembre'
],
monthsShort: [
'Jan',
'Fév',
'Mar',
'Avr',
'Mai',
'Juin',
'Juil',
'Aoû',
'Sep',
'Oct',
'Nov',
'Déc'
]
},
of: 'de',
// Menu
home: 'Accueil',
catalog: 'Catalogue',
pendingOrders: 'Commandes en attente',
confirmedOrders: 'Commandes confirmées',
invoices: 'Factures',
agencyPackages: 'Liste par agence',
accountConfig: 'Configuration',
addressesList: 'Adresses',
addressDetails: 'Configuration',
checkout: "Définissez l'ordre",
//
orderLoadedIntoBasket: 'Commande chargée dans le panier!'
};

View File

@ -1,7 +1,13 @@
import enUS from './en-US' import enUS from './en-US';
import esES from './es-ES' import esES from './es-ES';
import frFR from './fr-FR';
import ptPT from './pt-PT';
import caES from './ca-ES';
export default { export default {
'en-US': enUS, 'en-US': enUS,
'es-ES': esES 'es-ES': esES,
} 'fr-FR': frFR,
'pt-PT': ptPT,
'ca-ES': caES
};

57
src/i18n/pt-PT/index.js Normal file
View File

@ -0,0 +1,57 @@
export default {
date: {
days: [
'Domingo',
'Segunda-feira',
'Terça-feira',
'Quarta-feira',
'Quinta-feira',
'Sexta-feira',
'Sábado'
],
daysShort: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'],
months: [
'Janeiro',
'Fevereiro',
'Março',
'Abril',
'Maio',
'Junho',
'Julho',
'Agosto',
'Setembro',
'Outubro',
'Novembro',
'Dezembro'
],
monthsShort: [
'Jan',
'Fev',
'Mar',
'Abr',
'Mai',
'Jun',
'Jul',
'Ago',
'Set',
'Out',
'Nov',
'Dez'
]
},
of: 'de',
// Menu
home: 'Principio',
catalog: 'Catálogo',
pendingOrders: 'Pedidos pendentes',
confirmedOrders: 'Pedidos confirmados',
invoices: 'Facturas',
agencyPackages: 'Bultos por agencia',
accountConfig: 'Configuração',
addressesList: 'Moradas',
addressDetails: 'Configuração',
checkout: 'Configurar encomenda',
//
orderLoadedIntoBasket: 'Pedido carregado na cesta!'
};

View File

@ -1,73 +1,114 @@
import { date as qdate, format } from 'quasar' import { i18n } from 'src/boot/i18n';
const { pad } = format import { date as qdate, format } from 'quasar';
const { pad } = format;
export function currency (val) { export function currency(val) {
return typeof val === 'number' ? val.toFixed(2) + '€' : val return typeof val === 'number' ? val.toFixed(2) + '€' : val;
} }
export function date (val, format) { export function date(val, format) {
if (val == null) return val if (val == null) return val;
if (!(val instanceof Date)) { if (!(val instanceof Date)) {
val = new Date(val) val = new Date(val);
} }
return qdate.formatDate(val, format, window.i18n.tm('date')) return qdate.formatDate(val, format, i18n.global.tm('date'));
} }
export function relDate (val) { export const formatDate = (timeStamp, format = 'YYYY-MM-DD') => {
if (val == null) return val if (!timeStamp) return '';
const { messages, locale } = i18n.global;
return qdate.formatDate(timeStamp, format, {
days: messages.value[locale.value].date.days,
months: messages.value[locale.value].date.months,
daysShort: messages.value[locale.value].date.daysShort,
monthsShort: messages.value[locale.value].date.monthsShort
});
};
/**
* @param {Date} timeStamp - La marca de tiempo que se va a formatear. Si no se proporciona, la función devolverá una cadena vacía.
* @param {Object} options - Un objeto que contiene las opciones de formato.
* @param {boolean} options.showTime - Indica si se debe mostrar la hora en el formato de la fecha.
* @param {boolean} options.showSeconds - Indica si se deben mostrar los segundos en el formato de la hora. Solo se aplica si showTime es true.
* @param {boolean} options.shortDay - Indica si se debe usar una versión corta del día (por ejemplo, "Mon" en lugar de "Monday").
* @returns {string} La fecha formateada como un título.
*/
export const formatDateTitle = (
timeStamp,
options = { showTime: false, showSeconds: false, shortDay: false }
) => {
if (!timeStamp) return '';
const { t } = i18n.global;
const timeFormat = options.showTime
? options.showSeconds
? ` [${t('at')}] hh:mm:ss`
: ` [${t('at')}] hh:mm`
: '';
const day = options.shortDay ? 'dd' : 'dddd';
const formatString = `${day}, D [${t('of')}] MMMM [${t('of')}] YYYY${timeFormat}`;
const formattedString = formatDate(timeStamp, formatString);
return formattedString;
};
export function relDate(val) {
if (val == null) return val;
if (!(val instanceof Date)) { if (!(val instanceof Date)) {
val = new Date(val) val = new Date(val);
} }
const dif = qdate.getDateDiff(new Date(), val, 'days') const dif = qdate.getDateDiff(new Date(), val, 'days');
let day let day;
switch (dif) { switch (dif) {
case 0: case 0:
day = 'today' day = 'today';
break break;
case 1: case 1:
day = 'yesterday' day = 'yesterday';
break break;
case -1: case -1:
day = 'tomorrow' day = 'tomorrow';
break break;
} }
if (day) { if (day) {
day = window.i18n.t(day) day = i18n.global.t(day);
} else { } else {
if (dif > 0 && dif <= 7) { if (dif > 0 && dif <= 7) {
day = qdate.formatDate(val, 'ddd', window.i18n.tm('date')) day = qdate.formatDate(val, 'ddd', i18n.global.tm('date'));
} else { } else {
day = qdate.formatDate(val, 'ddd, MMMM Do', window.i18n.tm('date')) day = qdate.formatDate(val, 'ddd, MMMM Do', i18n.global.tm('date'));
} }
} }
return day return day;
} }
export function relTime (val) { export function relTime(val) {
if (val == null) return val if (val == null) return val;
if (!(val instanceof Date)) { if (!(val instanceof Date)) {
val = new Date(val) val = new Date(val);
} }
return relDate(val) + ' ' + qdate.formatDate(val, 'H:mm:ss') return relDate(val) + ' ' + qdate.formatDate(val, 'H:mm:ss');
} }
export function elapsedTime (val) { export function elapsedTime(val) {
if (val == null) return val if (val == null) return val;
if (!(val instanceof Date)) { if (!(val instanceof Date)) {
val = new Date(val) val = new Date(val);
} }
const now = new Date().getTime() const now = new Date().getTime();
val = Math.floor((now - val.getTime()) / 1000) val = Math.floor((now - val.getTime()) / 1000);
const hours = Math.floor(val / 3600) const hours = Math.floor(val / 3600);
val -= hours * 3600 val -= hours * 3600;
const minutes = Math.floor(val / 60) const minutes = Math.floor(val / 60);
val -= minutes * 60 val -= minutes * 60;
const seconds = val const seconds = val;
return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)}` return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)}`;
} }

View File

@ -50,7 +50,7 @@ onMounted(() => fetchLanguagesSql());
icon="location_on" icon="location_on"
rounded rounded
no-caps no-caps
:to="{ name: 'AddressesList' }" :to="{ name: 'addressesList' }"
/> />
<QBtn <QBtn
:label="t('changePassword')" :label="t('changePassword')"

View File

@ -32,7 +32,7 @@ watch(
async val => await getProvinces(val) async val => await getProvinces(val)
); );
const goBack = () => router.push({ name: 'AddressesList' }); const goBack = () => router.push({ name: 'addressesList' });
const getCountries = async () => { const getCountries = async () => {
countriesOptions.value = await jApi.query( countriesOptions.value = await jApi.query(

View File

@ -3,6 +3,8 @@ import { useI18n } from 'vue-i18n';
import { ref, onMounted, inject } from 'vue'; import { ref, onMounted, inject } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import CardList from 'src/components/ui/CardList.vue';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useVnConfirm } from 'src/composables/useVnConfirm.js'; import { useVnConfirm } from 'src/composables/useVnConfirm.js';
@ -17,7 +19,7 @@ const defaultAddress = ref(null);
const clientId = ref(null); const clientId = ref(null);
const goToAddressDetails = (id = 0) => const goToAddressDetails = (id = 0) =>
router.push({ name: 'AddressDetails', params: { id } }); router.push({ name: 'addressDetails', params: { id } });
const getDefaultAddress = async () => { const getDefaultAddress = async () => {
try { try {
@ -93,42 +95,33 @@ onMounted(async () => {
no-caps no-caps
/> />
</Teleport> </Teleport>
<QPage class="column items-center"> <QPage class="vn-w-sm">
<QList <QList class="rounded-borders shadow-1 shadow-transition" separator>
class="full-width rounded-borders shadow-1 shadow-transition" <CardList
style="max-width: 544px"
separator
>
<QItem
v-for="(address, index) in addresses" v-for="(address, index) in addresses"
:key="index" :key="index"
clickable :rounded="false"
v-ripple
tag="label" tag="label"
class="full-width row items-center justify-between address-item"
style="padding: 20px"
> >
<QItemSection> <template #prepend>
<div class="row">
<QRadio <QRadio
v-model="defaultAddress" v-model="defaultAddress"
:val="address.id" :val="address.id"
class="q-mr-sm" class="q-mr-sm"
@update:model-value="changeDefaultAddress" @update:model-value="changeDefaultAddress"
/> />
<div> </template>
<QItemLabel class="text-bold q-mb-sm"> <template #content>
<span class="text-bold q-mb-sm">
{{ address.nickname }} {{ address.nickname }}
</QItemLabel> </span>
<QItemLabel>{{ address.street }}</QItemLabel> <span>{{ address.street }}</span>
<QItemLabel> <span>
{{ address.postalCode }}, {{ address.postalCode }},
{{ address.city }} {{ address.city }}
</QItemLabel> </span>
</div> </template>
</div> <template #actions>
</QItemSection>
<QItemSection class="actions-wrapper" side>
<QBtn <QBtn
icon="delete" icon="delete"
flat flat
@ -147,25 +140,12 @@ onMounted(async () => {
rounded rounded
@click.stop="goToAddressDetails(address.id)" @click.stop="goToAddressDetails(address.id)"
/> />
</QItemSection> </template>
</QItem> </CardList>
</QList> </QList>
</QPage> </QPage>
</template> </template>
<style lang="scss" scoped>
.address-item {
.actions-wrapper {
visibility: hidden;
}
&:hover {
.actions-wrapper {
visibility: visible;
}
}
}
</style>
<i18n lang="yaml"> <i18n lang="yaml">
en-US: en-US:
addAddress: Add address addAddress: Add address

View File

@ -2,6 +2,8 @@
import { ref, inject, onMounted, computed } from 'vue'; import { ref, inject, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnTable from 'src/components/ui/VnTable.vue';
const jApi = inject('jApi'); const jApi = inject('jApi');
const { t } = useI18n(); const { t } = useI18n();
@ -53,32 +55,15 @@ onMounted(() => getPackages());
<template> <template>
<QPage class="flex justify-center q-pa-md"> <QPage class="flex justify-center q-pa-md">
<QTable <VnTable
:columns="columns" :columns="columns"
:rows="packages" :rows="packages"
:loading="loading" :loading="loading"
class="q-mt-lg" style="height: max-content; max-width: 100%"
style="max-width: 100%; height: max-content" />
table-header-class="packages-table-header"
hide-bottom
>
<template #body-cell-id="{ row }">
<QTd auto-width @click.stop>
<QBtn flat color="blue">{{ row.id }}</QBtn>
<ItemDescriptorProxy :id="row.id" />
</QTd>
</template>
</QTable>
</QPage> </QPage>
</template> </template>
<style lang="scss">
.packages-table-header {
background-color: $accent !important;
color: white;
}
</style>
<i18n lang="yaml"> <i18n lang="yaml">
en-US: en-US:
agency: Agency agency: Agency

View File

@ -0,0 +1 @@
<template>Basket view</template>

View File

@ -343,19 +343,23 @@
</style> </style>
<script> <script>
import { date, currency } from 'src/lib/filters.js'; import { date, currency, formatDate } from 'src/lib/filters.js';
import { date as qdate } from 'quasar';
import axios from 'axios'; import axios from 'axios';
import { useAppStore } from 'stores/app';
const CancelToken = axios.CancelToken; const CancelToken = axios.CancelToken;
export default { export default {
name: 'HederaCatalog', name: 'HederaCatalog',
setup() {
const appStore = useAppStore();
return { appStore };
},
data() { data() {
return { return {
uid: 0, uid: 0,
search: '', search: '',
orderDate: qdate.formatDate(new Date(), 'YYYY/MM/DD'), orderDate: formatDate(new Date(), 'YYYY/MM/DD'),
category: null, category: null,
categories: [], categories: [],
type: null, type: null,
@ -446,7 +450,7 @@ export default {
if (!value) return; if (!value) return;
const res = await this.$jApi.execQuery( const res = await this.$jApi.execQuery(
`CALL myBasket_getAvailable; `CALL myOrder_getAvailable(${this.appStore.basketOrderId});
SELECT DISTINCT t.id, l.name SELECT DISTINCT t.id, l.name
FROM vn.item i FROM vn.item i
JOIN vn.itemType t ON t.id = i.typeFk JOIN vn.itemType t ON t.id = i.typeFk

View File

@ -0,0 +1 @@
<template>Checkout</template>

View File

@ -1,183 +0,0 @@
<template>
<Teleport :to="$actions">
<QSelect
v-model="year"
:options="years"
color="white"
dark
standout
dense
rounded
/>
</Teleport>
<div class="vn-w-sm">
<div
v-if="!invoices?.length"
class="text-subtitle1 text-center text-grey-7 q-pa-md"
>
{{ $t('noInvoicesFound') }}
</div>
<QCard v-if="invoices?.length">
<QTable
:columns="columns"
:pagination="pagination"
:rows="invoices"
row-key="id"
hide-header
hide-bottom
>
<template v-slot:body="props">
<QTr :props="props">
<QTd key="ref" :props="props">
{{ props.row.ref }}
</QTd>
<QTd key="issued" :props="props">
{{ date(props.row.issued, 'ddd, MMMM Do') }}
</QTd>
<QTd key="amount" :props="props">
{{ currency(props.row.amount) }}
</QTd>
<QTd key="hasPdf" :props="props">
<QBtn
v-if="props.row.hasPdf"
icon="download"
:title="$t('downloadInvoicePdf')"
:href="invoiceUrl(props.row.id)"
target="_blank"
flat
round
/>
<QIcon
v-else
name="warning"
:title="$t('notDownloadable')"
color="warning"
size="24px"
/>
</QTd>
</QTr>
</template>
</QTable>
</QCard>
</div>
</template>
<script>
import { date, currency } from 'src/lib/filters.js';
export default {
name: 'OrdersPendingIndex',
data() {
const curYear = new Date().getFullYear();
const years = [];
for (let year = curYear - 5; year <= curYear; year++) {
years.push(year);
}
return {
columns: [
{ name: 'ref', label: 'serial', field: 'ref', align: 'left' },
{
name: 'issued',
label: 'issued',
field: 'issued',
align: 'left'
},
{ name: 'amount', label: 'amount', field: 'amount' },
{
name: 'hasPdf',
label: 'download',
field: 'hasPdf',
align: 'center'
}
],
pagination: {
rowsPerPage: 0
},
year: curYear,
years,
invoices: null
};
},
async mounted() {
await this.loadData();
},
watch: {
async year() {
await this.loadData();
}
},
methods: {
date,
currency,
async loadData() {
const params = {
from: new Date(this.year, 0),
to: new Date(this.year, 11, 31, 23, 59, 59)
};
this._invoices = await this.$jApi.query(
`SELECT id, ref, issued, amount, hasPdf
FROM myInvoice
WHERE issued BETWEEN #from AND #to
ORDER BY issued DESC
LIMIT 500`,
params
);
},
invoiceUrl(id) {
return (
'?' +
new URLSearchParams({
srv: 'rest:dms/invoice',
invoice: id,
access_token: this.$user.token
}).toString()
);
}
}
};
</script>
<i18n lang="yaml">
en-US:
noInvoicesFound: No invoices found
serial: Serial
issued: Date
amount: Import
downloadInvoicePdf: Download invoice PDF
notDownloadable: Not available for download, request the invoice to your salesperson
es-ES:
noInvoicesFound: No se han encontrado facturas
serial: Serie
issued: Fecha
amount: Importe
downloadInvoicePdf: Descargar factura en PDF
notDownloadable: No disponible para descarga, solicita la factura a tu comercial
ca-ES:
noInvoicesFound: No s'han trobat factures
serial: Sèrie
issued: Data
amount: Import
downloadInvoicePdf: Descarregar PDF
notDownloadable: No disponible per cescarrega, sol·licita la factura al teu comercial
fr-FR:
noInvoicesFound: Aucune facture trouvée
serial: Série
issued: Date
amount: Montant
downloadInvoicePdf: Télécharger le PDF
notDownloadable: Non disponible en téléchargement, demander la facture à votre commercial
pt-PT:
noInvoicesFound: Nenhuma fatura encontrada
serial: Serie
issued: Data
amount: Importe
downloadInvoicePdf: Baixar PDF
notDownloadable: Não disponível para download, solicite a fatura ao seu comercial
</i18n>

View File

@ -0,0 +1,159 @@
<script setup>
import { ref, onMounted, inject, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import VnTable from 'src/components/ui/VnTable.vue';
import { currency, formatDate } from 'src/lib/filters.js';
import { usePrintService } from 'src/composables/usePrintService';
// import { date as qdate } from 'quasar';
const { t } = useI18n();
const jApi = inject('jApi');
const { openReport } = usePrintService();
const currentYear = ref(Date.vnNew().getFullYear());
const years = ref([]);
const invoices = ref([]);
const columns = computed(() => [
{ name: 'ref', label: t('invoice'), field: 'ref', align: 'left' },
{
name: 'issued',
label: t('issued'),
field: 'issued',
align: 'left',
format: val => formatDate(val, 'D MMM YYYY')
},
{
name: 'amount',
label: t('amount'),
field: 'amount',
format: val => currency(val)
},
{
name: 'hasPdf',
field: 'hasPdf',
align: 'center'
}
]);
const fetchInvoices = async () => {
const params = {
from: new Date(currentYear.value, 0),
to: new Date(currentYear.value, 11, 31, 23, 59, 59)
};
invoices.value = await jApi.query(
`SELECT id, ref, issued, amount, hasPdf
FROM myInvoice
WHERE issued BETWEEN #from AND #to
ORDER BY issued DESC
LIMIT 100`,
params
);
};
onMounted(async () => {
await fetchInvoices();
for (let year = currentYear.value - 5; year <= currentYear.value; year++) {
years.value.push(year);
}
});
</script>
<template>
<Teleport :to="$actions">
<QSelect
v-model="currentYear"
:options="years"
color="white"
dark
standout
dense
rounded
@update:model-value="fetchInvoices()"
/>
</Teleport>
<div class="vn-w-sm">
<VnTable
:columns="columns"
:rows="invoices"
:hide-header="!invoices.length"
>
<template #body-cell-hasPdf="{ row }">
<QTd
auto-width
@click.stop
class="flex full-width justify-center items-center"
>
<QBtn
v-if="row.hasPdf"
icon="download"
target="_blank"
flat
round
@click="openReport(`InvoiceOuts/${row.id}/download`)"
>
<QTooltip>
{{ t('downloadInvoicePdf') }}
</QTooltip>
</QBtn>
<QIcon
v-else
name="warning"
:title="t('notDownloadable')"
color="warning"
size="sm"
>
<QTooltip>
{{ t('requestTheInvoiceToComercial') }}
</QTooltip>
</QIcon>
</QTd>
</template>
</VnTable>
</div>
</template>
<i18n lang="yaml">
en-US:
noInvoicesFound: No invoices found
invoice: Invoice
issued: Date
amount: Import
downloadInvoicePdf: Download invoice PDF
notDownloadable: Not available for download, request the invoice to your salesperson
requestTheInvoiceToComercial: Request the invoice to your salesperson
es-ES:
noInvoicesFound: No se han encontrado facturas
invoice: Factura
issued: Fecha
amount: Importe
downloadInvoicePdf: Descargar factura en PDF
notDownloadable: No disponible para descarga, solicita la factura a tu comercial
requestTheInvoiceToComercial: Solicita la factura a tu comercial
ca-ES:
noInvoicesFound: No s'han trobat factures
invoice: Factura
issued: Data
amount: Import
downloadInvoicePdf: Descarregar PDF
notDownloadable: No disponible per cescarrega, sol·licita la factura al teu comercial
requestTheInvoiceToComercial: Sol·licita la factura al teu comercial
fr-FR:
noInvoicesFound: Aucune facture trouvée
invoice: Facture
issued: Date
amount: Montant
downloadInvoicePdf: Télécharger le PDF
notDownloadable: Non disponible en téléchargement, demander la facture à votre commercial
requestTheInvoiceToComercial: Demander la facture à votre commercial
pt-PT:
noInvoicesFound: Nenhuma fatura encontrada
invoice: Fatura
issued: Data
amount: Importe
downloadInvoicePdf: Baixar PDF
notDownloadable: Não disponível para download, solicite a fatura ao seu comercial
requestTheInvoiceToComercial: Solicite a fatura ao seu comercial
</i18n>

View File

@ -1,70 +1,135 @@
<script setup>
import { ref, onMounted, inject } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardList from 'src/components/ui/CardList.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import { currency, formatDateTitle } from 'src/lib/filters.js';
import { tpvStore } from 'stores/tpv';
const { t } = useI18n();
const route = useRoute();
const jApi = inject('jApi');
const showAmountToPayDialog = ref(null);
const amountToPay = ref(null);
const orders = ref(null);
const debt = ref(0);
const tpv = tpvStore();
onMounted(async () => {
await tpv.check(route);
orders.value = await jApi.query('CALL myTicket_list(NULL, NULL)');
debt.value = await jApi.getValue('SELECT -myClient_getDebt(NULL)');
});
const onPayClick = async () => {
showAmountToPayDialog.value = true;
if (debt.value <= 0) {
amountToPay.value = -debt.value;
}
};
const onConfirmPay = async () => {
if (amountToPay.value) {
const amount = amountToPay.value.toString().replace('.', ',');
amountToPay.value = parseFloat(amount);
await tpv.pay(amountToPay.value);
}
};
</script>
<template> <template>
<Teleport :to="$actions"> <Teleport :to="$actions">
<div class="balance"> <div class="balance">
<span class="label">{{ $t('balance') }}</span> <span class="label">{{ t('balance') }}</span>
<span class="amount" :class="{ negative: debt < 0 }"> <span class="amount" :class="{ negative: debt < 0 }">
{{ currency(debt || 0) }} {{ currency(debt || 0) }}
</span> </span>
<QIcon <QIcon name="info" class="info" size="sm">
name="info" <QTooltip max-width="450px">
:title="$t('paymentInfo')" {{ t('paymentInfo') }}
class="info" </QTooltip>
size="24px" </QIcon>
/>
</div> </div>
<QBtn <QBtn
icon="payments" icon="payments"
:label="$t('makePayment')" :label="t('makePayment')"
@click="onPayClick()" @click="onPayClick()"
rounded rounded
no-caps no-caps
/> >
<QTooltip>
{{ t('makePayment') }}
</QTooltip>
</QBtn>
<QBtn <QBtn
to="/ecomerce/basket" :to="{ name: 'basket' }"
icon="shopping_cart" icon="shopping_cart"
:label="$t('shoppingCart')" :label="t('shoppingCart')"
rounded rounded
no-caps no-caps
/> >
<QTooltip>
{{ t('shoppingCart') }}
</QTooltip>
</QBtn>
</Teleport> </Teleport>
<div class="vn-w-sm"> <QPage class="vn-w-sm">
<div <div
v-if="!orders?.length" v-if="!orders?.length"
class="text-subtitle1 text-center text-grey-7 q-pa-md" class="text-subtitle1 text-center text-grey-7 q-pa-md"
> >
{{ $t('noOrdersFound') }} {{ t('noOrdersFound') }}
</div> </div>
<QCard v-if="orders?.length"> <QList v-if="orders?.length">
<QList bordered separator padding> <CardList
<QItem
v-for="order in orders" v-for="order in orders"
:key="order.id" :key="order.id"
:to="`ticket/${order.id}`" :to="`ticket/${order.id}`"
clickable tag="label"
v-ripple
> >
<QItemSection> <template #content>
<QItemLabel> <QItemLabel
{{ date(order.landed, 'ddd, MMMM Do') }} class="full-width text-bold q-mb-sm flex row justify-between"
>
<span>{{ formatDateTitle(order.landed) }}</span>
<span>{{ currency(order.total) }}</span>
</QItemLabel> </QItemLabel>
<QItemLabel caption>#{{ order.id }}</QItemLabel> <QItemLabel>#{{ order.id }}</QItemLabel>
<QItemLabel caption>{{ order.nickname }}</QItemLabel> <QItemLabel>{{ order.nickname }}</QItemLabel>
<QItemLabel caption>{{ order.agency }}</QItemLabel> <QItemLabel>{{ order.agency }}</QItemLabel>
</QItemSection> </template>
<QItemSection side top> {{ order.total }} </QItemSection> </CardList>
</QItem>
</QList> </QList>
</QCard>
<QPageSticky> <QPageSticky>
<QBtn <QBtn
fab fab
icon="add_shopping_cart" icon="add_shopping_cart"
color="accent" color="accent"
to="/ecomerce/catalog" :to="{ name: 'catalog' }"
:title="$t('startOrder')" :title="t('startOrder')"
/> />
</QPageSticky> </QPageSticky>
</div> <VnConfirm
v-model="showAmountToPayDialog"
:message="t('amountToPay')"
:promise="onConfirmPay"
>
<template #customHTML>
<VnInput
v-model="amountToPay"
:clearable="false"
class="full-width"
/>
</template>
</VnConfirm>
</QPage>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -92,50 +157,6 @@
} }
</style> </style>
<script>
import { date, currency } from 'src/lib/filters.js';
import { tpvStore } from 'stores/tpv';
export default {
name: 'OrdersPendingIndex',
data() {
return {
orders: null,
debt: 0,
tpv: tpvStore()
};
},
async mounted() {
await this.tpv.check(this.$route);
this.orders = await this.$jApi.query('CALL myTicket_list(NULL, NULL)');
this.debt = await this.$jApi.getValue('SELECT -myClient_getDebt(NULL)');
},
methods: {
date,
currency,
async onPayClick() {
let amount = -this.debt;
amount = amount <= 0 ? null : amount;
let defaultAmountStr = '';
if (amount !== null) {
defaultAmountStr = amount;
}
amount = prompt(this.$t('amountToPay'), defaultAmountStr);
if (amount != null) {
amount = parseFloat(amount.replace(',', '.'));
await this.tpv.pay(amount);
}
}
}
};
</script>
<i18n lang="yaml"> <i18n lang="yaml">
en-US: en-US:
startOrder: Start order startOrder: Start order
@ -148,6 +169,7 @@ en-US:
disregards future orders. For get your order shipped, this amount must be disregards future orders. For get your order shipped, this amount must be
equal to or greater than 0. If you want to make a down payment, click the equal to or greater than 0. If you want to make a down payment, click the
payment button, delete the suggested amount and enter the amount you want. payment button, delete the suggested amount and enter the amount you want.
amountToPay: 'Amount to pay (€):'
es-ES: es-ES:
startOrder: Empezar pedido startOrder: Empezar pedido
noOrdersFound: No se encontrado pedidos noOrdersFound: No se encontrado pedidos
@ -160,6 +182,7 @@ es-ES:
esta cantidad debe ser igual o mayor que 0. Si quieres realizar una entrega a esta cantidad debe ser igual o mayor que 0. Si quieres realizar una entrega a
cuenta, pulsa el botón de pago, borra la cantidad sugerida e introduce la cuenta, pulsa el botón de pago, borra la cantidad sugerida e introduce la
cantidad que desees. cantidad que desees.
amountToPay: 'Cantidad a pagar (€):'
ca-ES: ca-ES:
startOrder: Començar encàrrec startOrder: Començar encàrrec
noOrdersFound: No s'han trobat comandes noOrdersFound: No s'han trobat comandes
@ -172,6 +195,7 @@ ca-ES:
enviat, aquesta quantitat ha de ser igual o més gran que 0. Si vols fer un enviat, aquesta quantitat ha de ser igual o més gran que 0. Si vols fer un
lliurament a compte, prem el botó de pagament, esborra la quantitat suggerida lliurament a compte, prem el botó de pagament, esborra la quantitat suggerida
e introdueix la quantitat que vulguis. e introdueix la quantitat que vulguis.
amountToPay: 'Quantitat a pagar (€):'
fr-FR: fr-FR:
startOrder: Acheter startOrder: Acheter
noOrdersFound: Aucune commande trouvée noOrdersFound: Aucune commande trouvée
@ -184,6 +208,7 @@ fr-FR:
commande est expédiée, ce montant doit être égal ou supérieur à 0. Si vous commande est expédiée, ce montant doit être égal ou supérieur à 0. Si vous
voulez faire un versement, le montant suggéré effacé et entrez le montant que voulez faire un versement, le montant suggéré effacé et entrez le montant que
vous souhaitez. vous souhaitez.
amountToPay: 'Montant à payer (€):'
pt-PT: pt-PT:
startOrder: Iniciar encomenda startOrder: Iniciar encomenda
noOrdersFound: Nenhum pedido encontrado noOrdersFound: Nenhum pedido encontrado
@ -196,4 +221,5 @@ pt-PT:
quantidade deve ser igual ou superior a 0. Se queres realizar um depósito à quantidade deve ser igual ou superior a 0. Se queres realizar um depósito à
conta, clique no botão de pagamento, apague a quantidade sugerida e introduza conta, clique no botão de pagamento, apague a quantidade sugerida e introduza
a quantidade que deseje. a quantidade que deseje.
amountToPay: 'Valor a pagar (€):'
</i18n> </i18n>

View File

@ -0,0 +1,135 @@
<script setup>
import { ref, inject, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import CardList from 'src/components/ui/CardList.vue';
import { currency, formatDateTitle } from 'src/lib/filters.js';
import { useVnConfirm } from 'src/composables/useVnConfirm.js';
import useNotify from 'src/composables/useNotify.js';
import { useAppStore } from 'src/stores/app.js';
const jApi = inject('jApi');
const { t } = useI18n();
const { openConfirmationModal } = useVnConfirm();
const { notify } = useNotify();
const store = useAppStore();
const router = useRouter();
const orders = ref([]);
const getOrders = async () => {
try {
orders.value = await jApi.query(
`SELECT o.id, o.sent, o.deliveryMethodFk, o.taxableBase,
a.nickname, am.description agency
FROM myOrder o
JOIN myAddress a ON a.id = o.addressFk
JOIN vn.agencyMode am ON am.id = o.agencyModeFk
WHERE NOT o.isConfirmed
ORDER BY o.sent DESC`
);
} catch (error) {
console.error('Error getting orders:', error);
}
};
const removeOrder = async (id, index) => {
try {
await jApi.execQuery(
`START TRANSACTION;
DELETE FROM hedera.myOrder WHERE ((id = #id));
COMMIT`,
{
id
}
);
orders.value.splice(index, 1);
notify(t('dataSaved'), 'positive');
} catch (error) {
console.error('Error removing order:', error);
}
};
const loadOrder = orderId => {
store.loadIntoBasket(orderId);
router.push({ name: 'catalog' });
};
onMounted(async () => {
getOrders();
});
</script>
<template>
<Teleport :to="$actions">
<QBtn
:to="{ name: 'checkout' }"
icon="add_shopping_cart"
:label="t('newOrder')"
rounded
no-caps
>
<QTooltip>
{{ t('newOrder') }}
</QTooltip>
</QBtn>
</Teleport>
<QPage class="vn-w-sm">
<CardList
v-for="(order, index) in orders"
:key="index"
:to="{ name: 'basket', params: { id: order.id } }"
>
<template #content>
<QItemLabel class="text-bold q-mb-sm">{{
formatDateTitle(order.sent)
}}</QItemLabel>
<QItemLabel> #{{ order.id }} </QItemLabel>
<QItemLabel>{{ order.nickname }}</QItemLabel>
<QItemLabel>{{ order.agency }}</QItemLabel>
<QItemLabel>{{ currency(order.taxableBase) }}</QItemLabel>
</template>
<template #actions>
<QBtn
icon="delete"
flat
rounded
@click.stop.prevent="
openConfirmationModal(
null,
t('areYouSureDeleteOrder'),
() => removeOrder(order.id, index)
)
"
/>
<QBtn
icon="shopping_bag"
flat
rounded
@click.stop.prevent="loadOrder(order.id)"
/>
</template>
</CardList>
</QPage>
</template>
<style lang="scss" scoped></style>
<i18n lang="yaml">
en-US:
newOrder: New order
areYouSureDeleteOrder: Are you sure you want to delete the order?
es-ES:
newOrder: Nuevo pedido
areYouSureDeleteOrder: ¿Seguro que quieres borrar el pedido?
ca-ES:
newOrder: Nova comanda
areYouSureDeleteOrder: Segur que vols esborrar la comanda?
fr-FR:
newOrder: Nouvelle commande
areYouSureDeleteOrder: Êtes-vous sûr de vouloir supprimer la commande?
pt-PT:
newOrder: Novo pedido
areYouSureDeleteOrder: Tem certeza de que deseja excluir o pedido?
</i18n>

View File

@ -1,145 +0,0 @@
<template>
<Teleport :to="$actions">
<QBtn
icon="print"
:label="$t('printDeliveryNote')"
@click="onPrintClick()"
rounded
no-caps
/>
</Teleport>
<div>
<QCard class="vn-w-sm">
<QCardSection>
<div class="text-h6">#{{ ticket.id }}</div>
</QCardSection>
<QCardSection>
<div class="text-h6">{{ $t('shippingInformation') }}</div>
<div>
{{ $t('preparation') }}
{{ date(ticket.shipped, 'ddd, MMMM Do') }}
</div>
<div>
{{ $t('delivery') }}
{{ date(ticket.shipped, 'ddd, MMMM Do') }}
</div>
<div>
{{ $t(ticket.method != 'PICKUP' ? 'agency' : 'warehouse') }}
{{ ticket.agency }}
</div>
</QCardSection>
<QCardSection>
<div class="text-h6">{{ $t('deliveryAddress') }}</div>
<div>{{ ticket.nickname }}</div>
<div>{{ ticket.street }}</div>
<div>
{{ ticket.postalCode }} {{ ticket.city }} ({{
ticket.province
}})
</div>
</QCardSection>
<QSeparator inset />
<QList v-for="row in rows" :key="row.itemFk">
<QItem>
<QItemSection avatar>
<QAvatar size="68px">
<img
:src="`${$app.imageUrl}/catalog/200x200/${row.image}`"
/>
</QAvatar>
</QItemSection>
<QItemSection>
<QItemLabel lines="1">
{{ row.concept }}
</QItemLabel>
<QItemLabel lines="1" caption>
{{ row.value5 }} {{ row.value6 }} {{ row.value7 }}
</QItemLabel>
<QItemLabel lines="1">
{{ row.quantity }} x {{ currency(row.price) }}
</QItemLabel>
</QItemSection>
<QItemSection side class="total">
<QItemLabel>
<span class="discount" v-if="row.discount">
{{ currency(discountSubtotal(row)) }} -
{{ currency(row.discount) }} =
</span>
{{ currency(subtotal(row)) }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QCard>
</div>
</template>
<style lang="scss" scoped>
.total {
justify-content: flex-end;
}
</style>
<script>
import { date, currency } from 'src/lib/filters.js';
export default {
name: 'OrdersConfirmedView',
data() {
return {
ticket: {},
rows: null,
services: null,
packages: null
};
},
async mounted() {
const params = {
ticket: parseInt(this.$route.params.id)
};
this.ticket = await this.$jApi.getObject(
'CALL myTicket_get(#ticket)',
params
);
this.rows = await this.$jApi.query(
'CALL myTicket_getRows(#ticket)',
params
);
this.services = await this.$jApi.query(
'CALL myTicket_getServices(#ticket)',
params
);
this.packages = await this.$jApi.query(
'CALL myTicket_getPackages(#ticket)',
params
);
},
methods: {
date,
currency,
discountSubtotal(line) {
return line.quantity * line.price;
},
subtotal(line) {
const discount = line.discount;
return this.discountSubtotal(line) * ((100 - discount) / 100);
},
onPrintClick() {
const params = new URLSearchParams({
access_token: this.$user.token,
recipientId: this.$user.id,
type: 'deliveryNote'
});
window.open(
`/api/Tickets/${this.ticket.id}/delivery-note-pdf?${params.toString()}`
);
}
}
};
</script>

View File

@ -0,0 +1,74 @@
<script setup>
import { onMounted, inject, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import TicketDetails from 'src/components/ui/TicketDetails.vue';
import { userStore as useUserStore } from 'stores/user';
const { t } = useI18n();
const jApi = inject('jApi');
const route = useRoute();
const userStore = useUserStore();
const ticket = ref({});
const rows = ref([]);
const services = ref(null);
const packages = ref(null);
onMounted(async () => {
const params = {
ticket: parseInt(route.params.id)
};
ticket.value = await jApi.getObject('CALL myTicket_get(#ticket)', params);
rows.value = await jApi.query('CALL myTicket_getRows(#ticket)', params);
services.value = await jApi.query(
'CALL myTicket_getServices(#ticket)',
params
);
packages.value = await jApi.query(
'CALL myTicket_getPackages(#ticket)',
params
);
});
const onPrintClick = () => {
const params = new URLSearchParams({
access_token: userStore.token,
recipientId: userStore.id,
type: 'deliveryNote'
});
window.open(
`/api/Tickets/${ticket.value.id}/delivery-note-pdf?${params.toString()}`
);
};
</script>
<template>
<Teleport :to="$actions">
<QBtn
icon="print"
:label="t('printDeliveryNote')"
@click="onPrintClick()"
rounded
no-caps
/>
</Teleport>
<QPage>
<TicketDetails :rows="rows" :ticket="ticket" />
</QPage>
</template>
<i18n lang="yaml">
en-US:
printDeliveryNote: Print delivery note
es-ES:
printDeliveryNote: Imprimir albarán
ca-ES:
printDeliveryNote: Imprimir albarà
fr-FR:
printDeliveryNote: Imprimer bulletin de livraison
pt-PT:
printDeliveryNote: Imprimir nota de entrega
</i18n>

View File

@ -1,12 +1,13 @@
import { route } from 'quasar/wrappers' import { route } from 'quasar/wrappers';
import { appStore } from 'stores/app' import { useAppStore } from 'stores/app';
import { import {
createRouter, createRouter,
createMemoryHistory, createMemoryHistory,
createWebHistory, createWebHistory,
createWebHashHistory createWebHashHistory
} from 'vue-router' } from 'vue-router';
import routes from './routes' import routes from './routes';
import { i18n } from 'src/boot/i18n';
/* /*
* If not building with SSR mode, you can * If not building with SSR mode, you can
@ -22,7 +23,7 @@ export default route(function (/* { store, ssrContext } */) {
? createMemoryHistory ? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history' : process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory ? createWebHistory
: createWebHashHistory : createWebHashHistory;
const Router = createRouter({ const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({ left: 0, top: 0 }),
@ -34,18 +35,18 @@ export default route(function (/* { store, ssrContext } */) {
history: createHistory( history: createHistory(
process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE
) )
}) });
Router.afterEach((to, from) => { Router.afterEach((to, from) => {
if (from.name === to.name) return if (from.name === to.name) return;
const app = appStore() const app = useAppStore();
app.$patch({ app.$patch({
title: window.i18n.t(to.name || 'home'), title: i18n.global.t(to.name || 'home'),
subtitle: null, subtitle: null,
useRightDrawer: false, useRightDrawer: false,
rightDrawerOpen: true rightDrawerOpen: true
}) });
}) });
return Router return Router;
}) });

View File

@ -4,7 +4,7 @@ const routes = [
component: () => import('layouts/LoginLayout.vue'), component: () => import('layouts/LoginLayout.vue'),
children: [ children: [
{ {
name: 'Login', name: 'login',
path: '/login/:email?', path: '/login/:email?',
component: () => import('pages/Login/LoginView.vue') component: () => import('pages/Login/LoginView.vue')
}, },
@ -35,19 +35,24 @@ const routes = [
component: () => import('src/pages/Cms/HomeView.vue') component: () => import('src/pages/Cms/HomeView.vue')
}, },
{ {
name: 'orders', name: 'confirmedOrders',
path: '/ecomerce/orders', path: '/ecomerce/orders',
component: () => import('pages/Ecomerce/Orders.vue') component: () => import('pages/Ecomerce/OrdersView.vue')
}, },
{ {
name: 'ticket', name: 'ticket',
path: '/ecomerce/ticket/:id', path: '/ecomerce/ticket/:id',
component: () => import('pages/Ecomerce/Ticket.vue') component: () => import('pages/Ecomerce/TicketView.vue')
}, },
{ {
name: 'invoices', name: 'invoices',
path: '/ecomerce/invoices', path: '/ecomerce/invoices',
component: () => import('pages/Ecomerce/Invoices.vue') component: () => import('pages/Ecomerce/InvoicesView.vue')
},
{
name: 'pendingOrders',
path: '/ecomerce/pending',
component: () => import('pages/Ecomerce/PendingOrders.vue')
}, },
{ {
name: 'catalog', name: 'catalog',
@ -55,22 +60,32 @@ const routes = [
component: () => import('pages/Ecomerce/Catalog.vue') component: () => import('pages/Ecomerce/Catalog.vue')
}, },
{ {
name: 'packages', name: 'basket',
path: '/ecomerce/basket/:id?',
component: () => import('pages/Ecomerce/BasketView.vue')
},
{
name: 'checkout',
path: '/ecomerce/checkout',
component: () => import('pages/Ecomerce/CheckoutView.vue')
},
{
name: 'agencyPackages',
path: '/agencies/packages', path: '/agencies/packages',
component: () => import('src/pages/Agencies/PackagesView.vue') component: () => import('src/pages/Agencies/PackagesView.vue')
}, },
{ {
name: 'Account', name: 'accountConfig',
path: '/account/conf', path: '/account/conf',
component: () => import('pages/Account/AccountConfig.vue') component: () => import('pages/Account/AccountConfig.vue')
}, },
{ {
name: 'AddressesList', name: 'addressesList',
path: '/account/address-list', path: '/account/address-list',
component: () => import('pages/Account/AddressList.vue') component: () => import('pages/Account/AddressList.vue')
}, },
{ {
name: 'AddressDetails', name: 'addressDetails',
path: '/account/address/:id?', path: '/account/address/:id?',
component: () => import('pages/Account/AddressDetails.vue') component: () => import('pages/Account/AddressDetails.vue')
} }

View File

@ -1,19 +1,52 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia';
import { jApi } from 'boot/axios' import { jApi } from 'boot/axios';
import useNotify from 'src/composables/useNotify.js';
export const appStore = defineStore('hedera', { const { notify } = useNotify();
export const useAppStore = defineStore('hedera', {
state: () => ({ state: () => ({
title: null, title: null,
subtitle: null, subtitle: null,
imageUrl: '', imageUrl: '',
useRightDrawer: false, useRightDrawer: false,
rightDrawerOpen: false rightDrawerOpen: false,
basketOrderId: null,
isHeaderMounted: false
}), }),
actions: { actions: {
async loadConfig () { async init() {
const imageUrl = await jApi.getValue('SELECT url FROM imageConfig') this.getBasketOrderId();
this.$patch({ imageUrl }) },
getBasketOrderId() {
this.basketOrderId = localStorage.getItem('hederaBasket');
},
async loadConfig() {
const imageUrl = await jApi.getValue('SELECT url FROM imageConfig');
this.$patch({ imageUrl });
},
async checkOrder(orderId) {
try {
const resultSet = await jApi.execQuery(
'CALL myOrder_checkConfig(#id)',
{ id: orderId }
);
resultSet.fetchValue();
} catch (err) {
console.error('Error checking order', err);
}
},
loadIntoBasket(orderId) {
if (this.basketOrderId !== orderId) {
localStorage.setItem('hederaBasket', orderId);
this.basketOrderId = orderId;
notify('orderLoadedIntoBasket', 'positive');
} }
} }
}) }
});

View File

@ -1,5 +1,5 @@
import { store } from 'quasar/wrappers' import { store } from 'quasar/wrappers';
import { createPinia } from 'pinia' import { createPinia } from 'pinia';
/* /*
* If not building with SSR mode, you can * If not building with SSR mode, you can
@ -11,10 +11,10 @@ import { createPinia } from 'pinia'
*/ */
export default store((/* { ssrContext } */) => { export default store((/* { ssrContext } */) => {
const pinia = createPinia() const pinia = createPinia();
// You can add Pinia plugins here // You can add Pinia plugins here
// pinia.use(SomePiniaPlugin) // pinia.use(SomePiniaPlugin)
return pinia return pinia;
}) });