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

@ -14,243 +14,243 @@ const path = require('path');
const { configure } = require('quasar/wrappers');
module.exports = configure(function (ctx) {
return {
// https://v2.quasar.dev/quasar-cli-webpack/supporting-ts
supportTS: false,
return {
// https://v2.quasar.dev/quasar-cli-webpack/supporting-ts
supportTS: false,
// https://v2.quasar.dev/quasar-cli-webpack/prefetch-feature
// preFetch: true,
// https://v2.quasar.dev/quasar-cli-webpack/prefetch-feature
// preFetch: true,
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-webpack/boot-files
boot: ['i18n', 'axios', 'error-handler', 'app'],
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-webpack/boot-files
boot: ['i18n', 'axios', 'vnDate', 'error-handler', 'app'],
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
css: ['app.scss', 'width.scss', 'responsive.scss'],
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css
css: ['app.scss', 'width.scss', 'responsive.scss'],
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
// 'ionicons-v4',
// 'mdi-v5',
// 'fontawesome-v6',
// 'eva-icons',
// 'themify',
// 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
// 'ionicons-v4',
// 'mdi-v5',
// 'fontawesome-v6',
// 'eva-icons',
// 'themify',
// 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
'roboto-font', // optional, you are not bound to it
'material-icons' // optional, you are not bound to it
],
'roboto-font', // optional, you are not bound to it
'material-icons' // optional, you are not bound to it
],
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-build
build: {
vueRouterMode: 'hash', // available values: 'hash', 'history'
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-build
build: {
vueRouterMode: 'hash', // available values: 'hash', 'history'
// transpile: false,
// publicPath: '/',
// transpile: false,
// publicPath: '/',
// Add dependencies for transpiling with Babel (Array of string/regex)
// (from node_modules, which are by default not transpiled).
// Applies only if "transpile" is set to true.
// transpileDependencies: [],
// Add dependencies for transpiling with Babel (Array of string/regex)
// (from node_modules, which are by default not transpiled).
// Applies only if "transpile" is set to true.
// transpileDependencies: [],
// rtl: true, // https://quasar.dev/options/rtl-support
// preloadChunks: true,
// showProgress: false,
// gzip: true,
// analyze: true,
// rtl: true, // https://quasar.dev/options/rtl-support
// preloadChunks: true,
// showProgress: false,
// gzip: true,
// analyze: true,
// Options below are automatically set depending on the env, set them if you want to override
// extractCSS: false,
// Options below are automatically set depending on the env, set them if you want to override
// extractCSS: false,
// https://v2.quasar.dev/quasar-cli-webpack/handling-webpack
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
// https://v2.quasar.dev/quasar-cli-webpack/handling-webpack
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
chainWebpack(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js', 'vue'] }]);
chainWebpack (chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js', 'vue'] }]);
chain.module
.rule('i18n-resource')
.test(/\.(json5?|ya?ml)$/)
.include.add(path.resolve(__dirname, './src/i18n'))
.end()
.type('javascript/auto')
.use('i18n-resource')
.loader('@intlify/vue-i18n-loader');
chain.module
.rule('i18n')
.resourceQuery(/blockType=i18n/)
.type('javascript/auto')
.use('i18n')
.loader('@intlify/vue-i18n-loader');
}
},
chain.module
.rule('i18n-resource')
.test(/\.(json5?|ya?ml)$/)
.include.add(path.resolve(__dirname, './src/i18n'))
.end()
.type('javascript/auto')
.use('i18n-resource')
.loader('@intlify/vue-i18n-loader');
chain.module
.rule('i18n')
.resourceQuery(/blockType=i18n/)
.type('javascript/auto')
.use('i18n')
.loader('@intlify/vue-i18n-loader');
}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-devServer
devServer: {
server: {
type: 'http'
},
port: 8080,
open: false,
// static: __dirname,
headers: { 'Access-Control-Allow-Origin': '*' },
// stats: { chunks: false },
proxy: {
'/api': 'http://localhost:3000',
'/': {
target: 'http://localhost:3002',
bypass: req => (req.path !== '/' ? req.path : null)
}
}
},
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-framework
framework: {
config: {},
autoImportComponentCase: 'pascal',
// iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact
// (like functional components as one of the examples),
// you can manually specify Quasar components/directives to be available everywhere:
//
// components: [],
// directives: [],
// Quasar plugins
plugins: ['Notify', 'Dialog']
},
// animations: 'all', // --- includes all animations
// https://quasar.dev/options/animations
animations: [],
// https://v2.quasar.dev/quasar-cli-webpack/developing-ssr/configuring-ssr
ssr: {
pwa: false,
// manualStoreHydration: true,
// manualPostHydrationTrigger: true,
prodPort: 3000, // The default port that the production server should use
// (gets superseded if process.env.PORT is specified at runtime)
maxAge: 1000 * 60 * 60 * 24 * 30,
// Tell browser when a file from the server should expire from cache (in ms)
chainWebpackWebserver(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
middlewares: [
ctx.prod ? 'compression' : '',
'render' // keep this as last one
]
},
// https://v2.quasar.dev/quasar-cli-webpack/developing-pwa/configuring-pwa
pwa: {
workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
workboxOptions: {}, // only for GenerateSW
// for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts])
// if using workbox in InjectManifest mode
chainWebpackCustomSW(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
manifest: {
name: 'Hedera',
short_name: 'Hedera',
description: "Verdnatura's webshop",
display: 'standalone',
orientation: 'portrait',
background_color: '#ffffff',
theme_color: '#027be3',
icons: [
{
src: 'icons/icon-128x128.png',
sizes: '128x128',
type: 'image/png'
},
{
src: 'icons/icon-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'icons/icon-256x256.png',
sizes: '256x256',
type: 'image/png'
},
{
src: 'icons/icon-384x384.png',
sizes: '384x384',
type: 'image/png'
},
{
src: 'icons/icon-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-cordova-apps/configuring-cordova
cordova: {
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-electron-apps/configuring-electron
electron: {
bundler: 'packager', // 'packager' or 'builder'
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',
// Windows only
// win32metadata: { ... }
},
builder: {
// https://www.electron.build/configuration/configuration
appId: 'hedera-web'
},
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
chainWebpackMain(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
chainWebpackPreload(chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
}
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-devServer
devServer: {
server: {
type: 'http'
},
port: 8080,
open: false,
// static: __dirname,
headers: { 'Access-Control-Allow-Origin': '*' },
// stats: { chunks: false },
proxy: {
'/api': 'http://localhost:3000',
'/': {
target: 'http://localhost:3002',
bypass: req => (req.path !== '/' ? req.path : null)
}
};
}
},
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-framework
framework: {
config: {},
autoImportComponentCase: 'pascal',
// iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact
// (like functional components as one of the examples),
// you can manually specify Quasar components/directives to be available everywhere:
//
// components: [],
// directives: [],
// Quasar plugins
plugins: ['Notify', 'Dialog']
},
// animations: 'all', // --- includes all animations
// https://quasar.dev/options/animations
animations: [],
// https://v2.quasar.dev/quasar-cli-webpack/developing-ssr/configuring-ssr
ssr: {
pwa: false,
// manualStoreHydration: true,
// manualPostHydrationTrigger: true,
prodPort: 3000, // The default port that the production server should use
// (gets superseded if process.env.PORT is specified at runtime)
maxAge: 1000 * 60 * 60 * 24 * 30,
// Tell browser when a file from the server should expire from cache (in ms)
chainWebpackWebserver (chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
middlewares: [
ctx.prod ? 'compression' : '',
'render' // keep this as last one
]
},
// https://v2.quasar.dev/quasar-cli-webpack/developing-pwa/configuring-pwa
pwa: {
workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
workboxOptions: {}, // only for GenerateSW
// for the custom service worker ONLY (/src-pwa/custom-service-worker.[js|ts])
// if using workbox in InjectManifest mode
chainWebpackCustomSW (chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
manifest: {
name: 'Hedera',
short_name: 'Hedera',
description: "Verdnatura's webshop",
display: 'standalone',
orientation: 'portrait',
background_color: '#ffffff',
theme_color: '#027be3',
icons: [
{
src: 'icons/icon-128x128.png',
sizes: '128x128',
type: 'image/png'
},
{
src: 'icons/icon-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'icons/icon-256x256.png',
sizes: '256x256',
type: 'image/png'
},
{
src: 'icons/icon-384x384.png',
sizes: '384x384',
type: 'image/png'
},
{
src: 'icons/icon-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-cordova-apps/configuring-cordova
cordova: {
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true
},
// Full list of options: https://v2.quasar.dev/quasar-cli-webpack/developing-electron-apps/configuring-electron
electron: {
bundler: 'packager', // 'packager' or 'builder'
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',
// Windows only
// win32metadata: { ... }
},
builder: {
// https://www.electron.build/configuration/configuration
appId: 'hedera-web'
},
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
chainWebpackMain (chain) {
chain
.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ extensions: ['js'] }]);
},
chainWebpackPreload (chain) {
chain
.plugin('eslint-webpack-plugin')
.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>
<router-view />
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'App'
});
</script>

View File

@ -1,10 +1,10 @@
import { boot } from 'quasar/wrappers'
import { appStore } from 'stores/app'
import { userStore } from 'stores/user'
import { boot } from 'quasar/wrappers';
import { useAppStore } from 'stores/app';
import { userStore } from 'stores/user';
export default boot(({ app }) => {
const props = app.config.globalProperties
props.$app = appStore()
props.$user = userStore()
props.$actions = document.createElement('div')
})
const props = app.config.globalProperties;
props.$app = useAppStore();
props.$user = userStore();
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"
ref="addressFormRef"
class="column full-width q-gutter-y-xs"
@submit="submit()"
>
<span class="text-h6 text-bold">
{{ title }}
@ -197,7 +196,7 @@ defineExpose({
:class="{ 'q-mt-md': showBottomActions }"
>
<QBtn
v-if="defaultActions"
v-if="defaultActions && showBottomActions"
:label="t('cancel')"
:icon="showBottomActions ? undefined : 'check'"
rounded
@ -208,12 +207,12 @@ defineExpose({
<QBtn
v-if="defaultActions"
:label="t('save')"
type="submit"
:icon="showBottomActions ? undefined : 'check'"
rounded
no-caps
flat
:disabled="!showBottomActions && !updatedColumns.length"
@click="submit()"
/>
<slot name="actions" />
</component>

View File

@ -9,7 +9,7 @@ const emit = defineEmits([
'remove'
]);
const $props = defineProps({
const props = defineProps({
modelValue: {
type: [String, Number],
default: null
@ -33,7 +33,7 @@ const requiredFieldRule = val => !!val || t('globals.fieldRequired');
const vnInputRef = ref(null);
const value = computed({
get() {
return $props.modelValue;
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
@ -41,7 +41,7 @@ const value = computed({
});
const hover = ref(false);
const styleAttrs = computed(() => {
return $props.isOutlined
return props.isOutlined
? { dense: true, outlined: true, rounded: true }
: {};
});
@ -88,9 +88,7 @@ const inputRules = [
<template #append>
<slot v-if="$slots.append && !$attrs.disabled" name="append" />
<QIcon
v-if="
hover && value && !$attrs.disabled && $props.clearable
"
v-if="hover && value && !$attrs.disabled && props.clearable"
name="close"
size="xs"
@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>
import { ref, computed } from 'vue';
import { appStore } from 'stores/app';
import { useAppStore } from 'stores/app';
const $props = defineProps({
const props = defineProps({
baseURL: {
type: String,
default: null
@ -23,23 +23,27 @@ const $props = defineProps({
id: {
type: Number,
required: true
},
rounded: {
type: Boolean,
default: false
}
});
const app = appStore();
const app = useAppStore();
const show = ref(false);
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>
<template>
<QImg
:class="{ zoomIn: $props.zoomSize }"
:class="{ zoomIn: props.zoomSize, rounded: props.rounded }"
:src="url"
v-bind="$attrs"
@click="show = !show"
spinner-color="primary"
/>
<QDialog v-model="show" v-if="$props.zoomSize">
<QDialog v-model="show" v-if="props.zoomSize">
<QImg
:src="url"
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

@ -1,8 +1,8 @@
// app global css in SCSS form
@font-face {
font-family: Poppins;
src: url(./poppins.ttf) format('truetype');
font-family: Poppins;
src: url(./poppins.ttf) format('truetype');
}
@font-face {
font-family: 'Open Sans';
@ -36,3 +36,9 @@ a.link {
.q-page-sticky.fixed-bottom-right {
margin: 18px;
}
.no-border-radius {
border-radius: 0 !important;
}
.no-padding {
padding: 0 !important;
}

View File

@ -15,7 +15,7 @@
$primary: #1a1a1a;
$secondary: #26a69a;
$accent: #8cc63f;
$gray-light: #ddd;
$dark: #1d1d1d;
$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',
'December'
],
shortMonths: [
monthsShort: [
'Jan',
'Feb',
'Mar',
@ -56,6 +56,17 @@ export default {
// menu
home: 'Home',
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',
order: 'Pending order',
ticket: 'Order',
@ -76,5 +87,6 @@ export default {
addressEdit: 'Edit address',
dataSaved: 'Data saved',
save: 'Save',
cancel: 'Cancel'
cancel: 'Cancel',
of: 'of'
};

View File

@ -46,7 +46,7 @@ export default {
'Noviembre',
'Diciembre'
],
shortMonths: [
monthsShort: [
'Ene',
'Feb',
'Mar',
@ -65,6 +65,17 @@ export default {
// Menu
home: 'Inicio',
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',
order: 'Pedido pendiente',
ticket: 'Pedido',
@ -94,5 +105,6 @@ export default {
addressEdit: 'Editar dirección',
dataSaved: 'Datos guardados',
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 esES from './es-ES'
import enUS from './en-US';
import esES from './es-ES';
import frFR from './fr-FR';
import ptPT from './pt-PT';
import caES from './ca-ES';
export default {
'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'
const { pad } = format
import { i18n } from 'src/boot/i18n';
import { date as qdate, format } from 'quasar';
const { pad } = format;
export function currency (val) {
return typeof val === 'number' ? val.toFixed(2) + '€' : val
export function currency(val) {
return typeof val === 'number' ? val.toFixed(2) + '€' : val;
}
export function date (val, format) {
if (val == null) return val
export function date(val, format) {
if (val == null) return val;
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) {
if (val == null) return val
export const formatDate = (timeStamp, format = 'YYYY-MM-DD') => {
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)) {
val = new Date(val)
val = new Date(val);
}
const dif = qdate.getDateDiff(new Date(), val, 'days')
let day
const dif = qdate.getDateDiff(new Date(), val, 'days');
let day;
switch (dif) {
case 0:
day = 'today'
break
day = 'today';
break;
case 1:
day = 'yesterday'
break
day = 'yesterday';
break;
case -1:
day = 'tomorrow'
break
day = 'tomorrow';
break;
}
if (day) {
day = window.i18n.t(day)
day = i18n.global.t(day);
} else {
if (dif > 0 && dif <= 7) {
day = qdate.formatDate(val, 'ddd', window.i18n.tm('date'))
day = qdate.formatDate(val, 'ddd', i18n.global.tm('date'));
} 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) {
if (val == null) return val
export function relTime(val) {
if (val == null) return val;
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) {
if (val == null) return val
export function elapsedTime(val) {
if (val == null) return val;
if (!(val instanceof Date)) {
val = new Date(val)
val = new Date(val);
}
const now = new Date().getTime()
val = Math.floor((now - val.getTime()) / 1000)
const now = new Date().getTime();
val = Math.floor((now - val.getTime()) / 1000);
const hours = Math.floor(val / 3600)
val -= hours * 3600
const minutes = Math.floor(val / 60)
val -= minutes * 60
const seconds = val
const hours = Math.floor(val / 3600);
val -= hours * 3600;
const minutes = Math.floor(val / 60);
val -= minutes * 60;
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"
rounded
no-caps
:to="{ name: 'AddressesList' }"
:to="{ name: 'addressesList' }"
/>
<QBtn
:label="t('changePassword')"

View File

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

View File

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

View File

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

View File

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

View File

@ -343,19 +343,23 @@
</style>
<script>
import { date, currency } from 'src/lib/filters.js';
import { date as qdate } from 'quasar';
import { date, currency, formatDate } from 'src/lib/filters.js';
import axios from 'axios';
import { useAppStore } from 'stores/app';
const CancelToken = axios.CancelToken;
export default {
name: 'HederaCatalog',
setup() {
const appStore = useAppStore();
return { appStore };
},
data() {
return {
uid: 0,
search: '',
orderDate: qdate.formatDate(new Date(), 'YYYY/MM/DD'),
orderDate: formatDate(new Date(), 'YYYY/MM/DD'),
category: null,
categories: [],
type: null,
@ -446,7 +450,7 @@ export default {
if (!value) return;
const res = await this.$jApi.execQuery(
`CALL myBasket_getAvailable;
`CALL myOrder_getAvailable(${this.appStore.basketOrderId});
SELECT DISTINCT t.id, l.name
FROM vn.item i
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>
<Teleport :to="$actions">
<div class="balance">
<span class="label">{{ $t('balance') }}</span>
<span class="label">{{ t('balance') }}</span>
<span class="amount" :class="{ negative: debt < 0 }">
{{ currency(debt || 0) }}
</span>
<QIcon
name="info"
:title="$t('paymentInfo')"
class="info"
size="24px"
/>
<QIcon name="info" class="info" size="sm">
<QTooltip max-width="450px">
{{ t('paymentInfo') }}
</QTooltip>
</QIcon>
</div>
<QBtn
icon="payments"
:label="$t('makePayment')"
:label="t('makePayment')"
@click="onPayClick()"
rounded
no-caps
/>
>
<QTooltip>
{{ t('makePayment') }}
</QTooltip>
</QBtn>
<QBtn
to="/ecomerce/basket"
:to="{ name: 'basket' }"
icon="shopping_cart"
:label="$t('shoppingCart')"
:label="t('shoppingCart')"
rounded
no-caps
/>
>
<QTooltip>
{{ t('shoppingCart') }}
</QTooltip>
</QBtn>
</Teleport>
<div class="vn-w-sm">
<QPage class="vn-w-sm">
<div
v-if="!orders?.length"
class="text-subtitle1 text-center text-grey-7 q-pa-md"
>
{{ $t('noOrdersFound') }}
{{ t('noOrdersFound') }}
</div>
<QCard v-if="orders?.length">
<QList bordered separator padding>
<QItem
v-for="order in orders"
:key="order.id"
:to="`ticket/${order.id}`"
clickable
v-ripple
>
<QItemSection>
<QItemLabel>
{{ date(order.landed, 'ddd, MMMM Do') }}
</QItemLabel>
<QItemLabel caption>#{{ order.id }}</QItemLabel>
<QItemLabel caption>{{ order.nickname }}</QItemLabel>
<QItemLabel caption>{{ order.agency }}</QItemLabel>
</QItemSection>
<QItemSection side top> {{ order.total }} </QItemSection>
</QItem>
</QList>
</QCard>
<QList v-if="orders?.length">
<CardList
v-for="order in orders"
:key="order.id"
:to="`ticket/${order.id}`"
tag="label"
>
<template #content>
<QItemLabel
class="full-width text-bold q-mb-sm flex row justify-between"
>
<span>{{ formatDateTitle(order.landed) }}</span>
<span>{{ currency(order.total) }}</span>
</QItemLabel>
<QItemLabel>#{{ order.id }}</QItemLabel>
<QItemLabel>{{ order.nickname }}</QItemLabel>
<QItemLabel>{{ order.agency }}</QItemLabel>
</template>
</CardList>
</QList>
<QPageSticky>
<QBtn
fab
icon="add_shopping_cart"
color="accent"
to="/ecomerce/catalog"
:title="$t('startOrder')"
:to="{ name: 'catalog' }"
:title="t('startOrder')"
/>
</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>
<style lang="scss" scoped>
@ -92,50 +157,6 @@
}
</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">
en-US:
startOrder: Start order
@ -148,6 +169,7 @@ en-US:
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
payment button, delete the suggested amount and enter the amount you want.
amountToPay: 'Amount to pay (€):'
es-ES:
startOrder: Empezar pedido
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
cuenta, pulsa el botón de pago, borra la cantidad sugerida e introduce la
cantidad que desees.
amountToPay: 'Cantidad a pagar (€):'
ca-ES:
startOrder: Començar encàrrec
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
lliurament a compte, prem el botó de pagament, esborra la quantitat suggerida
e introdueix la quantitat que vulguis.
amountToPay: 'Quantitat a pagar (€):'
fr-FR:
startOrder: Acheter
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
voulez faire un versement, le montant suggéré effacé et entrez le montant que
vous souhaitez.
amountToPay: 'Montant à payer (€):'
pt-PT:
startOrder: Iniciar encomenda
noOrdersFound: Nenhum pedido encontrado
@ -196,4 +221,5 @@ pt-PT:
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
a quantidade que deseje.
amountToPay: 'Valor a pagar (€):'
</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 { appStore } from 'stores/app'
import { route } from 'quasar/wrappers';
import { useAppStore } from 'stores/app';
import {
createRouter,
createMemoryHistory,
createWebHistory,
createWebHashHistory
} from 'vue-router'
import routes from './routes'
} from 'vue-router';
import routes from './routes';
import { i18n } from 'src/boot/i18n';
/*
* If not building with SSR mode, you can
@ -22,7 +23,7 @@ export default route(function (/* { store, ssrContext } */) {
? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory
: createWebHashHistory
: createWebHashHistory;
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
@ -34,18 +35,18 @@ export default route(function (/* { store, ssrContext } */) {
history: createHistory(
process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE
)
})
});
Router.afterEach((to, from) => {
if (from.name === to.name) return
const app = appStore()
if (from.name === to.name) return;
const app = useAppStore();
app.$patch({
title: window.i18n.t(to.name || 'home'),
title: i18n.global.t(to.name || 'home'),
subtitle: null,
useRightDrawer: false,
rightDrawerOpen: true
})
})
});
});
return Router
})
return Router;
});

View File

@ -4,7 +4,7 @@ const routes = [
component: () => import('layouts/LoginLayout.vue'),
children: [
{
name: 'Login',
name: 'login',
path: '/login/:email?',
component: () => import('pages/Login/LoginView.vue')
},
@ -35,19 +35,24 @@ const routes = [
component: () => import('src/pages/Cms/HomeView.vue')
},
{
name: 'orders',
name: 'confirmedOrders',
path: '/ecomerce/orders',
component: () => import('pages/Ecomerce/Orders.vue')
component: () => import('pages/Ecomerce/OrdersView.vue')
},
{
name: 'ticket',
path: '/ecomerce/ticket/:id',
component: () => import('pages/Ecomerce/Ticket.vue')
component: () => import('pages/Ecomerce/TicketView.vue')
},
{
name: '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',
@ -55,22 +60,32 @@ const routes = [
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',
component: () => import('src/pages/Agencies/PackagesView.vue')
},
{
name: 'Account',
name: 'accountConfig',
path: '/account/conf',
component: () => import('pages/Account/AccountConfig.vue')
},
{
name: 'AddressesList',
name: 'addressesList',
path: '/account/address-list',
component: () => import('pages/Account/AddressList.vue')
},
{
name: 'AddressDetails',
name: 'addressDetails',
path: '/account/address/:id?',
component: () => import('pages/Account/AddressDetails.vue')
}

View File

@ -1,19 +1,52 @@
import { defineStore } from 'pinia'
import { jApi } from 'boot/axios'
import { defineStore } from 'pinia';
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: () => ({
title: null,
subtitle: null,
imageUrl: '',
useRightDrawer: false,
rightDrawerOpen: false
rightDrawerOpen: false,
basketOrderId: null,
isHeaderMounted: false
}),
actions: {
async loadConfig () {
const imageUrl = await jApi.getValue('SELECT url FROM imageConfig')
this.$patch({ imageUrl })
async init() {
this.getBasketOrderId();
},
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 { createPinia } from 'pinia'
import { store } from 'quasar/wrappers';
import { createPinia } from 'pinia';
/*
* If not building with SSR mode, you can
@ -11,10 +11,10 @@ import { createPinia } from 'pinia'
*/
export default store((/* { ssrContext } */) => {
const pinia = createPinia()
const pinia = createPinia();
// You can add Pinia plugins here
// pinia.use(SomePiniaPlugin)
return pinia
})
return pinia;
});