diff --git a/src/App.vue b/src/App.vue index 69fb8958..8b15cff2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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> diff --git a/src/components/common/VnForm.vue b/src/components/common/VnForm.vue index 71f36fd3..cf2788cf 100644 --- a/src/components/common/VnForm.vue +++ b/src/components/common/VnForm.vue @@ -206,7 +206,6 @@ defineExpose({ ref="addressFormRef" class="form" :class="separationBetweenInputs" - @submit="submit()" > <span v-if="title" class="text-h6 text-bold"> {{ title }} diff --git a/src/components/ui/TicketDetails.vue b/src/components/ui/TicketDetails.vue new file mode 100644 index 00000000..37af51ab --- /dev/null +++ b/src/components/ui/TicketDetails.vue @@ -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> diff --git a/src/components/ui/VnTable.vue b/src/components/ui/VnTable.vue new file mode 100644 index 00000000..a32240c5 --- /dev/null +++ b/src/components/ui/VnTable.vue @@ -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> diff --git a/src/composables/usePrintService.js b/src/composables/usePrintService.js new file mode 100644 index 00000000..e96e26e2 --- /dev/null +++ b/src/composables/usePrintService.js @@ -0,0 +1,38 @@ +import { 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 + }; +} diff --git a/src/i18n/en-US/index.js b/src/i18n/en-US/index.js index 45c4b4d8..27bdec11 100644 --- a/src/i18n/en-US/index.js +++ b/src/i18n/en-US/index.js @@ -37,7 +37,7 @@ export default { 'November', 'December' ], - shortMonths: [ + monthsShort: [ 'Jan', 'Feb', 'Mar', diff --git a/src/i18n/es-ES/index.js b/src/i18n/es-ES/index.js index 98af2234..a7475965 100644 --- a/src/i18n/es-ES/index.js +++ b/src/i18n/es-ES/index.js @@ -43,7 +43,7 @@ export default { 'Noviembre', 'Diciembre' ], - shortMonths: [ + monthsShort: [ 'Ene', 'Feb', 'Mar', diff --git a/src/i18n/fr-FR/index.js b/src/i18n/fr-FR/index.js index 1ca353b8..659ae4b2 100644 --- a/src/i18n/fr-FR/index.js +++ b/src/i18n/fr-FR/index.js @@ -24,7 +24,7 @@ export default { 'Novembre', 'Décembre' ], - shortMonths: [ + monthsShort: [ 'Jan', 'Fév', 'Mar', diff --git a/src/i18n/pt-PT/index.js b/src/i18n/pt-PT/index.js index ef31036b..5b92f163 100644 --- a/src/i18n/pt-PT/index.js +++ b/src/i18n/pt-PT/index.js @@ -24,7 +24,7 @@ export default { 'Novembro', 'Dezembro' ], - shortMonths: [ + monthsShort: [ 'Jan', 'Fev', 'Mar', diff --git a/src/pages/Account/AddressList.vue b/src/pages/Account/AddressList.vue index 7efc9412..cd8e8857 100644 --- a/src/pages/Account/AddressList.vue +++ b/src/pages/Account/AddressList.vue @@ -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'; import { useAppStore } from 'stores/app'; @@ -97,42 +99,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 @@ -151,25 +144,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 diff --git a/src/pages/Agencies/PackagesView.vue b/src/pages/Agencies/PackagesView.vue index d9af6e7f..2ef44424 100644 --- a/src/pages/Agencies/PackagesView.vue +++ b/src/pages/Agencies/PackagesView.vue @@ -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 diff --git a/src/pages/Ecomerce/BasketView.vue b/src/pages/Ecomerce/BasketView.vue new file mode 100644 index 00000000..baac12d6 --- /dev/null +++ b/src/pages/Ecomerce/BasketView.vue @@ -0,0 +1 @@ +<template>Basket view</template> diff --git a/src/pages/Ecomerce/Catalog.vue b/src/pages/Ecomerce/Catalog.vue index 2a6bca06..6261955e 100644 --- a/src/pages/Ecomerce/Catalog.vue +++ b/src/pages/Ecomerce/Catalog.vue @@ -10,7 +10,7 @@ dense standout > - <template v-slot:prepend> + <template #prepend> <QIcon v-if="search === ''" name="search" /> <QIcon v-else @@ -220,7 +220,7 @@ </QCardActions> </QCard> </div> - <template v-slot:loading> + <template #loading> <div class="row justify-center q-my-md"> <QSpinner color="primary" name="dots" size="40px" /> </div> @@ -343,8 +343,7 @@ </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'; import { storeToRefs } from 'pinia'; @@ -362,7 +361,7 @@ export default { return { uid: 0, search: '', - orderDate: qdate.formatDate(new Date(), 'YYYY/MM/DD'), + orderDate: formatDate(new Date(), 'YYYY/MM/DD'), category: null, categories: [], type: null, @@ -453,7 +452,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 diff --git a/src/pages/Ecomerce/CheckoutView.vue b/src/pages/Ecomerce/CheckoutView.vue new file mode 100644 index 00000000..5730758d --- /dev/null +++ b/src/pages/Ecomerce/CheckoutView.vue @@ -0,0 +1 @@ +<template>Checkout</template> diff --git a/src/pages/Ecomerce/Invoices.vue b/src/pages/Ecomerce/Invoices.vue deleted file mode 100644 index f116c738..00000000 --- a/src/pages/Ecomerce/Invoices.vue +++ /dev/null @@ -1,190 +0,0 @@ -<template> - <Teleport v-if="isHeaderMounted" 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'; -import { useAppStore } from 'stores/app'; -import { storeToRefs } from 'pinia'; - -export default { - name: 'OrdersPendingIndex', - setup() { - const appStore = useAppStore(); - const { isHeaderMounted } = storeToRefs(appStore); - return { isHeaderMounted }; - }, - 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.fetchUser(); - }, - - watch: { - async year() { - await this.fetchUser(); - } - }, - - methods: { - date, - currency, - - async fetchUser() { - 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> diff --git a/src/pages/Ecomerce/InvoicesView.vue b/src/pages/Ecomerce/InvoicesView.vue new file mode 100644 index 00000000..124d8e03 --- /dev/null +++ b/src/pages/Ecomerce/InvoicesView.vue @@ -0,0 +1,162 @@ +<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 { useAppStore } from 'stores/app'; +import { storeToRefs } from 'pinia'; + +const { t } = useI18n(); +const jApi = inject('jApi'); +const { openReport } = usePrintService(); +const appStore = useAppStore(); +const { isHeaderMounted } = storeToRefs(appStore); + +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 v-if="isHeaderMounted" 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> diff --git a/src/pages/Ecomerce/Orders.vue b/src/pages/Ecomerce/OrdersView.vue similarity index 54% rename from src/pages/Ecomerce/Orders.vue rename to src/pages/Ecomerce/OrdersView.vue index e9ce1818..d94c2e4c 100644 --- a/src/pages/Ecomerce/Orders.vue +++ b/src/pages/Ecomerce/OrdersView.vue @@ -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 v-if="isHeaderMounted" 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,57 +157,6 @@ } </style> -<script> -import { date, currency } from 'src/lib/filters.js'; -import { tpvStore } from 'stores/tpv'; -import { useAppStore } from 'stores/app'; -import { storeToRefs } from 'pinia'; - -export default { - name: 'OrdersPendingIndex', - setup() { - const appStore = useAppStore(); - const { isHeaderMounted } = storeToRefs(appStore); - return { isHeaderMounted }; - }, - 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 @@ -155,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 @@ -167,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 @@ -179,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 @@ -191,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 @@ -203,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> diff --git a/src/pages/Ecomerce/PendingOrders.vue b/src/pages/Ecomerce/PendingOrders.vue new file mode 100644 index 00000000..20737abb --- /dev/null +++ b/src/pages/Ecomerce/PendingOrders.vue @@ -0,0 +1,137 @@ +<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 'stores/app'; +import { storeToRefs } from 'pinia'; + +const jApi = inject('jApi'); +const { t } = useI18n(); +const { openConfirmationModal } = useVnConfirm(); +const { notify } = useNotify(); +const appStore = useAppStore(); +const { isHeaderMounted } = storeToRefs(appStore); +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 v-if="isHeaderMounted" 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> diff --git a/src/pages/Ecomerce/Ticket.vue b/src/pages/Ecomerce/Ticket.vue deleted file mode 100644 index 3d5386e7..00000000 --- a/src/pages/Ecomerce/Ticket.vue +++ /dev/null @@ -1,151 +0,0 @@ -<template> - <Teleport v-if="isHeaderMounted" 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'; -import { useAppStore } from 'stores/app'; -import { storeToRefs } from 'pinia'; - -export default { - name: 'OrdersConfirmedView', - setup() { - const appStore = useAppStore(); - const { isHeaderMounted } = storeToRefs(appStore); - return { isHeaderMounted }; - }, - 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> diff --git a/src/pages/Ecomerce/TicketView.vue b/src/pages/Ecomerce/TicketView.vue new file mode 100644 index 00000000..71799f6e --- /dev/null +++ b/src/pages/Ecomerce/TicketView.vue @@ -0,0 +1,78 @@ +<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 { useUserStore } from 'stores/user'; +import { useAppStore } from 'stores/app'; +import { storeToRefs } from 'pinia'; + +const { t } = useI18n(); +const jApi = inject('jApi'); +const route = useRoute(); +const userStore = useUserStore(); +const appStore = useAppStore(); +const { isHeaderMounted } = storeToRefs(appStore); + +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 v-if="isHeaderMounted" 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> diff --git a/src/router/routes.js b/src/router/routes.js index accf1700..21c2434c 100644 --- a/src/router/routes.js +++ b/src/router/routes.js @@ -37,13 +37,38 @@ const routes = [ { name: 'confirmedOrders', path: '/ecomerce/orders', - component: () => import('src/pages/Ecomerce/Orders.vue') + component: () => import('pages/Ecomerce/OrdersView.vue') + }, + { + name: 'ticket', + path: '/ecomerce/ticket/:id', + component: () => import('pages/Ecomerce/TicketView.vue') + }, + { + name: 'invoices', + path: '/ecomerce/invoices', + component: () => import('pages/Ecomerce/InvoicesView.vue') + }, + { + name: 'pendingOrders', + path: '/ecomerce/pending', + component: () => import('pages/Ecomerce/PendingOrders.vue') }, { name: 'catalog', path: '/ecomerce/catalog/:category?/:type?', component: () => import('pages/Ecomerce/Catalog.vue') }, + { + 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', diff --git a/src/stores/app.js b/src/stores/app.js index 53be4e73..bda79ab3 100644 --- a/src/stores/app.js +++ b/src/stores/app.js @@ -1,5 +1,8 @@ import { defineStore } from 'pinia'; import { jApi } from 'boot/axios'; +import useNotify from 'src/composables/useNotify.js'; + +const { notify } = useNotify(); export const useAppStore = defineStore('hedera', { state: () => ({ @@ -9,7 +12,8 @@ export const useAppStore = defineStore('hedera', { useRightDrawer: false, rightDrawerOpen: false, isHeaderMounted: false, - menuEssentialLinks: [] + menuEssentialLinks: [], + basketOrderId: null }), actions: { @@ -38,9 +42,40 @@ export const useAppStore = defineStore('hedera', { this.menuEssentialLinks = sectionTree; }, + 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 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) { + console.log('loadIntoBasket', orderId); + console.log('this.basketOrderId', this.basketOrderId); + if (this.basketOrderId !== orderId) { + localStorage.setItem('hederaBasket', orderId); + this.basketOrderId = orderId; + notify('orderLoadedIntoBasket', 'positive'); + } } } });