#7623 add updateRoute prop in VnPaginate #475

Merged
jorgep merged 12 commits from 7623-fix-redirection into dev 2024-06-26 12:22:00 +00:00
30 changed files with 3069 additions and 127 deletions
Showing only changes of commit 121204fee4 - Show all commits

View File

@ -2,7 +2,8 @@
import { ref, reactive } from 'vue'; import { ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
@ -18,33 +19,68 @@ const $props = defineProps({
}, },
}); });
const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const { notify } = useNotify(); const { notify } = useNotify();
const checked = ref(true);
const transferInvoiceParams = reactive({ const transferInvoiceParams = reactive({
id: $props.invoiceOutData?.id, id: $props.invoiceOutData?.id,
refFk: $props.invoiceOutData?.ref, refFk: $props.invoiceOutData?.ref,
}); });
const closeButton = ref(null);
const clientsOptions = ref([]);
const rectificativeTypeOptions = ref([]); const rectificativeTypeOptions = ref([]);
const siiTypeInvoiceOutsOptions = ref([]); const siiTypeInvoiceOutsOptions = ref([]);
const invoiceCorrectionTypesOptions = ref([]); const invoiceCorrectionTypesOptions = ref([]);
const closeForm = () => { const selectedClient = (client) => {
if (closeButton.value) closeButton.value.click(); transferInvoiceParams.selectedClientData = client;
};
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,
}; };
const transferInvoice = async () => {
try { try {
const { data } = await axios.post( if (checked.value && hasToInvoiceByAddress) {
'InvoiceOuts/transferInvoice', const response = await new Promise((resolve) => {
transferInvoiceParams 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'); notify(t('Transferred invoice'), 'positive');
closeForm(); const id = data?.[0];
router.push('InvoiceOutSummary', { id: data.id }); if (id) router.push({ name: 'InvoiceOutSummary', params: { id } });
} catch (err) { } catch (err) {
console.error('Error transfering invoice', err); console.error('Error transfering invoice', err);
} }
@ -52,22 +88,30 @@ const transferInvoice = async () => {
</script> </script>
<template> <template>
<FetchData
url="Clients"
@on-fetch="(data) => (clientsOptions = data)"
:filter="{ fields: ['id', 'name'], order: 'id', limit: 30 }"
auto-load
/>
<FetchData <FetchData
url="CplusRectificationTypes" url="CplusRectificationTypes"
:filter="{ order: 'description' }" :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 auto-load
/> />
<FetchData <FetchData
url="SiiTypeInvoiceOuts" url="SiiTypeInvoiceOuts"
:filter="{ where: { code: { like: 'R%' } } }" :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 auto-load
/> />
<FetchData <FetchData
@ -76,7 +120,7 @@ const transferInvoice = async () => {
auto-load auto-load
/> />
<FormPopup <FormPopup
@on-submit="transferInvoice()" @on-submit="makeInvoice()"
:title="t('Transfer invoice')" :title="t('Transfer invoice')"
:custom-submit-button-label="t('Transfer client')" :custom-submit-button-label="t('Transfer client')"
:default-cancel-button="false" :default-cancel-button="false"
@ -91,13 +135,18 @@ const transferInvoice = async () => {
option-value="id" option-value="id"
v-model="transferInvoiceParams.newClientFk" v-model="transferInvoiceParams.newClientFk"
:required="true" :required="true"
url="Clients"
:fields="['id', 'name', 'hasToInvoiceByAddress']"
auto-load
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem
v-bind="scope.itemProps"
@click="selectedClient(scope.opt)"
>
<QItemSection> <QItemSection>
<QItemLabel> <QItemLabel>
#{{ scope.opt?.id }} - #{{ scope.opt?.id }} - {{ scope.opt?.name }}
{{ scope.opt?.name }}
</QItemLabel> </QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -144,11 +193,23 @@ const transferInvoice = async () => {
:required="true" :required="true"
/> />
</VnRow> </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> </template>
</FormPopup> </FormPopup>
</template> </template>
<i18n> <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: es:
Transfer invoice: Transferir factura Transfer invoice: Transferir factura
Transfer client: Transferir cliente Transfer client: Transferir cliente
@ -157,4 +218,7 @@ es:
Class: Clase Class: Clase
Type: Tipo Type: Tipo
Transferred invoice: Factura transferida 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> </i18n>

View File

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

View File

@ -22,6 +22,10 @@ const $props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
clearable: {
type: Boolean,
default: true,
},
}); });
const { t } = useI18n(); const { t } = useI18n();
@ -88,7 +92,7 @@ const inputRules = [
<QIcon <QIcon
name="close" name="close"
size="xs" size="xs"
v-if="hover && value && !$attrs.disabled" v-if="hover && value && !$attrs.disabled && $props.clearable"
@click="value = null" @click="value = null"
></QIcon> ></QIcon>
<QIcon v-if="info" name="info"> <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> <script setup>
import { ref, toRefs, computed, watch } from 'vue'; import { ref, toRefs, computed, watch, onMounted } from 'vue';
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
const emit = defineEmits(['update:modelValue', 'update:options']); const emit = defineEmits(['update:modelValue', 'update:options']);
@ -58,6 +57,10 @@ const $props = defineProps({
type: [Number, String], type: [Number, String],
default: '30', default: '30',
}, },
focusOnMount: {
type: Boolean,
default: false,
},
}); });
const { t } = useI18n(); const { t } = useI18n();
@ -148,6 +151,10 @@ watch(modelValue, (newValue) => {
if (!myOptions.value.some((option) => option[optionValue.value] == newValue)) if (!myOptions.value.some((option) => option[optionValue.value] == newValue))
fetchFilter(newValue); fetchFilter(newValue);
}); });
onMounted(async () => {
if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300);
});
</script> </script>
<template> <template>

View File

@ -184,6 +184,7 @@ en:
minAmount: 'A minimum amount of 50 (VAT excluded) is required for your order minAmount: 'A minimum amount of 50 (VAT excluded) is required for your order
{ orderId } of { shipped } to receive it without additional shipping costs.' { orderId } of { shipped } to receive it without additional shipping costs.'
orderChanges: 'Order {orderId} of { shipped }: { changes }' 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 en: English
es: Spanish es: Spanish
fr: French fr: French
@ -203,6 +204,7 @@ es:
Te recomendamos amplíes para no generar costes extra, provocarán un incremento de tu tarifa. Te recomendamos amplíes para no generar costes extra, provocarán un incremento de tu tarifa.
¡Un saludo!' ¡Un saludo!'
orderChanges: 'Pedido {orderId} con llegada estimada día { landing }: { changes }' 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 en: Inglés
es: Español es: Español
fr: Francés fr: Francés
@ -222,6 +224,7 @@ fr:
Montant minimum nécessaire de 50 euros pour recevoir la commande { orderId } livraison { landing }. Montant minimum nécessaire de 50 euros pour recevoir la commande { orderId } livraison { landing }.
Merci.' Merci.'
orderChanges: 'Commande {orderId} livraison {landing} indisponible/s. Désolés pour le dérangement.' 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 en: Anglais
es: Espagnol es: Espagnol
fr: Français fr: Français
@ -240,6 +243,7 @@ pt:
minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido 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.' { orderId } do dia { landing } para recebê-lo sem custos de envio adicionais.'
orderChanges: 'Pedido { orderId } com chegada dia { landing }: { changes }' 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 en: Inglês
es: Espanhol es: Espanhol
fr: Francês fr: Francês

View File

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

View File

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

View File

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

View File

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

View File

@ -6,8 +6,8 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
import AccountDescriptorMenu from './AccountDescriptorMenu.vue'; import AccountDescriptorMenu from './AccountDescriptorMenu.vue';
import { useSession } from 'src/composables/useSession';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -19,7 +19,6 @@ const $props = defineProps({
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
@ -31,10 +30,6 @@ const filter = {
fields: ['id', 'nickname', 'name', 'role'], fields: ['id', 'nickname', 'name', 'role'],
include: { relation: 'role', scope: { fields: ['id', 'name'] } }, 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); const hasAccount = ref(false);
</script> </script>
@ -72,7 +67,8 @@ const hasAccount = ref(false);
<AccountDescriptorMenu :has-account="hasAccount" /> <AccountDescriptorMenu :has-account="hasAccount" />
</template> </template>
<template #before> <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> <template #error>
<div <div
class="absolute-full picture text-center q-pa-md flex flex-center" class="absolute-full picture text-center q-pa-md flex flex-center"
@ -87,7 +83,7 @@ const hasAccount = ref(false);
</div> </div>
</div> </div>
</template> </template>
</QImg> </VnImg>
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('account.card.nickname')" :value="entity.nickname" /> <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 VnInputDate from 'components/common/VnInputDate.vue';
import axios from 'axios'; 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 route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession(); // const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); // const token = getTokenMultimedia();
const claimStates = ref([]); const claimStates = ref([]);
const claimStatesCopy = ref([]); const claimStatesCopy = ref([]);
@ -97,9 +98,11 @@ const statesFilter = {
> >
<template #before> <template #before>
<QAvatar color="orange"> <QAvatar color="orange">
<QImg <VnImg
v-if="data.workerFk" 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" spinner-color="white"
/> />
</QAvatar> </QAvatar>

View File

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

View File

@ -16,14 +16,15 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { toDate, toCurrency } from 'src/filters'; import { toDate, toCurrency } from 'src/filters';
import { useSession } from 'composables/useSession'; // import { useSession } from 'composables/useSession';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const router = useRouter(); const router = useRouter();
const { getTokenMultimedia } = useSession(); // const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); // const token = getTokenMultimedia();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
@ -695,14 +696,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</template> </template>
<template #body-cell-picture="{ row }"> <template #body-cell-picture="{ row }">
<QTd> <QTd>
<QImg <VnImg :id="row.itemFk" size="50x50" class="image" />
:src="`/api/Images/catalog/50x50/${row.itemFk}/download?access_token=${token}`"
spinner-color="primary"
:ratio="1"
height="50px"
width="50px"
class="image"
/>
</QTd> </QTd>
</template> </template>
<template #body-cell-itemFk="{ row }"> <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 { useState } from 'src/composables/useState';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
import { useSession } from 'src/composables/useSession';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import axios from 'axios'; import axios from 'axios';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
@ -42,14 +41,12 @@ const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
const image = ref(null);
const regularizeStockFormDialog = ref(null); const regularizeStockFormDialog = ref(null);
const item = ref(null); const item = ref(null);
const available = ref(null); const available = ref(null);
@ -67,17 +64,10 @@ const warehouseFk = computed({
}); });
onMounted(async () => { onMounted(async () => {
await getItemAvatar();
warehouseFk.value = user.value.warehouseFk; warehouseFk.value = user.value.warehouseFk;
salixUrl.value = await getUrl(''); 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 data = ref(useCardDescription());
const setData = (entity) => { const setData = (entity) => {
if (!entity) return; if (!entity) return;

View File

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

View File

@ -1,17 +1,29 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { computed } from 'vue';
import VnCard from 'components/common/VnCard.vue'; import VnCard from 'components/common/VnCard.vue';
import TicketDescriptor from './TicketDescriptor.vue'; import TicketDescriptor from './TicketDescriptor.vue';
import TicketFilter from '../TicketFilter.vue'; import TicketFilter from '../TicketFilter.vue';
const { t } = useI18n();
const route = useRoute();
const routeName = computed(() => route.name);
const searchBarDataKeys = {
TicketSummary: 'TicketSummary',
TicketSale: 'TicketSale',
};
</script> </script>
<template> <template>
<VnCard <VnCard
data-key="Ticket" data-key="Ticket"
base-url="Tickets"
:descriptor="TicketDescriptor"
:filter-panel="TicketFilter" :filter-panel="TicketFilter"
search-data-key="TicketList" :descriptor="TicketDescriptor"
search-url="Tickets/filter" :search-data-key="searchBarDataKeys[routeName]"
searchbar-label="Search ticket" :search-custom-route-redirect="routeName"
searchbar-info="You can search by ticket id or alias" :searchbar-label="t('card.search')"
:searchbar-info="t('card.searchInfo')"
/> />
</template> </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 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 { toDate } from 'src/filters';
import { downloadFile } from 'src/composables/downloadFile'; import { downloadFile } from 'src/composables/downloadFile';
const {{id}} = useRoute(); const route = useRoute();
const quasar = useQuasar(); const quasar = useQuasar();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
@ -29,7 +29,7 @@ const thermographFilter = {
fields: ['id', 'name'], fields: ['id', 'name'],
}, },
}, },
where: { travelFk: id }, where: { travelFk: route.params.id },
order: ['created'], order: ['created'],
}; };

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ export default {
component: RouterView, component: RouterView,
redirect: { name: 'TicketMain' }, redirect: { name: 'TicketMain' },
menus: { menus: {
main: ['TicketList'], main: ['TicketList', 'TicketAdvance', 'TicketFuture'],
card: ['TicketBoxing', 'TicketSms', 'TicketSale', 'TicketLog'], card: ['TicketBoxing', 'TicketSms', 'TicketSale', 'TicketLog'],
}, },
children: [ children: [
@ -40,6 +40,24 @@ export default {
}, },
component: () => import('src/pages/Ticket/TicketCreate.vue'), 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'),
},
], ],
}, },
{ {