0
0
Fork 0

Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 7623-fix-redirection

This commit is contained in:
Jorge Penadés 2024-06-25 15:37:00 +02:00
commit 121204fee4
30 changed files with 3069 additions and 127 deletions

View File

@ -2,7 +2,8 @@
import { ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
import VnSelect from 'components/common/VnSelect.vue';
@ -18,33 +19,68 @@ const $props = defineProps({
},
});
const quasar = useQuasar();
const { t } = useI18n();
const router = useRouter();
const { notify } = useNotify();
const checked = ref(true);
const transferInvoiceParams = reactive({
id: $props.invoiceOutData?.id,
refFk: $props.invoiceOutData?.ref,
});
const closeButton = ref(null);
const clientsOptions = ref([]);
const rectificativeTypeOptions = ref([]);
const siiTypeInvoiceOutsOptions = ref([]);
const invoiceCorrectionTypesOptions = ref([]);
const closeForm = () => {
if (closeButton.value) closeButton.value.click();
const selectedClient = (client) => {
transferInvoiceParams.selectedClientData = client;
};
const transferInvoice = async () => {
const makeInvoice = async () => {
const hasToInvoiceByAddress =
transferInvoiceParams.selectedClientData.hasToInvoiceByAddress;
const params = {
id: transferInvoiceParams.id,
cplusRectificationTypeFk: transferInvoiceParams.cplusRectificationTypeFk,
invoiceCorrectionTypeFk: transferInvoiceParams.invoiceCorrectionTypeFk,
newClientFk: transferInvoiceParams.newClientFk,
refFk: transferInvoiceParams.refFk,
siiTypeInvoiceOutFk: transferInvoiceParams.siiTypeInvoiceOutFk,
makeInvoice: checked.value,
};
try {
const { data } = await axios.post(
'InvoiceOuts/transferInvoice',
transferInvoiceParams
);
if (checked.value && hasToInvoiceByAddress) {
const response = await new Promise((resolve) => {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Bill destination client'),
message: t('transferInvoiceInfo'),
},
})
.onOk(() => {
resolve(true);
})
.onCancel(() => {
resolve(false);
});
});
if (!response) {
console.log('entra cuando no checkbox');
return;
}
}
console.log('params: ', params);
const { data } = await axios.post('InvoiceOuts/transferInvoice', params);
console.log('data: ', data);
notify(t('Transferred invoice'), 'positive');
closeForm();
router.push('InvoiceOutSummary', { id: data.id });
const id = data?.[0];
if (id) router.push({ name: 'InvoiceOutSummary', params: { id } });
} catch (err) {
console.error('Error transfering invoice', err);
}
@ -52,22 +88,30 @@ const transferInvoice = async () => {
</script>
<template>
<FetchData
url="Clients"
@on-fetch="(data) => (clientsOptions = data)"
:filter="{ fields: ['id', 'name'], order: 'id', limit: 30 }"
auto-load
/>
<FetchData
url="CplusRectificationTypes"
:filter="{ order: 'description' }"
@on-fetch="(data) => (rectificativeTypeOptions = data)"
@on-fetch="
(data) => (
(rectificativeTypeOptions = data),
(transferInvoiceParams.cplusRectificationTypeFk = data.filter(
(type) => type.description == 'I Por diferencias'
)[0].id)
)
"
auto-load
/>
<FetchData
url="SiiTypeInvoiceOuts"
:filter="{ where: { code: { like: 'R%' } } }"
@on-fetch="(data) => (siiTypeInvoiceOutsOptions = data)"
@on-fetch="
(data) => (
(siiTypeInvoiceOutsOptions = data),
(transferInvoiceParams.siiTypeInvoiceOutFk = data.filter(
(type) => type.code == 'R4'
)[0].id)
)
"
auto-load
/>
<FetchData
@ -76,7 +120,7 @@ const transferInvoice = async () => {
auto-load
/>
<FormPopup
@on-submit="transferInvoice()"
@on-submit="makeInvoice()"
:title="t('Transfer invoice')"
:custom-submit-button-label="t('Transfer client')"
:default-cancel-button="false"
@ -91,13 +135,18 @@ const transferInvoice = async () => {
option-value="id"
v-model="transferInvoiceParams.newClientFk"
:required="true"
url="Clients"
:fields="['id', 'name', 'hasToInvoiceByAddress']"
auto-load
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItem
v-bind="scope.itemProps"
@click="selectedClient(scope.opt)"
>
<QItemSection>
<QItemLabel>
#{{ scope.opt?.id }} -
{{ scope.opt?.name }}
#{{ scope.opt?.id }} - {{ scope.opt?.name }}
</QItemLabel>
</QItemSection>
</QItem>
@ -144,11 +193,23 @@ const transferInvoice = async () => {
:required="true"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div>
<QCheckbox :label="t('Bill destination client')" v-model="checked" />
<QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>{{ t('transferInvoiceInfo') }}</QTooltip>
</QIcon>
</div>
</VnRow>
</template>
</FormPopup>
</template>
<i18n>
en:
checkInfo: New tickets from the destination customer will be generated in the consignee by default.
transferInvoiceInfo: Destination customer is marked to bill in the consignee
confirmTransferInvoice: The destination customer has selected to bill in the consignee, do you want to continue?
es:
Transfer invoice: Transferir factura
Transfer client: Transferir cliente
@ -157,4 +218,7 @@ es:
Class: Clase
Type: Tipo
Transferred invoice: Factura transferida
Bill destination client: Facturar cliente destino
transferInvoiceInfo: Los nuevos tickets del cliente destino, serán generados en el consignatario por defecto.
confirmTransferInvoice: El cliente destino tiene marcado facturar por consignatario, desea continuar?
</i18n>

View File

@ -11,6 +11,7 @@ import VnSelect from 'src/components/common/VnSelect.vue';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
import { useClipboard } from 'src/composables/useClipboard';
import VnImg from 'src/components/ui/VnImg.vue';
const state = useState();
const session = useSession();
@ -47,7 +48,6 @@ const darkMode = computed({
});
const user = state.getUser();
const token = session.getTokenMultimedia();
const warehousesData = ref();
const companiesData = ref();
const accountBankData = ref();
@ -149,10 +149,7 @@ function saveUserData(param, value) {
<div class="col column items-center q-mb-sm">
<QAvatar size="80px">
<QImg
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
spinner-color="white"
/>
<VnImg :id="user.id" collection="user" size="160x160" />
</QAvatar>
<div class="text-subtitle1 q-mt-md">

View File

@ -22,6 +22,10 @@ const $props = defineProps({
type: String,
default: '',
},
clearable: {
type: Boolean,
default: true,
},
});
const { t } = useI18n();
@ -88,7 +92,7 @@ const inputRules = [
<QIcon
name="close"
size="xs"
v-if="hover && value && !$attrs.disabled"
v-if="hover && value && !$attrs.disabled && $props.clearable"
@click="value = null"
></QIcon>
<QIcon v-if="info" name="info">

View File

@ -0,0 +1,97 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
const { t } = useI18n();
const $props = defineProps({
progress: {
type: Number, //Progress value (1.0 > x > 0.0)
required: true,
},
showDialog: {
type: Boolean,
required: true,
},
cancelled: {
type: Boolean,
required: false,
default: false,
},
});
const emit = defineEmits(['cancel', 'close']);
const dialogRef = ref(null);
const _showDialog = computed({
get: () => $props.showDialog,
set: (value) => {
if (value) dialogRef.value.show();
},
});
const _progress = computed(() => $props.progress);
const progressLabel = computed(() => `${Math.round($props.progress * 100)}%`);
const cancel = () => {
dialogRef.value.hide();
emit('cancel');
};
</script>
<template>
<QDialog ref="dialogRef" v-model="_showDialog" @hide="onDialogHide">
<QCard class="full-width dialog">
<QCardSection class="row">
<span class="text-h6">{{ t('Progress') }}</span>
<QSpace />
<QBtn icon="close" flat round dense @click="emit('close')" />
</QCardSection>
<QCardSection>
<div class="column">
<span>{{ t('Total progress') }}:</span>
<QLinearProgress
size="30px"
:value="_progress"
color="primary"
stripe
class="q-mt-sm q-mb-md"
>
<div class="absolute-full flex flex-center">
<QBadge
v-if="cancelled"
text-color="white"
color="negative"
:label="t('Cancelled')"
/>
<span v-else class="text-white text-subtitle1">
{{ progressLabel }}
</span>
</div>
</QLinearProgress>
<slot />
</div>
</QCardSection>
<QCardActions align="right">
<QBtn
v-if="!cancelled && progress < 1"
type="button"
flat
class="text-primary"
@click="cancel()"
>
{{ t('globals.cancel') }}
</QBtn>
</QCardActions>
</QCard>
</QDialog>
</template>
<i18n>
es:
Progress: Progreso
Total progress: Progreso total
Cancelled: Cancelado
</i18n>

View File

@ -1,6 +1,5 @@
<script setup>
import { ref, toRefs, computed, watch } from 'vue';
import { onMounted } from 'vue';
import { ref, toRefs, computed, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'src/components/FetchData.vue';
const emit = defineEmits(['update:modelValue', 'update:options']);
@ -58,6 +57,10 @@ const $props = defineProps({
type: [Number, String],
default: '30',
},
focusOnMount: {
type: Boolean,
default: false,
},
});
const { t } = useI18n();
@ -148,6 +151,10 @@ watch(modelValue, (newValue) => {
if (!myOptions.value.some((option) => option[optionValue.value] == newValue))
fetchFilter(newValue);
});
onMounted(async () => {
if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300);
});
</script>
<template>

View File

@ -184,6 +184,7 @@ en:
minAmount: 'A minimum amount of 50 (VAT excluded) is required for your order
{ orderId } of { shipped } to receive it without additional shipping costs.'
orderChanges: 'Order {orderId} of { shipped }: { changes }'
productNotAvailable: 'Verdnatura communicates: Your order {ticketFk} with reception date on {landed}. {notAvailables} not available. Sorry for the inconvenience.'
en: English
es: Spanish
fr: French
@ -203,6 +204,7 @@ es:
Te recomendamos amplíes para no generar costes extra, provocarán un incremento de tu tarifa.
¡Un saludo!'
orderChanges: 'Pedido {orderId} con llegada estimada día { landing }: { changes }'
productNotAvailable: 'Verdnatura le comunica: Pedido {ticketFk} con fecha de recepción {landed}. {notAvailables} no disponible/s. Disculpe las molestias.'
en: Inglés
es: Español
fr: Francés
@ -222,6 +224,7 @@ fr:
Montant minimum nécessaire de 50 euros pour recevoir la commande { orderId } livraison { landing }.
Merci.'
orderChanges: 'Commande {orderId} livraison {landing} indisponible/s. Désolés pour le dérangement.'
productNotAvailable: 'Verdnatura communique : Votre commande {ticketFk} avec date de réception le {landed}. {notAvailables} non disponible. Nous sommes désolés pour les inconvénients.'
en: Anglais
es: Espagnol
fr: Français
@ -240,6 +243,7 @@ pt:
minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido
{ orderId } do dia { landing } para recebê-lo sem custos de envio adicionais.'
orderChanges: 'Pedido { orderId } com chegada dia { landing }: { changes }'
productNotAvailable: 'Verdnatura comunica: Seu pedido {ticketFk} com data de recepção em {landed}. {notAvailables} não disponível/eis. Desculpe pelo transtorno.'
en: Inglês
es: Espanhol
fr: Francês

View File

@ -3,22 +3,26 @@ import { ref, computed, onMounted } from 'vue';
import { useSession } from 'src/composables/useSession';
const $props = defineProps({
collection: {
storage: {
type: [String, Number],
default: 'Images',
},
collection: {
type: String,
default: 'catalog',
},
size: {
type: String,
default: '200x200',
},
zoomSize: {
type: String,
required: true,
required: false,
default: 'lg',
},
id: {
type: Boolean,
default: false,
type: Number,
required: true,
},
});
const show = ref(false);
@ -26,9 +30,8 @@ const token = useSession().getTokenMultimedia();
const timeStamp = ref(`timestamp=${Date.now()}`);
const url = computed(
() =>
`/api/${$props.collection}/catalog/${$props.size}/${$props.id}/download?access_token=${token}&${timeStamp.value}`
`/api/${$props.storage}/${$props.collection}/${$props.size}/${$props.id}/download?access_token=${token}&${timeStamp.value}`
);
const emits = defineEmits(['refresh']);
const reload = (emit = false) => {
timeStamp.value = `timestamp=${Date.now()}`;
};
@ -41,20 +44,25 @@ onMounted(() => {});
<template>
<QImg :src="url" v-bind="$attrs" @click="show = !show" spinner-color="primary" />
<QDialog v-model="show" v-if="$props.zoomSize">
<QImg :src="url" class="img_zoom" v-bind="$attrs" spinner-color="primary" />
<QImg
:src="url"
size="full"
class="img_zoom"
v-bind="$attrs"
spinner-color="primary"
/>
</QDialog>
</template>
<style lang="scss" scoped>
.q-img {
cursor: zoom-in;
min-width: 50px;
}
.rounded {
border-radius: 50%;
}
.img_zoom {
width: 100%;
height: auto;
border-radius: 0%;
}
</style>

View File

@ -8,11 +8,8 @@ export default function (value, fractionSize = 2) {
const options = {
style: 'percent',
minimumFractionDigits: fractionSize,
maximumFractionDigits: fractionSize
maximumFractionDigits: fractionSize,
};
return new Intl.NumberFormat(locale, options)
.format(parseFloat(value));
}
return new Intl.NumberFormat(locale, options).format(parseFloat(value));
}

View File

@ -444,6 +444,8 @@ ticket:
sms: Sms
notes: Notes
sale: Sale
ticketAdvance: Advance tickets
futureTickets: Future tickets
list:
nickname: Nickname
state: State

View File

@ -443,6 +443,8 @@ ticket:
sms: Sms
notes: Notas
sale: Lineas del pedido
ticketAdvance: Adelantar tickets
futureTickets: Tickets a futuro
list:
nickname: Alias
state: Estado

View File

@ -6,8 +6,8 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
import AccountDescriptorMenu from './AccountDescriptorMenu.vue';
import { useSession } from 'src/composables/useSession';
import FetchData from 'src/components/FetchData.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const $props = defineProps({
id: {
@ -19,7 +19,6 @@ const $props = defineProps({
const route = useRoute();
const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const entityId = computed(() => {
return $props.id || route.params.id;
});
@ -31,10 +30,6 @@ const filter = {
fields: ['id', 'nickname', 'name', 'role'],
include: { relation: 'role', scope: { fields: ['id', 'name'] } },
};
function getAccountAvatar() {
const token = getTokenMultimedia();
return `/api/Images/user/160x160/${entityId.value}/download?access_token=${token}`;
}
const hasAccount = ref(false);
</script>
@ -72,7 +67,8 @@ const hasAccount = ref(false);
<AccountDescriptorMenu :has-account="hasAccount" />
</template>
<template #before>
<QImg :src="getAccountAvatar()" class="photo">
<!-- falla id :id="entityId.value" collection="user" size="160x160" -->
<VnImg :id="entityId" collection="user" size="160x160" class="photo">
<template #error>
<div
class="absolute-full picture text-center q-pa-md flex flex-center"
@ -87,7 +83,7 @@ const hasAccount = ref(false);
</div>
</div>
</template>
</QImg>
</VnImg>
</template>
<template #body="{ entity }">
<VnLv :label="t('account.card.nickname')" :value="entity.nickname" />

View File

@ -10,12 +10,13 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import axios from 'axios';
import { useSession } from 'src/composables/useSession';
// import { useSession } from 'src/composables/useSession';
import VnImg from 'src/components/ui/VnImg.vue';
const route = useRoute();
const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
// const { getTokenMultimedia } = useSession();
// const token = getTokenMultimedia();
const claimStates = ref([]);
const claimStatesCopy = ref([]);
@ -97,9 +98,11 @@ const statesFilter = {
>
<template #before>
<QAvatar color="orange">
<QImg
<VnImg
v-if="data.workerFk"
:src="`/api/Images/user/160x160/${data.workerFk}/download?access_token=${token}`"
:size="'160x160'"
:id="data.workerFk"
collection="user"
spinner-color="white"
/>
</QAvatar>

View File

@ -3,16 +3,14 @@ import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const route = useRoute();
const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const workers = ref([]);
const workersCopy = ref([]);
@ -143,10 +141,11 @@ const filterOptions = {
>
<template #prepend>
<QAvatar color="orange">
<QImg
:src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
spinner-color="white"
<VnImg
v-if="data.salesPersonFk"
:id="user.id"
collection="user"
spinner-color="white"
/>
</QAvatar>
</template>

View File

@ -16,14 +16,15 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useStateStore } from 'stores/useStateStore';
import { toDate, toCurrency } from 'src/filters';
import { useSession } from 'composables/useSession';
// import { useSession } from 'composables/useSession';
import { dashIfEmpty } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
import RightMenu from 'src/components/common/RightMenu.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const router = useRouter();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
// const { getTokenMultimedia } = useSession();
// const token = getTokenMultimedia();
const stateStore = useStateStore();
const { t } = useI18n();
@ -695,14 +696,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</template>
<template #body-cell-picture="{ row }">
<QTd>
<QImg
:src="`/api/Images/catalog/50x50/${row.itemFk}/download?access_token=${token}`"
spinner-color="primary"
:ratio="1"
height="50px"
width="50px"
class="image"
/>
<VnImg :id="row.itemFk" size="50x50" class="image" />
</QTd>
</template>
<template #body-cell-itemFk="{ row }">

View File

@ -13,7 +13,6 @@ import ItemDescriptorImage from 'src/pages/Item/Card/ItemDescriptorImage.vue';
import { useState } from 'src/composables/useState';
import useCardDescription from 'src/composables/useCardDescription';
import { useSession } from 'src/composables/useSession';
import { getUrl } from 'src/composables/getUrl';
import axios from 'axios';
import { dashIfEmpty } from 'src/filters';
@ -42,14 +41,12 @@ const quasar = useQuasar();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const state = useState();
const user = state.getUser();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const image = ref(null);
const regularizeStockFormDialog = ref(null);
const item = ref(null);
const available = ref(null);
@ -67,17 +64,10 @@ const warehouseFk = computed({
});
onMounted(async () => {
await getItemAvatar();
warehouseFk.value = user.value.warehouseFk;
salixUrl.value = await getUrl('');
});
const getItemAvatar = async () => {
const token = getTokenMultimedia();
const timeStamp = `timestamp=${Date.now()}`;
image.value = `/api/Images/catalog/200x200/${entityId.value}/download?access_token=${token}&${timeStamp}`;
};
const data = ref(useCardDescription());
const setData = (entity) => {
if (!entity) return;

View File

@ -16,17 +16,15 @@ import ItemListFilter from './ItemListFilter.vue';
import { useStateStore } from 'stores/useStateStore';
import { toDateFormat } from 'src/filters/date.js';
import { useSession } from 'composables/useSession';
import { dashIfEmpty } from 'src/filters';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useVnConfirm } from 'composables/useVnConfirm';
import axios from 'axios';
import RightMenu from 'src/components/common/RightMenu.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const router = useRouter();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const stateStore = useStateStore();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
@ -491,10 +489,9 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</template>
<template #body-cell-picture="{ row }">
<QTd>
<QImg
:src="`/api/Images/catalog/50x50/${row.id}/download?access_token=${token}`"
spinner-color="primary"
:ratio="1"
<VnImg
size="50x50"
:id="row.id"
height="50px"
width="50px"
class="image"

View File

@ -1,17 +1,29 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { computed } from 'vue';
import VnCard from 'components/common/VnCard.vue';
import TicketDescriptor from './TicketDescriptor.vue';
import TicketFilter from '../TicketFilter.vue';
const { t } = useI18n();
const route = useRoute();
const routeName = computed(() => route.name);
const searchBarDataKeys = {
TicketSummary: 'TicketSummary',
TicketSale: 'TicketSale',
};
</script>
<template>
<VnCard
data-key="Ticket"
base-url="Tickets"
:descriptor="TicketDescriptor"
:filter-panel="TicketFilter"
search-data-key="TicketList"
search-url="Tickets/filter"
searchbar-label="Search ticket"
searchbar-info="You can search by ticket id or alias"
:descriptor="TicketDescriptor"
:search-data-key="searchBarDataKeys[routeName]"
:search-custom-route-redirect="routeName"
:searchbar-label="t('card.search')"
:searchbar-info="t('card.searchInfo')"
/>
</template>

View File

@ -0,0 +1,96 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { toCurrency } from 'src/filters';
const $props = defineProps({
mana: {
type: Number,
default: null,
},
newPrice: {
type: Number,
default: 0,
},
});
const emit = defineEmits(['save', 'cancel']);
const { t } = useI18n();
const QPopupProxyRef = ref(null);
const save = () => {
emit('save');
QPopupProxyRef.value.hide();
};
const cancel = () => {
emit('cancel');
QPopupProxyRef.value.hide();
};
</script>
<template>
<QPopupProxy ref="QPopupProxyRef">
<div class="container">
<QSpinner v-if="!mana" color="orange" size="md" />
<div v-else>
<div class="header">Mana: {{ toCurrency(mana) }}</div>
<div class="q-pa-md">
<slot />
<div v-if="newPrice" class="column items-center q-mt-lg">
<span class="text-primary">{{ t('New price') }}</span>
<span class="text-subtitle1">
{{ toCurrency($props.newPrice) }}
</span>
</div>
</div>
</div>
<div class="row">
<QBtn
color="primary"
class="no-border-radius"
dense
style="width: 50%"
@click="cancel()"
>
{{ t('globals.cancel') }}
</QBtn>
<QBtn
color="primary"
class="no-border-radius"
dense
style="width: 50%"
@click="save()"
>
{{ t('globals.save') }}
</QBtn>
</div>
</div>
</QPopupProxy>
</template>
<style lang="scss" scoped>
.container {
background-color: $dark;
width: 230px;
}
.header {
height: 54px;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: $primary;
font-size: 1.2rem;
font-weight: bold;
min-width: 230px;
}
</style>
<i18n>
es:
New price: Nuevo precio
</i18n>

View File

@ -1 +1,779 @@
<template>Ticket sale</template>
<script setup>
import { onMounted, ref, computed, onUnmounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter, useRoute } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import TicketEditManaProxy from './TicketEditMana.vue';
import VnImg from 'src/components/ui/VnImg.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import TicketSaleMoreActions from './TicketSaleMoreActions.vue';
import TicketTransfer from './TicketTransfer.vue';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency, toPercentage, dashIfEmpty } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
import { useVnConfirm } from 'composables/useVnConfirm';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
const route = useRoute();
const router = useRouter();
const stateStore = useStateStore();
const { t } = useI18n();
const { notify } = useNotify();
const { openConfirmationModal } = useVnConfirm();
const editPriceProxyRef = ref(null);
const stateBtnDropdownRef = ref(null);
const arrayData = useArrayData('ticketData');
const { store } = arrayData;
const ticketConfig = ref(null);
const isLocked = ref(false);
const isTicketEditable = ref(false);
const sales = ref([]);
const itemsWithNameOptions = ref([]);
const editableStatesOptions = ref([]);
const selectedSales = ref([]);
const mana = ref(null);
const manaCode = ref('mana');
const ticketState = computed(() => store.data?.ticketState?.state?.code);
const transfer = ref({
lastActiveTickets: [],
sales: [],
});
watch(
() => route.params.id,
async () => await getSales()
);
const columns = computed(() => [
{
label: '',
name: 'statusIcons',
align: 'left',
},
{
label: '',
name: 'picture',
align: 'left',
},
{
label: t('ticketSale.visible'),
name: 'visible',
field: 'visible',
align: 'left',
sortable: true,
},
{
label: t('ticketSale.available'),
name: 'available',
field: 'available',
align: 'left',
sortable: true,
},
{
label: t('ticketSale.id'),
name: 'itemFk',
field: 'itemFk',
align: 'left',
sortable: true,
},
{
label: t('ticketSale.quantity'),
name: 'quantity',
field: 'quantity',
align: 'left',
sortable: true,
},
{
label: t('ticketSale.item'),
name: 'item',
field: 'item',
align: 'left',
sortable: true,
},
{
label: t('ticketSale.price'),
name: 'price',
field: 'price',
align: 'left',
sortable: true,
format: (val) => toCurrency(val),
},
{
label: t('ticketSale.discount'),
name: 'discount',
field: 'discount',
align: 'left',
sortable: true,
},
{
label: t('ticketSale.amount'),
name: 'amount',
field: 'amount',
align: 'left',
sortable: true,
format: (val) => toCurrency(val),
},
{
label: t('ticketSale.packaging'),
name: 'itemPackingTypeFk',
field: 'item',
align: 'left',
sortable: true,
format: (val) => dashIfEmpty(val?.itemPackingTypeFk),
},
{
label: '',
name: 'history',
align: 'left',
columnFilter: null,
},
]);
const getConfig = async () => {
try {
let filter = {
fields: ['daysForWarningClaim'],
};
const { data } = await axios.get(`TicketConfigs`, { filter });
ticketConfig.value = data;
} catch (err) {
console.error('Error getting ticket config', err);
}
};
const onSalesFetched = (salesData) => {
sales.value = salesData;
for (let sale of salesData) sale.amount = getSaleTotal(sale);
};
const getSales = async () => {
try {
const { data } = await axios.get(`Tickets/${route.params.id}/getSales`);
onSalesFetched(data);
} catch (err) {
console.error('Error fetching sales', err);
}
};
const getSaleTotal = (sale) => {
if (sale.quantity == null || sale.price == null) return null;
const price = sale.quantity * sale.price;
const discount = (sale.discount * price) / 100;
return price - discount;
};
const resetChanges = async () => {
arrayData.fetch({ append: false });
getSales();
};
const updateQuantity = async (sale) => {
try {
const payload = { quantity: sale.quantity };
await axios.post(`Sales/${sale.id}/updateQuantity`, payload);
notify('globals.dataSaved', 'positive');
} catch (err) {
console.error('Error updating quantity', err);
}
};
const addSale = async (sale) => {
try {
const payload = {
barcode: sale.itemFk,
quantity: sale.quantity,
};
const { data } = await axios.post(`tickets/${route.params.id}/addSale`, payload);
if (!data) return;
const newSale = data;
sale.id = newSale.id;
sale.image = newSale.item.image;
sale.subName = newSale.item.subName;
sale.concept = newSale.concept;
sale.quantity = newSale.quantity;
sale.discount = newSale.discount;
sale.price = newSale.price;
sale.item = newSale.item;
notify('globals.dataSaved', 'positive');
} catch (err) {
console.error('Error adding sale', err);
}
};
const changeQuantity = (sale) => {
if (
!sale.itemFk ||
sale.quantity == null ||
edit.value?.oldQuantity === sale.quantity
)
return;
if (!sale.id) return addSale(sale);
updateQuantity(sale);
};
const updateConcept = async (sale) => {
try {
const data = { newConcept: sale.concept };
await axios.post(`Sales/${sale.id}/updateConcept`, data);
notify('globals.dataSaved', 'positive');
} catch (err) {
console.error('Error updating concept', err);
}
};
const DEFAULT_EDIT = {
price: null,
discount: null,
sale: null,
sales: null,
oldQuantity: null,
};
const edit = ref({ ...DEFAULT_EDIT });
const usesMana = ref(null);
const getUsesMana = async () => {
const { data } = await axios.get('Sales/usesMana');
usesMana.value = data;
};
const getMana = async () => {
const { data } = await axios.get(`Tickets/${route.params.id}/getSalesPersonMana`);
mana.value = data;
await getUsesMana();
};
const selectedValidSales = computed(() => {
if (!sales.value) return;
return selectedSales.value.filter((sale) => sale.id != undefined);
});
const onOpenEditPricePopover = async (sale) => {
await getMana();
edit.value = {
sale: JSON.parse(JSON.stringify(sale)),
price: sale.price,
};
};
const onOpenEditDiscountPopover = async (sale) => {
await getMana();
if (isLocked.value) return;
if (sale) {
edit.value = {
sale: JSON.parse(JSON.stringify(sale)),
discount: sale.discount,
};
} else {
edit.value = {
discount: null,
sales: selectedValidSales.value,
};
}
};
const updatePrice = async (sale) => {
try {
const newPrice = edit.value.price;
if (newPrice != null && newPrice != sale.price) {
await axios.post(`Sales/${sale.id}/updatePrice`, { newPrice });
sale.price = newPrice;
edit.value = { ...DEFAULT_EDIT };
notify('globals.dataSaved', 'positive');
}
await getMana();
} catch (err) {
console.error('Error updating price', err);
}
};
const changeDiscount = (sale) => {
const newDiscount = edit.value.discount;
if (newDiscount != null && newDiscount != sale.discount) updateDiscount([sale]);
};
const updateDiscount = async (sales, newDiscount = null) => {
const saleIds = sales.map((sale) => sale.id);
const _newDiscount = newDiscount || edit.value.discount;
const params = {
salesIds: saleIds,
newDiscount: _newDiscount,
manaCode: manaCode.value,
};
await axios.post(`Tickets/${route.params.id}/updateDiscount`, params);
notify('globals.dataSaved', 'positive');
for (let sale of sales) sale.discount = _newDiscount;
edit.value = { ...DEFAULT_EDIT };
};
const getNewPrice = computed(() => {
if (edit.value?.sale) {
const sale = edit.value.sale;
let newDiscount = sale.discount;
let newPrice = edit.value.price || sale.price;
if (edit.value.discount != null) newDiscount = edit.value.discount;
if (edit.value.price != null) newPrice = edit.value.price;
const price = sale.quantity * newPrice;
const discount = (newDiscount * price) / 100;
return price - discount;
}
return 0;
});
const newOrderFromTicket = async () => {
try {
const { data } = await axios.post(`Orders/newFromTicket`, {
ticketFk: Number(route.params.id),
});
const routeData = router.resolve({ name: 'OrderCatalog', params: { id: data } });
window.open(routeData.href, '_blank');
} catch (err) {
console.error('Error creating new order', err);
}
};
const goToLog = (saleId) => {
//TODO: Redireccionar cuando exista la vista TicketLog
// router.push({
// name: 'TicketLog',
// params: {
// originId: route.params.id,
// changedModel: 'Sale',
// changedModelId: saleId,
// },
// });
};
const changeTicketState = async (val) => {
try {
stateBtnDropdownRef.value.hide();
const params = { ticketFk: route.params.id, code: val };
await axios.post('Tickets/state', params);
notify('globals.dataSaved', 'positive');
await resetChanges();
} catch (err) {
console.error('Error changing ticket state', err);
}
};
const removeSelectedSales = () => {
selectedSales.value.forEach((sale) => {
const index = sales.value.indexOf(sale);
sales.value.splice(index, 1);
});
};
const removeSales = async () => {
try {
const params = { sales: selectedValidSales.value, ticketId: store.data.id };
await axios.post('Sales/deleteSales', params);
removeSelectedSales();
notify('globals.dataSaved', 'positive');
} catch (err) {
console.error('Error deleting sales', err);
}
};
const insertRow = () => sales.value.push({ ...DEFAULT_EDIT });
const setTransferParams = async () => {
try {
const checkedSales = JSON.parse(JSON.stringify(selectedSales.value));
transfer.value = {
lastActiveTickets: [],
sales: checkedSales,
};
const params = { ticketId: store.data.id };
const { data } = await axios.get(
`clients/${store.data.clientFk}/lastActiveTickets`,
{
params,
}
);
transfer.value.lastActiveTickets = data;
} catch (err) {
console.error('Error setting transfer params', err);
}
};
onMounted(async () => {
stateStore.rightDrawer = true;
getConfig();
getSales();
});
onUnmounted(() => (stateStore.rightDrawer = false));
</script>
<template>
<FetchData
:url="`Tickets/${route.params.id}/isEditable`"
auto-load
@on-fetch="(data) => (isTicketEditable = data)"
/>
<FetchData
:url="`Tickets/${route.params.id}/isLocked`"
auto-load
@on-fetch="(data) => (isLocked = data)"
/>
<FetchData
url="Items/withName"
:filter="{ fields: ['id', 'name'], order: 'id DESC' }"
auto-load
@on-fetch="(data) => (itemsWithNameOptions = data)"
/>
<FetchData
url="States/editableStates"
:filter="{ fields: ['code', 'name', 'id', 'alertLevel'], order: 'name ASC' }"
auto-load
@on-fetch="(data) => (editableStatesOptions = data)"
/>
<VnSubToolbar>
<template #st-actions>
<QBtnGroup push class="q-gutter-x-sm" flat>
<QBtn
:label="t('ticketSale.ok')"
color="primary"
:disable="!isTicketEditable || ticketState === 'OK'"
@click="changeTicketState('OK')"
>
<QTooltip>{{ t(`Change ticket state to 'Ok'`) }}</QTooltip>
</QBtn>
<QBtnDropdown
ref="stateBtnDropdownRef"
color="primary"
:label="t('ticketSale.state')"
:disable="!isTicketEditable"
>
<VnSelect
:options="editableStatesOptions"
hide-selected
option-label="name"
option-value="code"
hide-dropdown-icon
focus-on-mount
@update:model-value="changeTicketState"
/>
</QBtnDropdown>
<TicketSaleMoreActions
:ticket="store.data"
:is-ticket-editable="isTicketEditable"
:sales="selectedValidSales"
:disable="!selectedSales.length"
:mana="mana"
:ticket-config="ticketConfig"
@get-mana="getMana()"
@update-discounts="updateDiscount"
/>
<QBtn
color="primary"
icon="delete"
:disable="!isTicketEditable || !selectedSales.length"
@click="
openConfirmationModal(
t('Continue anyway?'),
t('You are going to delete lines of the ticket'),
removeSales
)
"
>
<QTooltip>{{ t('Remove lines') }}</QTooltip>
</QBtn>
<QBtn
color="primary"
icon="vn:splitline"
:disable="!isTicketEditable || !selectedSales.length"
@click="setTransferParams()"
>
<QTooltip>{{ t('Transfer lines') }}</QTooltip>
<TicketTransfer
:transfer="transfer"
:ticket="store.data"
@refresh-data="resetChanges()"
/>
</QBtn>
</QBtnGroup>
</template>
</VnSubToolbar>
<RightMenu>
<template #right-panel>
<div
class="q-pa-md q-mb-md q-ma-md color-vn-text"
style="border: 2px solid black"
>
<QCardSection class="justify-center text-subtitle1" horizontal>
<span class="q-mr-xs color-vn-label"
>{{ t('ticketSale.subtotal') }}:
</span>
<span>{{ toCurrency(store.data?.totalWithoutVat) }}</span>
</QCardSection>
<QCardSection class="justify-center text-subtitle1" horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('ticketSale.tax') }}:
</span>
<span>{{
toCurrency(store.data?.totalWithVat - store.data?.totalWithoutVat)
}}</span>
</QCardSection>
<QCardSection
class="justify-center text-weight-bold text-subtitle1"
horizontal
>
<span class="q-mr-xs color-vn-label">
{{ t('ticketSale.total') }}:
</span>
<span>{{ toCurrency(store.data?.totalWithVat) }}</span>
</QCardSection>
</div>
</template>
</RightMenu>
<QTable
:rows="sales"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
selection="multiple"
v-model:selected="selectedSales"
:no-data-label="t('globals.noResults')"
>
<template #body-cell-statusIcons="{ row }">
<QTd class="q-gutter-x-xs">
<router-link
v-if="row.claim?.claimFk"
:to="{ name: 'ClaimBasicData', params: { id: row.claim?.claimFk } }"
>
<QIcon color="primary" name="vn:claims" size="xs">
<QTooltip>
{{ t('ticketSale.claim') }}:
{{ row.claim?.claimFk }}
</QTooltip>
</QIcon>
</router-link>
<QIcon v-if="row.visible < 0" color="primary" name="warning" size="xs">
<QTooltip>
{{ t('ticketSale.visible') }}: {{ row.visible || 0 }}
</QTooltip>
</QIcon>
<QIcon v-if="row.reserved" color="primary" name="vn:reserva" size="xs">
<QTooltip>
{{ t('ticketSale.reserved') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.itemShortage"
color="primary"
name="vn:unavailable"
size="xs"
>
<QTooltip>
{{ t('ticketSale.noVisible') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.hasComponentLack"
color="primary"
name="vn:components"
size="xs"
>
<QTooltip>
{{ t('ticketSale.hasComponentLack') }}
</QTooltip>
</QIcon>
</QTd>
</template>
<template #body-cell-picture="{ row }">
<QTd>
<div class="image-wrapper">
<VnImg :id="row.itemFk" class="rounded" />
</div>
</QTd>
</template>
<template #body-cell-visible="{ row }">
<QTd @click.stop>
<QBadge :color="row.visible < 0 ? 'alert' : 'transparent'" dense>
{{ row.visible }}
</QBadge>
</QTd>
</template>
<template #body-cell-available="{ row }">
<QTd @click.stop>
<QBadge :color="row.available < 0 ? 'alert' : 'transparent'" dense>
{{ row.available }}
</QBadge>
</QTd>
</template>
<template #body-cell-itemFk="{ row }">
<QTd @click.stop>
<div v-if="row.id">
<QBtn flat color="primary" dense>
{{ row.itemFk }}
</QBtn>
<ItemDescriptorProxy :id="row.itemFk" />
</div>
<VnSelect
v-else
:options="itemsWithNameOptions"
hide-selected
option-label="name"
option-value="id"
@update:model-value="changeQuantity(row)"
v-model="row.itemFk"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{ scope.opt?.name }}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QTd>
</template>
<template #body-cell-quantity="{ row }">
<QTd @click.stop>
<VnInput
v-if="isTicketEditable"
v-model.number="row.quantity"
@keyup.enter="changeQuantity(row)"
@blur="changeQuantity(row)"
@focus="edit.oldQuantity = row.quantity"
/>
<span v-else>{{ row.quantity }}</span>
</QTd>
</template>
<template #body-cell-item="{ row }">
<QTd class="col">
<div class="column">
<span>{{ row.concept }}</span>
<span class="color-vn-label">{{ row.item?.subName }}</span>
<FetchedTags v-if="row.item" :item="row.item" :max-length="6" />
<QPopupProxy v-if="row.id && isTicketEditable">
<VnInput v-model="row.concept" @change="updateConcept(row)" />
</QPopupProxy>
</div>
</QTd>
</template>
<template #body-cell-price="{ row }">
<QTd>
<template v-if="isTicketEditable && row.id">
<QBtn flat color="primary" dense @click="onOpenEditPricePopover(row)">
{{ toCurrency(row.price) }}
</QBtn>
<TicketEditManaProxy
ref="editPriceProxyRef"
:mana="mana"
:new-price="getNewPrice"
@save="updatePrice(row)"
>
<VnInput
v-model.number="edit.price"
:label="t('ticketSale.price')"
type="number"
/>
</TicketEditManaProxy>
</template>
<span v-else>{{ toCurrency(row.price) }}</span>
</QTd>
</template>
<template #body-cell-discount="{ row }">
<QTd>
<template v-if="!isLocked && row.id">
<QBtn
flat
color="primary"
dense
@click="onOpenEditDiscountPopover(row)"
>
{{ toPercentage(row.discount / 100) }}
</QBtn>
<TicketEditManaProxy
:mana="mana"
:new-price="getNewPrice"
@save="changeDiscount(row)"
>
<VnInput
v-model.number="edit.discount"
:label="t('ticketSale.discount')"
type="number"
/>
</TicketEditManaProxy>
</template>
<span v-else>{{ toPercentage(row.discount / 100) }}</span>
</QTd>
</template>
<template #body-cell-history="{ row }">
<QTd v-if="row.hasLogs">
<QBtn
@click.stop="goToLog(row.id)"
color="primary"
icon="history"
size="md"
flat
>
<QTooltip class="text-no-wrap">
{{ t('ticketSale.history') }}
</QTooltip>
</QBtn>
</QTd>
</template>
<template #bottom-row>
<QBtn
class="cursor-pointer fill-icon q-ml-md q-my-lg"
color="primary"
icon="add_circle"
size="md"
round
flat
:disable="!isTicketEditable"
@click="insertRow()"
>
<QTooltip>
{{ t('Add item') }}
</QTooltip>
</QBtn>
</template>
</QTable>
<QPageSticky :offset="[20, 20]">
<QBtn @click="newOrderFromTicket()" color="primary" fab icon="add" />
<QTooltip class="text-no-wrap">
{{ t('Add item to basket') }}
</QTooltip>
</QPageSticky>
</template>
<i18n>
es:
New item: Nuevo artículo
Add item to basket: Añadir artículo a la cesta
Change ticket state to 'Ok': Cambiar estado del ticket a 'Ok'
Remove lines: Eliminar líneas
Continue anyway?: ¿Continuar de todas formas?
You are going to delete lines of the ticket: Vas a eliminar lineas del ticket
Add item: Añadir artículo
Select lines to see the options: Selecciona líneas para ver las opciones
Transfer lines: Transferir líneas
</i18n>

View File

@ -0,0 +1,289 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useRouter } from 'vue-router';
import VnSmsDialog from 'components/common/VnSmsDialog.vue';
import TicketEditManaProxy from './TicketEditMana.vue';
import VnInput from 'src/components/common/VnInput.vue';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
import { toDateFormat } from 'src/filters/date';
import { useRole } from 'src/composables/useRole';
import { useVnConfirm } from 'composables/useVnConfirm';
const emit = defineEmits(['updateDiscounts', 'getMana']);
const props = defineProps({
disable: {
type: Boolean,
default: false,
},
isTicketEditable: {
type: Boolean,
default: false,
},
ticket: {
type: Object,
required: true,
default: () => {},
},
sales: {
type: Array,
default: () => [],
},
mana: {
type: Number,
default: null,
},
ticketConfig: {
type: Array,
default: () => [],
},
});
const router = useRouter();
const { t } = useI18n();
const { dialog } = useQuasar();
const { notify } = useNotify();
const role = useRole();
const btnDropdownRef = ref(null);
const { openConfirmationModal } = useVnConfirm();
const newDiscount = ref(null);
const ticket = computed(() => props.ticket);
const isClaimable = computed(() => {
if (ticket.value) {
const landedPlusWeek = new Date(ticket.value.landed);
landedPlusWeek.setDate(landedPlusWeek.getDate() + 7);
const hasClaimManagerRole = role.hasAny('claimManager');
return landedPlusWeek >= Date.vnNew() || hasClaimManagerRole;
}
return false;
});
const hasReserves = computed(() => props.sales.some((sale) => sale.reserved == true));
const sendSms = async (params) => {
await axios.post(`Tickets/${ticket.value.id}/sendSms`, params);
notify(t('SMS sent'), 'positive');
};
const showSmsDialog = (template) => {
const address = ticket.value.address;
const client = ticket.value.client;
const phone = address.mobile || address.phone || client.mobile || client.phone;
const items = props.sales.map((sale) => {
return `${sale.quantity} ${sale.concept}`;
});
const notAvailables = items.join(', ');
const data = {
ticketId: ticket.value.id,
destinationFk: ticket.value.clientFk,
destination: phone,
ticketFk: ticket.value.id,
created: ticket.value.updated,
landed: toDateFormat(ticket.value.landed),
notAvailables,
};
dialog({
component: VnSmsDialog,
componentProps: {
phone: phone,
template: template,
locale: client?.user?.lang ?? 'default_locale',
data: data,
promise: sendSms,
},
});
};
const calculateSalePrice = async () => {
if (!props.sales) return;
await axios.post(`Sales/recalculatePrice`, props.sales);
notify(t('globals.dataSaved'), 'positive');
};
const changeMultipleDiscount = () => {
const hasChanges = props.sales.some((sale) => {
return sale.discount != newDiscount.value;
});
if (newDiscount.value != null && hasChanges)
emit('updateDiscounts', props.sales, newDiscount.value);
btnDropdownRef.value.hide();
};
const createClaim = () => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const timeDifference = today.getTime() - new Date(ticket.value.landed).getTime();
const pastDays = Math.floor(timeDifference / 86400000);
if (pastDays >= props.ticketConfig[0].daysForWarningClaim)
openConfirmationModal(
t('Claim out of time'),
t('Do you want to continue?'),
onCreateClaimAccepted
);
else
openConfirmationModal(t('Do you want to create a claim?'), onCreateClaimAccepted);
};
const onCreateClaimAccepted = async () => {
try {
const params = { ticketId: ticket.value.id, sales: props.sales };
const { data } = await axios.post(`Claims/createFromSales`, params);
router.push({ name: 'ClaimBasicData', params: { id: data.id } });
} catch (error) {
console.error('Error creating claim: ', error);
}
};
const setReserved = async (reserved) => {
const params = { ticketId: ticket.value.id, sales: props.sales, reserved: reserved };
await axios.post(`Sales/reserve`, params);
props.sales.forEach((sale) => {
sale.reserved = reserved;
});
};
const createRefund = async (withWarehouse) => {
if (!props.sales) return;
const salesIds = props.sales.map((sale) => sale.id);
const params = { salesIds: salesIds, withWarehouse: withWarehouse, negative: true };
const { data } = await axios.post('Sales/clone', params);
const [refundTicket] = data;
notify(t('refundTicketCreated', { ticketId: refundTicket.id }), 'positive');
router.push({ name: 'TicketSale', params: { id: refundTicket.id } });
};
</script>
<template>
<QBtnDropdown
ref="btnDropdownRef"
color="primary"
:label="t('ticketSale.more')"
:disable="disable"
>
<template #label>
<QTooltip>{{ t('Select lines to see the options') }}</QTooltip>
</template>
<QList>
<QItem
v-if="ticket"
clickable
v-close-popup
v-ripple
@click="showSmsDialog('productNotAvailable')"
>
<QItemSection>
<QItemLabel>{{ t('Send shortage SMS') }}</QItemLabel>
</QItemSection>
</QItem>
<QItem
v-if="isTicketEditable"
clickable
v-close-popup
v-ripple
@click="calculateSalePrice()"
>
<QItemSection>
<QItemLabel>{{ t('Recalculate price') }}</QItemLabel>
</QItemSection>
</QItem>
<QItem clickable v-ripple @click="emit('getMana')">
<QItemSection>
<QItemLabel>{{ t('Update discount') }}</QItemLabel>
</QItemSection>
<TicketEditManaProxy :mana="props.mana" @save="changeMultipleDiscount()">
<VnInput
v-model.number="newDiscount"
:label="t('ticketSale.discount')"
type="number"
/>
</TicketEditManaProxy>
</QItem>
<QItem
v-if="isClaimable"
clickable
v-close-popup
v-ripple
@click="createClaim()"
>
<QItemSection>
<QItemLabel>{{ t('Add claim') }}</QItemLabel>
</QItemSection>
</QItem>
<QItem
v-if="isTicketEditable"
clickable
v-close-popup
v-ripple
@click="setReserved(true)"
>
<QItemSection>
<QItemLabel>{{ t('Mark as reserved') }}</QItemLabel>
</QItemSection>
</QItem>
<QItem
v-if="isTicketEditable && hasReserves"
clickable
v-close-popup
v-ripple
@click="setReserved(false)"
>
<QItemSection>
<QItemLabel>{{ t('Unmark as reserved') }}</QItemLabel>
</QItemSection>
</QItem>
<QItem clickable v-ripple>
<QItemSection>
<QItemLabel>{{ t('Refund...') }}</QItemLabel>
</QItemSection>
<QItemSection side>
<QIcon name="keyboard_arrow_right" />
</QItemSection>
<QMenu anchor="top end" self="top start" auto-close bordered>
<QList>
<QItem v-ripple clickable @click="createRefund(true)">
<QItemSection>
{{ t('with warehouse') }}
</QItemSection>
</QItem>
<QItem v-ripple clickable @click="createRefund(false)">
<QItemSection>
{{ t('without warehouse') }}
</QItemSection>
</QItem>
</QList>
</QMenu>
</QItem>
</QList>
</QBtnDropdown>
</template>
<i18n>
en:
refundTicketCreated: 'The following refund ticket have been created {ticketId}'
es:
SMS sent: SMS enviado
Send shortage SMS: Enviar SMS faltas
Recalculate price: Recalcular precio
Update discount: Actualizar descuento
Add claim: Crear reclamación
Mark as reserved: Marcar como reservado
Unmark as reserved: Desmarcar como reservado
Refund...: Abono...
with warehouse: con almacén
without warehouse: sin almacén
Claim out of time: Reclamación fuera de plazo
Do you want to continue?: ¿Desea continuar?
Do you want to create a claim?: ¿Quieres crear una reclamación?
refundTicketCreated: 'The following refund ticket have been created: {ticketId}'
</i18n>

View File

@ -0,0 +1,196 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnInput from 'src/components/common/VnInput.vue';
import { toDateFormat } from 'src/filters/date.js';
import axios from 'axios';
const $props = defineProps({
mana: {
type: Number,
default: null,
},
newPrice: {
type: Number,
default: 0,
},
transfer: {
type: Object,
default: () => {},
},
ticket: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['refreshData']);
const router = useRouter();
const { t } = useI18n();
const QPopupProxyRef = ref(null);
const _transfer = ref(null);
const transferLinesColumns = computed(() => [
{
label: t('ticketSale.id'),
name: 'itemFk',
field: 'itemFk',
align: 'left',
},
{
label: t('ticketSale.item'),
name: 'item',
field: 'concept',
align: 'left',
},
{
label: t('ticketSale.quantity'),
name: 'quantity',
field: 'quantity',
align: 'left',
},
]);
const destinationTicketColumns = computed(() => [
{
label: t('ticketSale.id'),
name: 'id',
field: 'id',
align: 'left',
},
{
label: t('ticketSale.shipped'),
name: 'item',
field: 'shipped',
align: 'left',
format: (val) => toDateFormat(val),
},
{
label: t('ticketSale.agency'),
name: 'agency',
field: 'agencyName',
align: 'left',
},
{
label: t('ticketSale.address'),
name: 'address',
field: 'address',
align: 'left',
},
]);
const transferSales = async (ticketId) => {
const params = {
ticketId: ticketId,
sales: $props.transfer.sales,
};
const { data } = await axios.post(
`tickets/${$props.ticket.id}/transferSales`,
params
);
if (data && data.id === $props.ticket.id) emit('refreshData');
else router.push({ name: 'TicketSale', params: { id: data.id } });
};
onMounted(() => (_transfer.value = $props.transfer));
</script>
<template>
<QPopupProxy ref="QPopupProxyRef">
<QCard class="q-px-md" style="display: flex">
<QTable
v-if="transfer.sales"
:rows="transfer.sales"
:columns="transferLinesColumns"
:title="t('Sales to transfer')"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
>
<template #body-cell-quantity="{ row }">
<QTd @click.stop>
<VnInput
v-model.number="row.quantity"
:clearable="false"
@keyup.enter="changeQuantity(row)"
@blur="changeQuantity(row)"
@focus="edit.oldQuantity = row.quantity"
style="max-width: 60px"
/>
</QTd>
</template>
</QTable>
<QSeparator vertical spaced />
<QTable
v-if="transfer.lastActiveTickets"
:rows="transfer.lastActiveTickets"
:columns="destinationTicketColumns"
:title="t('Destination ticket')"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
>
<template #body-cell-address="{ row }">
<QTd @click.stop>
<span>
{{ row.nickname }}
{{ row.name }}
{{ row.street }}
{{ row.postalCode }}
{{ row.city }}
</span>
<QTooltip>
{{ row.nickname }}
{{ row.name }}
{{ row.street }}
{{ row.postalCode }}
{{ row.city }}
</QTooltip>
</QTd>
</template>
<template #bottom>
<QForm class="q-mt-lg full-width">
<VnInput
v-model.number="_transfer.ticketId"
:label="t('Transfer to ticket')"
:clearable="false"
>
<template #append>
<QBtn
icon="keyboard_arrow_right"
color="primary"
@click="transferSales(_transfer.ticketId)"
style="width: 30px"
/>
</template>
</VnInput>
<QBtn
:label="t('New ticket')"
color="primary"
class="full-width q-my-lg"
@click="transferSales()"
/>
</QForm>
</template>
</QTable>
</QCard>
</QPopupProxy>
</template>
<i18n>
es:
Sales to transfer: Líneas a transferir
Destination ticket: Ticket destinatario
Transfer to ticket: Transferir a ticket
New ticket: Nuevo ticket
</i18n>

View File

@ -0,0 +1,710 @@
<script setup>
import { onMounted, ref, computed, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import VnProgress from 'src/components/common/VnProgressModal.vue';
import { dashIfEmpty, toCurrency } from 'src/filters';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useArrayData } from 'composables/useArrayData';
import useNotify from 'src/composables/useNotify.js';
import { useState } from 'src/composables/useState';
import { toDateFormat } from 'src/filters/date.js';
import axios from 'axios';
const state = useState();
const { t } = useI18n();
const { openConfirmationModal } = useVnConfirm();
const { notify } = useNotify();
const user = state.getUser();
const itemPackingTypesOptions = ref([]);
const zonesOptions = ref([]);
const selectedTickets = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'id':
case 'futureId':
case 'liters':
case 'futureLiters':
case 'lines':
case 'futureLines':
case 'totalWithVat':
case 'futureTotalWithVat':
case 'futureZone':
case 'notMovableLines':
case 'futureZoneFk':
return { [param]: value };
case 'ipt':
return { ipt: { like: `%${value}%` } };
case 'futureIpt':
return { futureIpt: { like: `%${value}%` } };
}
};
const userParams = reactive({});
const arrayData = useArrayData('AdvanceTickets', {
url: 'Tickets/getTicketsAdvance',
userParams: userParams,
exprBuilder: exprBuilder,
});
const { store } = arrayData;
const tickets = computed(() =>
(store.data || []).map((ticket, index) => ({ ...ticket, index: index }))
);
const applyColumnFilter = async (col) => {
try {
const paramKey = col.columnFilter?.filterParamKey || col.field;
userParams[paramKey] = col.columnFilter.filterValue;
await arrayData.addFilter({ params: userParams });
} catch (err) {
console.error('Error applying column filter', err);
}
};
const getInputEvents = (col) => {
return col.columnFilter.type === 'select'
? { 'update:modelValue': () => applyColumnFilter(col) }
: {
'keyup.enter': () => applyColumnFilter(col),
};
};
const ticketColumns = computed(() => [
{
label: '',
name: 'icons',
align: 'left',
columnFilter: null,
},
{
label: t('advanceTickets.ticketId'),
name: 'ticketId',
align: 'center',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
filterParamKey: 'id',
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('advanceTickets.ipt'),
name: 'ipt',
field: 'ipt',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
filterParamKey: 'ipt',
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemPackingTypesOptions.value,
'option-value': 'code',
'option-label': 'description',
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.state'),
name: 'state',
align: 'left',
sortable: true,
columnFilter: null,
},
{
label: t('advanceTickets.liters'),
name: 'liters',
field: 'liters',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('advanceTickets.lines'),
name: 'lines',
field: 'lines',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.import'),
field: 'import',
name: 'import',
align: 'left',
sortable: true,
},
{
label: t('advanceTickets.futureId'),
name: 'futureId',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
filterParamKey: 'futureId',
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('advanceTickets.futureIpt'),
name: 'futureIpt',
field: 'futureIpt',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
filterParamKey: 'futureIpt',
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemPackingTypesOptions.value,
'option-value': 'code',
'option-label': 'description',
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.futureState'),
name: 'futureState',
align: 'left',
sortable: true,
columnFilter: null,
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.futureLiters'),
name: 'futureLiters',
field: 'futureLiters',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.futureZone'),
name: 'futureZoneName',
field: 'futureZoneName',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
type: 'select',
filterValue: null,
filterParamKey: 'futureZoneFk',
event: getInputEvents,
attrs: {
options: zonesOptions.value,
'option-value': 'id',
'option-label': 'name',
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.notMovableLines'),
name: 'notMovableLines',
field: 'notMovableLines',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.futureLines'),
name: 'futureLines',
field: 'futureLines',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('advanceTickets.futureImport'),
name: 'futureImport',
align: 'left',
sortable: true,
columnFilter: null,
},
]);
const isLessThan50 = (totalWithVat) =>
parseInt(totalWithVat) > 0 && parseInt(totalWithVat) < 50;
const totalPriceColor = (totalWithVat) =>
isLessThan50(totalWithVat) ? 'warning' : 'transparent';
const getLanded = async (params) => {
try {
const query = `Agencies/getLanded`;
const { data } = await axios.get(query, { params });
if (!data) return;
return data;
} catch (error) {
notify(t('advanceTickets.noDeliveryZone'), 'negative');
console.error('Error getting landed', error);
}
};
const requestComponentUpdate = async (ticket, isWithoutNegatives) => {
const query = `tickets/${ticket.futureId}/componentUpdate`;
if (!ticket.landed) {
const newLanded = await getLanded({
shipped: userParams.dateToAdvance,
addressFk: ticket.futureAddressFk,
agencyModeFk: ticket.agencyModeFk ?? ticket.futureAgencyModeFk,
warehouseFk: ticket.futureWarehouseFk,
});
if (!newLanded) {
notify(t('advanceTickets.noDeliveryZone'), 'negative');
return;
}
ticket.landed = newLanded.landed;
ticket.zoneFk = newLanded.zoneFk;
}
const params = {
clientFk: ticket.futureClientFk,
nickname: ticket.nickname,
agencyModeFk: ticket.agencyModeFk ?? ticket.futureAgencyModeFk,
addressFk: ticket.futureAddressFk,
zoneFk: ticket.zoneFk ?? ticket.futureZoneFk,
warehouseFk: ticket.futureWarehouseFk,
companyFk: ticket.futureCompanyFk,
shipped: userParams.dateToAdvance,
landed: ticket.landed,
isDeleted: false,
isWithoutNegatives,
newTicket: ticket.id ?? undefined,
keepPrice: true,
};
return { query, params };
};
const moveTicketsAdvance = async () => {
try {
let ticketsToMove = [];
for (const ticket of selectedTickets.value) {
if (!ticket.id) {
try {
const { query, params } = await requestComponentUpdate(ticket, false);
axios.post(query, params);
} catch (e) {
console.error('Error moving ticket', e);
}
continue;
}
ticketsToMove.push({
originId: ticket.futureId,
destinationId: ticket.id,
originShipped: ticket.futureShipped,
destinationShipped: ticket.shipped,
workerFk: ticket.workerFk,
});
}
const params = { tickets: ticketsToMove };
await axios.post('Tickets/merge', params);
arrayData.fetch({ append: false });
selectedTickets.value = [];
if (ticketsToMove.length)
notify(t('advanceTickets.moveTicketSuccess'), 'positive');
} catch (error) {
console.error('Error moving tickets', error);
}
};
const progressLength = ref(0);
const progressPercentage = computed(() => {
if (progressLength.value === 0 || selectedTickets.value.length === 0) return 0;
return progressLength.value / selectedTickets.value.length;
});
const splitErrors = ref([]);
const showProgressDialog = ref(false);
const cancelProgress = ref(false);
const progressAdd = () => {
progressLength.value++;
if (progressLength.value === selectedTickets.value.length) {
notify(
t('advanceTickets.moveTicketSuccess', {
ticketsNumber: progressLength.value - splitErrors.value.length,
}),
'positive'
);
}
};
const splitTickets = async () => {
try {
showProgressDialog.value = true;
for (const ticket of selectedTickets.value) {
if (cancelProgress.value) break;
try {
const { query, params } = await requestComponentUpdate(ticket, true);
await axios.post(query, params);
progressAdd(ticket.futureId);
} catch (error) {
splitErrors.value.push({
id: ticket.futureId,
reason: error.response?.data?.error?.message,
});
progressAdd(ticket.futureId);
}
}
} catch (error) {
console.error('Error splitting tickets', error);
} finally {
arrayData.fetch({ append: false });
}
};
const resetProgressData = () => {
if (cancelProgress.value) cancelProgress.value = false;
progressLength.value = 0;
splitErrors.value = [];
selectedTickets.value = [];
};
const handleCloseProgressDialog = () => {
showProgressDialog.value = false;
resetProgressData();
};
const handleCancelProgress = () => (cancelProgress.value = true);
onMounted(async () => {
let today = Date.vnNew();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
userParams.dateFuture = tomorrow;
userParams.dateToAdvance = today;
userParams.warehouseFk = user.value.warehouseFk;
await arrayData.addFilter({ userParams });
});
</script>
<template>
<FetchData
url="itemPackingTypes"
:filter="{
fields: ['code', 'description'],
order: 'description ASC',
where: { isActive: true },
}"
auto-load
@on-fetch="(data) => (itemPackingTypesOptions = data)"
/>
<FetchData
url="Zones"
:filter="{
fields: ['id', 'name'],
order: 'name ASC',
}"
auto-load
@on-fetch="(data) => (zonesOptions = data)"
/>
<VnSearchbar
data-key="WeeklyTickets"
:label="t('weeklyTickets.search')"
:info="t('weeklyTickets.searchInfo')"
/>
<VnSubToolbar>
<template #st-data>
<QBtn
icon="keyboard_double_arrow_left"
color="primary"
class="q-mr-sm"
:disable="!selectedTickets.length"
@click.stop="
openConfirmationModal(
t('advanceTickets.advanceTicketTitle'),
t(`advanceTickets.advanceTitleSubtitle`, {
selectedTickets: selectedTickets.length,
}),
moveTicketsAdvance
)
"
>
<QTooltip>
{{ t('advanceTickets.advanceTickets') }}
</QTooltip>
</QBtn>
<QBtn
icon="alt_route"
color="primary"
:disable="!selectedTickets.length"
@click.stop="
openConfirmationModal(
t('advanceTickets.advanceWithoutNegativeTitle'),
t(`advanceTickets.advanceWithoutNegativeSubtitle`, {
selectedTickets: selectedTickets.length,
}),
splitTickets
)
"
>
<QTooltip>
{{ t('advanceTickets.advanceTicketsWithoutNegatives') }}
</QTooltip>
</QBtn>
</template>
</VnSubToolbar>
<QPage class="column items-center q-pa-md">
<QTable
:rows="tickets"
:columns="ticketColumns"
row-key="index"
selection="multiple"
v-model:selected="selectedTickets"
:pagination="{ rowsPerPage: 0 }"
:no-data-label="t('globals.noResults')"
style="max-width: 99%"
>
<template #header="props">
<QTr :props="props">
<QTh
class="horizontal-separator text-uppercase color-vn-label"
colspan="7"
translate
>
{{ t('advanceTickets.destination') }}
{{ toDateFormat(userParams.dateToAdvance) }}
</QTh>
<QTh
class="horizontal-separator text-uppercase color-vn-label"
colspan="9"
translate
>
{{ t('advanceTickets.origin') }}
{{ toDateFormat(userParams.dateFuture) }}
</QTh>
</QTr>
<QTr>
<QTh>
<QCheckbox v-model="props.selected" />
</QTh>
<QTh
v-for="(col, index) in ticketColumns"
:key="index"
:class="{ 'vertical-separator': col.name === 'futureId' }"
>
{{ col.label }}
</QTh>
</QTr>
</template>
<template #top-row="{ cols }">
<QTr>
<QTd />
<QTd
v-for="(col, index) in cols"
:key="index"
style="max-width: 100px"
>
<component
:is="col.columnFilter.component"
v-if="col.columnFilter"
v-model="col.columnFilter.filterValue"
v-bind="col.columnFilter.attrs"
v-on="col.columnFilter.event(col)"
dense
/>
</QTd>
</QTr>
</template>
<template #header-cell-availableLines="{ col }">
<QTh class="vertical-separator">
{{ col.label }}
</QTh>
</template>
<template #body-cell-icons="{ row }">
<QTd class="q-gutter-x-xs">
<QIcon
v-if="row.futureAgency !== row.agency && row.agency"
color="primary"
name="vn:agency-term"
size="xs"
>
<QTooltip class="column">
<span>
{{
t('advanceTickets.originAgency', {
agency: row.futureAgency,
})
}}
</span>
<span>
{{
t('advanceTickets.destinationAgency', {
agency: row.agency,
})
}}
</span>
</QTooltip>
</QIcon>
</QTd>
</template>
<template #body-cell-ticketId="{ row }">
<QTd>
<QBtn flat color="primary">
{{ row.id }}
<TicketDescriptorProxy :id="row.id" />
</QBtn>
</QTd>
</template>
<template #body-cell-state="{ row }">
<QTd>
<QBadge
text-color="black"
:color="row.classColor"
class="q-ma-none"
dense
>
{{ row.state }}
</QBadge>
</QTd>
</template>
<template #body-cell-import="{ row }">
<QTd>
<QBadge
:text-color="isLessThan50(row.totalWithVat) ? 'black' : 'white'"
:color="totalPriceColor(row.totalWithVat)"
class="q-ma-none"
dense
>
{{ toCurrency(row.totalWithVat || 0) }}
</QBadge>
</QTd>
</template>
<template #body-cell-futureId="{ row }">
<QTd class="vertical-separator">
<QBtn flat color="primary" dense>
{{ row.futureId }}
<TicketDescriptorProxy :id="row.futureId" />
</QBtn>
</QTd>
</template>
<template #body-cell-futureState="{ row }">
<QTd>
<QBadge
text-color="black"
:color="row.futureClassColor"
class="q-ma-none"
dense
>
{{ row.futureState }}
</QBadge>
</QTd>
</template>
<template #body-cell-futureImport="{ row }">
<QTd>
<QBadge
:text-color="
isLessThan50(row.futureTotalWithVat) ? 'black' : 'white'
"
:color="totalPriceColor(row.futureTotalWithVat)"
class="q-ma-none"
dense
>
{{ toCurrency(row.futureTotalWithVat || 0) }}
</QBadge>
</QTd>
</template>
</QTable>
<VnProgress
:progress="progressPercentage"
:cancelled="cancelProgress"
v-model:show-dialog="showProgressDialog"
@cancel="handleCancelProgress()"
@close="handleCloseProgressDialog()"
>
<div v-if="splitErrors.length" class="column">
<span>{{ t('advanceTickets.errorsList') }}:</span>
<span v-for="(error, index) in splitErrors" :key="index">
{{ error.id }}: {{ error.reason }}
</span>
</div>
</VnProgress>
</QPage>
</template>
<style scoped lang="scss">
.vertical-separator {
border-left: 4px solid white !important;
}
.horizontal-separator {
border-bottom: 4px solid white !important;
}
</style>

View File

@ -0,0 +1,533 @@
<script setup>
import { onMounted, ref, computed, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { dashIfEmpty, toCurrency } from 'src/filters';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useArrayData } from 'composables/useArrayData';
import { getDateQBadgeColor } from 'src/composables/getDateQBadgeColor.js';
import useNotify from 'src/composables/useNotify.js';
import { useState } from 'src/composables/useState';
import { toDateTimeFormat } from 'src/filters/date.js';
import axios from 'axios';
const state = useState();
const { t } = useI18n();
const { openConfirmationModal } = useVnConfirm();
const { notify } = useNotify();
const user = state.getUser();
const itemPackingTypesOptions = ref([]);
const selectedTickets = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'id':
return { id: value };
case 'futureId':
return { futureId: value };
case 'liters':
return { liters: value };
case 'lines':
return { lines: value };
case 'ipt':
return { ipt: { like: `%${value}%` } };
case 'futureIpt':
return { futureIpt: { like: `%${value}%` } };
case 'totalWithVat':
return { totalWithVat: value };
}
};
const userParams = reactive({
futureDated: Date.vnNew(),
originDated: Date.vnNew(),
warehouseFk: user.value.warehouseFk,
});
const arrayData = useArrayData('FutureTickets', {
url: 'Tickets/getTicketsFuture',
userParams: userParams,
exprBuilder: exprBuilder,
});
const { store } = arrayData;
const params = reactive({
futureDated: Date.vnNew(),
originDated: Date.vnNew(),
warehouseFk: user.value.warehouseFk,
});
const applyColumnFilter = async (col) => {
try {
const paramKey = col.columnFilter?.filterParamKey || col.field;
params[paramKey] = col.columnFilter.filterValue;
await arrayData.addFilter({ params });
} catch (err) {
console.error('Error applying column filter', err);
}
};
const getInputEvents = (col) => {
return col.columnFilter.type === 'select'
? { 'update:modelValue': () => applyColumnFilter(col) }
: {
'keyup.enter': () => applyColumnFilter(col),
};
};
const ticketColumns = computed(() => [
{
label: t('futureTickets.problems'),
name: 'problems',
align: 'left',
columnFilter: null,
},
{
label: t('futureTickets.ticketId'),
name: 'ticketId',
align: 'center',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
filterParamKey: 'id',
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('futureTickets.shipped'),
name: 'shipped',
align: 'left',
sortable: true,
columnFilter: null,
},
{
label: t('futureTickets.ipt'),
name: 'ipt',
field: 'ipt',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
filterParamKey: 'ipt',
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemPackingTypesOptions.value,
'option-value': 'code',
'option-label': 'description',
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('futureTickets.state'),
name: 'state',
align: 'left',
sortable: true,
columnFilter: null,
},
{
label: t('futureTickets.liters'),
name: 'liters',
field: 'liters',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('futureTickets.import'),
field: 'import',
name: 'import',
align: 'left',
sortable: true,
},
{
label: t('futureTickets.availableLines'),
name: 'lines',
field: 'lines',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('futureTickets.futureId'),
name: 'futureId',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
filterParamKey: 'futureId',
event: getInputEvents,
attrs: {
dense: true,
},
},
},
{
label: t('futureTickets.futureShipped'),
name: 'futureShipped',
align: 'left',
sortable: true,
columnFilter: null,
format: (val) => dashIfEmpty(val),
},
{
label: t('futureTickets.futureIpt'),
name: 'futureIpt',
field: 'futureIpt',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
filterParamKey: 'futureIpt',
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemPackingTypesOptions.value,
'option-value': 'code',
'option-label': 'description',
dense: true,
},
},
format: (val) => dashIfEmpty(val),
},
{
label: t('futureTickets.futureState'),
name: 'futureState',
align: 'left',
sortable: true,
columnFilter: null,
format: (val) => dashIfEmpty(val),
},
]);
const isLessThan50 = (totalWithVat) =>
parseInt(totalWithVat) > 0 && parseInt(totalWithVat) < 50;
const totalPriceColor = (totalWithVat) =>
isLessThan50(totalWithVat) ? 'warning' : 'transparent';
const moveTicketsFuture = async () => {
try {
const ticketsToMove = selectedTickets.value.map((ticket) => ({
originId: ticket.id,
destinationId: ticket.futureId,
originShipped: ticket.shipped,
destinationShipped: ticket.futureShipped,
workerFk: ticket.workerFk,
}));
let params = { tickets: ticketsToMove };
await axios.post('Tickets/merge', params);
notify(t('futureTickets.moveTicketSuccess'), 'positive');
selectedTickets.value = [];
arrayData.fetch({ append: false });
} catch (error) {
console.error('Error moving tickets to future', error);
}
};
onMounted(async () => {
await arrayData.fetch({ append: false });
});
</script>
<template>
<FetchData
url="itemPackingTypes"
:filter="{
fields: ['code', 'description'],
order: 'description ASC',
where: { isActive: true },
}"
auto-load
@on-fetch="(data) => (itemPackingTypesOptions = data)"
/>
<VnSearchbar
data-key="FutureTickets"
:label="t('Search ticket')"
:info="t('futureTickets.searchInfo')"
/>
<VnSubToolbar>
<template #st-data>
<QBtn
icon="keyboard_double_arrow_right"
color="primary"
:disable="!selectedTickets.length"
@click.stop="
openConfirmationModal(
t('futureTickets.moveTicketTitle'),
t(`futureTickets.moveTicketDialogSubtitle`, {
selectedTickets: selectedTickets.length,
}),
moveTicketsFuture
)
"
>
<QTooltip>
{{ t('futureTickets.futureTicket') }}
</QTooltip>
</QBtn>
</template>
</VnSubToolbar>
<QPage class="column items-center q-pa-md">
<QTable
:rows="store.data"
:columns="ticketColumns"
row-key="id"
selection="multiple"
v-model:selected="selectedTickets"
:pagination="{ rowsPerPage: 0 }"
:no-data-label="t('globals.noResults')"
style="max-width: 99%"
>
<template #header="props">
<QTr>
<QTh class="horizontal-separator" />
<QTh
class="horizontal-separator text-uppercase color-vn-label"
colspan="8"
translate
>
{{ t('futureTickets.origin') }}
</QTh>
<QTh
class="horizontal-separator text-uppercase color-vn-label"
colspan="4"
translate
>
{{ t('futureTickets.destination') }}
</QTh>
</QTr>
<QTr>
<QTh>
<QCheckbox v-model="props.selected" />
</QTh>
<QTh
v-for="(col, index) in ticketColumns"
:key="index"
:class="{ 'vertical-separator': col.name === 'futureId' }"
>
{{ col.label }}
</QTh>
</QTr>
</template>
<template #top-row="{ cols }">
<QTr>
<QTd />
<QTd
v-for="(col, index) in cols"
:key="index"
style="max-width: 100px"
>
<component
:is="col.columnFilter.component"
v-if="col.columnFilter"
v-model="col.columnFilter.filterValue"
v-bind="col.columnFilter.attrs"
v-on="col.columnFilter.event(col)"
dense
/>
</QTd>
</QTr>
</template>
<template #header-cell-availableLines="{ col }">
<QTh class="vertical-separator">
{{ col.label }}
</QTh>
</template>
<template #body-cell-problems="{ row }">
<QTd class="q-gutter-x-xs">
<QIcon
v-if="row.isTaxDataChecked === 0"
color="primary"
name="vn:no036"
size="xs"
>
<QTooltip>
{{ t('futureTickets.noVerified') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.hasTicketRequest"
color="primary"
name="vn:buyrequest"
size="xs"
>
<QTooltip>
{{ t('futureTickets.purchaseRequest') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.itemShortage"
color="primary"
name="vn:unavailable"
size="xs"
>
<QTooltip>
{{ t('futureTickets.noVisible') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.isFreezed"
color="primary"
name="vn:frozen"
size="xs"
>
<QTooltip>
{{ t('futureTickets.clientFrozen') }}
</QTooltip>
</QIcon>
<QIcon v-if="row.risk" color="primary" name="vn:risk" size="xs">
<QTooltip>
{{ t('futureTickets.risk') }}: {{ row.risk }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.hasComponentLack"
color="primary"
name="vn:components"
size="xs"
>
<QTooltip>
{{ t('futureTickets.componentLack') }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.hasRounding"
color="primary"
name="sync_problem"
size="xs"
>
<QTooltip>
{{ t('futureTickets.rounding') }}
</QTooltip>
</QIcon>
</QTd>
</template>
<template #body-cell-ticketId="{ row }">
<QTd>
<QBtn flat color="primary">
{{ row.id }}
<TicketDescriptorProxy :id="row.id" />
</QBtn>
</QTd>
</template>
<template #body-cell-shipped="{ row }">
<QTd>
<QBadge
text-color="black"
:color="getDateQBadgeColor(row.shipped)"
class="q-ma-none"
>
{{ toDateTimeFormat(row.shipped) }}
</QBadge>
</QTd>
</template>
<template #body-cell-state="{ row }">
<QTd>
<QBadge
text-color="black"
:color="row.classColor"
class="q-ma-none"
dense
>
{{ row.state }}
</QBadge>
</QTd>
</template>
<template #body-cell-import="{ row }">
<QTd>
<QBadge
:text-color="
totalPriceColor(row.totalWithVat) === 'warning'
? 'black'
: 'white'
"
:color="totalPriceColor(row.totalWithVat)"
class="q-ma-none"
dense
>
{{ toCurrency(row.totalWithVat || 0) }}
</QBadge>
</QTd>
</template>
<template #body-cell-futureId="{ row }">
<QTd class="vertical-separator">
<QBtn flat color="primary" dense>
{{ row.futureId }}
<TicketDescriptorProxy :id="row.futureId" />
</QBtn>
</QTd>
</template>
<template #body-cell-futureShipped="{ row }">
<QTd>
<QBadge
text-color="black"
:color="getDateQBadgeColor(row.futureShipped)"
class="q-ma-none"
>
{{ toDateTimeFormat(row.futureShipped) }}
</QBadge>
</QTd>
</template>
<template #body-cell-futureState="{ row }">
<QTd>
<QBadge
text-color="black"
:color="row.futureClassColor"
class="q-ma-none"
dense
>
{{ row.futureState }}
</QBadge>
</QTd>
</template>
</QTable>
</QPage>
</template>
<style scoped lang="scss">
.vertical-separator {
border-left: 4px solid white !important;
}
.horizontal-separator {
border-bottom: 4px solid white !important;
}
</style>

View File

@ -0,0 +1,82 @@
ticketSale:
id: Id
visible: Visible
available: Available
quantity: Quantity
item: Item
price: Price
discount: Disc
amount: Amount
packaging: Packaging
subtotal: Subtotal
tax: VAT
total: Total
history: History
claim: Claim
reserved: Reserved
noVisible: Not visible
hasComponentLack: Component lack
ok: Ok
state: State
more: More
shipped: Shipped
agency: Agency
address: Address
card:
search: Search tickets
searchInfo: You can search by ticket id or alias
advanceTickets:
origin: Origin
destination: Destination
originAgency: 'Origin agency: {agency}'
destinationAgency: 'Destination agency: {agency}'
ticketId: ID
ipt: IPT
state: State
liters: Liters
lines: Lines
import: Import
futureId: ID
futureIpt: IPT
futureState: State
futureLiters: Liters
futureZone: Zone
notMovableLines: Not movable
futureLines: Lines
futureImport: Import
advanceTickets: Advance tickets with negatives
advanceTicketTitle: Advance {selectedTickets} tickets
advanceTitleSubtitle: Advance {selectedTickets} tickets confirmation
noDeliveryZone: No delivery zone available for this landing date
moveTicketSuccess: 'Tickets moved successfully! {ticketsNumber}'
advanceTicketsWithoutNegatives: Advance tickets without negatives
advanceWithoutNegativeTitle: Advance tickets (without negatives)
advanceWithoutNegativeSubtitle: Advance {selectedTickets} tickets confirmation
errorsList: Errors list
futureTickets:
problems: Problems
ticketId: ID
shipped: Date
ipt: IPT
state: State
liters: Liters
import: Import
availableLines: Available lines
futureId: ID
futureShipped: Date
futureIpt: IPT
futureState: State
noVerified: No verified data
noVisible: Not visible
purchaseRequest: Purchase request
clientFrozen: Client frozen
componentLack: Component lack
rounding: Rounding
risk: Risk
origin: Origin
destination: Destination
moveTicketTitle: Move tickets
moveTicketDialogSubtitle: 'Do you want to move {selectedTickets} tickets to the future?'
moveTicketSuccess: Tickets moved successfully!
searchInfo: Search future tickets by date
futureTicket: Future tickets

View File

@ -1,2 +1,84 @@
Search ticket: Buscar ticket
advanceTickets:
origin: Origen
destination: Destinatario
originAgency: 'Agencia origen: {agency}'
destinationAgency: 'Agencia destino: {agency}'
ticketId: ID
ipt: IPT
state: Estado
liters: Litros
lines: Líneas
import: Importe
futureId: ID
futureIpt: IPT
futureState: Estado
futureLiters: Litros
futureZone: Zona
notMovableLines: No movibles
futureLines: Líneas
futureImport: Importe
advanceTickets: Adelantar tickets con negativos
advanceTicketTitle: Advance tickets
advanceTitleSubtitle: '¿Desea adelantar {selectedTickets} tickets?'
noDeliveryZone: No hay una zona de reparto disponible para la fecha de envío seleccionada
moveTicketSuccess: 'Tickets movidos correctamente {ticketsNumber}'
advanceTicketsWithoutNegatives: Adelantar tickets sin negativos
advanceWithoutNegativeTitle: Adelantar tickets (sin negativos)
advanceWithoutNegativeSubtitle: '¿Desea adelantar {selectedTickets} tickets?'
errorsList: Lista de errores
futureTickets:
problems: Problemas
ticketId: ID
shipped: Fecha
ipt: IPT
state: Estado
liters: Litros
import: Importe
availableLines: Líneas disponibles
futureId: ID
futureShipped: Fecha
futureIpt: IPT
futureState: Estado
noVerified: Sin datos comprobados
noVisible: No visible
purchaseRequest: Petición de compra
clientFrozen: Cliente congelado
risk: Riesgo
componentLack: Faltan componentes
rounding: Redondeo
origin: Origen
destination: Destino
moveTicketTitle: Mover tickets
moveTicketDialogSubtitle: '¿Desea mover {selectedTickets} tickets hacia el futuro?'
moveTicketSuccess: Tickets movidos correctamente
searchInfo: Buscar tickets por fecha
futureTicket: Tickets a futuro
Search ticket: Buscar tickets
You can search by ticket id or alias: Puedes buscar por id o alias del ticket
ticketSale:
id: Id
visible: Visible
available: Disponible
quantity: Cantidad
item: Artículo
price: Precio
discount: Dto
amount: Importe
packaging: Encajado
subtotal: Subtotal
tax: IVA
total: Total
history: Historial
claim: Reclamación
reserved: Reservado
noVisible: No visible
hasComponentLack: Faltan componentes
ok: Ok
state: Estado
more: Más
shipped: F. Envío
agency: Agencia
address: Consignatario
card:
search: Buscar tickets
searchInfo: Buscar tickets por identificador o alias

View File

@ -13,7 +13,7 @@ import useNotify from 'src/composables/useNotify.js';
import { toDate } from 'src/filters';
import { downloadFile } from 'src/composables/downloadFile';
const {{id}} = useRoute();
const route = useRoute();
const quasar = useQuasar();
const router = useRouter();
const { t } = useI18n();
@ -29,7 +29,7 @@ const thermographFilter = {
fields: ['id', 'name'],
},
},
where: { travelFk: id },
where: { travelFk: route.params.id },
order: ['created'],
};

View File

@ -1,14 +1,12 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useSession } from 'src/composables/useSession';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const quasar = useQuasar();
const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const counters = ref({
alquilerBandeja: { count: 0, id: 96001, title: 'CC Bandeja', isTray: true },
@ -35,10 +33,6 @@ onMounted(() => {
}
});
function getUrl(id) {
return `/api/Images/catalog/200x200/${id}/download?access_token=${token}`;
}
async function handleEvent(type, action, amount) {
const counter = counters.value[type].count;
let isOk = true;
@ -70,11 +64,7 @@ function confirm() {
<QList class="row q-mx-auto q-mt-xl">
<QItem v-for="(props, name) in counters" :key="name" class="col-6">
<QItemSection>
<QImg
:src="getUrl(props.id)"
width="130px"
@click="handleEvent(name, 'add')"
/>
<VnImg :id="props.id" width="130px" @click="handleEvent(name, 'add')" />
<p class="title">{{ props.title }}</p>
</QItemSection>
<QItemSection class="q-ma-none">

View File

@ -2,7 +2,6 @@
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
@ -10,6 +9,7 @@ import WorkerChangePasswordForm from 'src/pages/Worker/Card/WorkerChangePassword
import useCardDescription from 'src/composables/useCardDescription';
import { useState } from 'src/composables/useState';
import axios from 'axios';
import VnImg from 'src/components/ui/VnImg.vue';
const $props = defineProps({
id: {
@ -25,7 +25,6 @@ const $props = defineProps({
const route = useRoute();
const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const state = useState();
const user = state.getUser();
const changePasswordFormDialog = ref(null);
@ -73,11 +72,6 @@ watch(
}
);
function getWorkerAvatar() {
const token = getTokenMultimedia();
return `/api/Images/user/160x160/${entityId.value}/download?access_token=${token}`;
}
const data = ref(useCardDescription());
const setData = (entity) => {
if (!entity) return;
@ -155,7 +149,7 @@ const refetch = async () => await cardDescriptorRef.value.getData();
</QItem>
</template>
<template #before>
<QImg :src="getWorkerAvatar()" class="photo">
<VnImg :id="entityId" collection="user" size="160x160" class="photo">
<template #error>
<div
class="absolute-full picture text-center q-pa-md flex flex-center"
@ -170,7 +164,7 @@ const refetch = async () => await cardDescriptorRef.value.getData();
</div>
</div>
</template>
</QImg>
</VnImg>
</template>
<template #body="{ entity }">
<VnLv :label="t('worker.card.name')" :value="entity.user?.nickname" />

View File

@ -22,6 +22,7 @@ const searchBarDataKeys = {
ZoneEvents: 'ZoneEvents',
};
</script>
<template>
<VnCard
data-key="Zone"

View File

@ -11,7 +11,7 @@ export default {
component: RouterView,
redirect: { name: 'TicketMain' },
menus: {
main: ['TicketList'],
main: ['TicketList', 'TicketAdvance', 'TicketFuture'],
card: ['TicketBoxing', 'TicketSms', 'TicketSale', 'TicketLog'],
},
children: [
@ -40,6 +40,24 @@ export default {
},
component: () => import('src/pages/Ticket/TicketCreate.vue'),
},
{
name: 'TicketAdvance',
path: 'advance',
meta: {
title: 'ticketAdvance',
icon: 'keyboard_double_arrow_left',
},
component: () => import('src/pages/Ticket/TicketAdvance.vue'),
},
{
name: 'TicketFuture',
path: 'future',
meta: {
title: 'futureTickets',
icon: 'keyboard_double_arrow_right',
},
component: () => import('src/pages/Ticket/TicketFuture.vue'),
},
],
},
{