0
0
Fork 0

Merge pull request '7691-testToMaster' (!537) from 7691-testToMaster into master

Reviewed-on: verdnatura/salix-front#537
Reviewed-by: Guillermo Bonet <guillermo@verdnatura.es>
This commit is contained in:
Alex Moreno 2024-07-09 05:39:55 +00:00
commit ac8c3d86fc
132 changed files with 8717 additions and 2313 deletions

View File

@ -1,3 +1,169 @@
# Version 24.28 - 2024-07-09
### Added 🆕
- Change header titles style by:wbuezas
- chore: refs #7436 fix e2e (origin/7436-showQCheckbox) by:jorgep
- feat: #7196 eslint (origin/7196-cjsToEsm) by:jgallego
- feat: adapt tu VnTable → CrudModel by:alexm
- feat(CustomerFIlter): use correct table by:alexm
- feat(customerList): add searchbar by:alexm
- feat: customerList is customerExtendedList by:alexm
- feat: fixes #7196 by:jgallego
- feat: refs #6739 transferInvoice new checkbox and functionality by:Jon
- feat: refs #6825 create vnTable and add in CustomerExtendedList by:alexm
- feat: refs #6825 create vnTableColumn, cardActions by:alexm
- feat: refs #6825 fix modes by:alexm
- feat: refs #6825 qchip color by:alexm
- feat: refs #6825 right filter panel (6825-vnTable) by:alexm
- feat: refs #6825 scroll for table mode by:alexm
- feat: refs #6825 share filters, create popup by:alexm
- feat: refs #6825 VnComponent mix component and attrs Form to create new row by:alexm
- feat: refs #6825 VnTableFilter and VnPanelFilter init by:alexm
- feat: refs #6826 added rol summary link by:Jon
- feat: refs #6896 created VnImg and added to order module by:Jon
- feat: refs #6896 new filters by:Jon
- feat: refs #7129 fix some code and add order by:pablone
- feat: refs #7436 show checkbox by:jorgep
- feat: refs #7545 Deleted hasIncoterms client column (origin/7545-hasIncoterms) by:guillermo
- feat(TicketService): use correct format by:alexm
- feat(url): sepate filters by:alexm
- feat(VnFilter): merge objects by:alexm
- feat(VnTable): is-editable and use-model. fix: checkbox by:alexm
- feat(VnTable): refs #6825 actions sticky by:alexm
- feat(VnTable): refs #6825 addInWhere by:alexm
- feat(VnTable): refs #6825 dinamic columns by:alexm
- feat(VnTable): refs #6825 execute function when create by:alexm
- feat(VnTable): refs #6825 fix ellipsis and add titles by:alexm
- feat(VnTable): refs #6825 merge where's by:alexm
- feat(VnTable): refs #6825 move to folder. fix checkboxs by:alexm
- feat(VnTable): refs #6825 remove field prop. Add actions in table by:alexm
- feat(VnTable): refs #6825 use checkbox if startsWith 'is' or 'has' by:alexm
- feat(VnTable): refs #6825 VnTableChip component by:alexm
- feat(vnTable): reload data when change url by:alexm
- feat(WorkerFormation): add columnFilter by:alexm
- feat(WorkerFormation): is-editable and use-model by:alexm
- fix: notify icon style by:Javier Segarra
- refactor: refs #6896 fixed styles by:Jon
- Revert "feat: fixes #7196" by:alexm
- style: refs #6464 changed checkbox and qbtn styles by:Jon
### Changed 📦
- perf: Remove div.col by:Javier Segarra
- perf: remove ItemPicture by:Javier Segarra
- perf: replace ItemPicture in favour of VnImg by:Javier Segarra
- refactor by:wbuezas
- refactor: refs #5447 changed warehouse filter by:Jon
- refactor: refs #5447 changed warehouse out filter behavior by:Jon
- refactor: refs #5447 fixed filter if continent not selected by:Jon
- refactor: refs #5447 fix request by:Jon
- refactor: refs #5447 refactor filters by:Jon
- refactor: refs #6739 changed invoice functions' name by:Jon
- refactor: refs #6739 changed router.push by:Jon
- refactor: refs #6739 deleted useless const by:Jon
- refactor: refs #6739 fix redirect transferInvoice by:Jon
- refactor: refs #6739 new confirmation window by:Jon
- refactor: refs #6739 requested changes by:Jon
- refactor: refs #6739 updated transferInvoice function by:Jon
- refactor: refs #6896 changes requested in PR by:Jon
- refactor: refs #6896 end migration orders by:Jon
- refactor: refs #6896 fixed styles by:Jon
- refactor: refs #6896 fix qdrawer by:Jon
- refactor: refs #6896 refactor VnImg by:Jon
- refactor: refs #6896 requested changes by:Jon
- refactor: refs #6977 fix VnImg props (origin/6977-ClonedURL) by:Jon
- refactor: refs #6977 refactor VnImg by:Jon
- refactor: refs #6977 use VnImg by:Jon
- refactors by:alexm
### Fixed 🛠️
- chore: refs #7436 fix e2e (origin/7436-showQCheckbox) by:jorgep
- feat: fixes #7196 by:jgallego
- feat: refs #6825 fix modes by:alexm
- feat: refs #7129 fix some code and add order by:pablone
- feat(VnTable): is-editable and use-model. fix: checkbox by:alexm
- feat(VnTable): refs #6825 fix ellipsis and add titles by:alexm
- feat(VnTable): refs #6825 move to folder. fix checkboxs by:alexm
- fix(ArrayData): refs #6825 router.replace and use filter.where by:alexm
- fix: bug replace by:alexm
- fix: column hidden v-if by:Javier Segarra
- fix: comment 4 by:Javier Segarra
- fix: comments by:Javier Segarra
- fix: cypress.config to mjs by:alexm
- fix(EntryBuys): fix VnSubtoolbar by:alexm
- fixes: fix vnFilter params and redirect by:alexm
- fix: fix warnings by:alexm
- fix: invoiceDueDay test by:alexm
- fix log view not refreshing when changing id param by:wbuezas
- fix: map selected by:Javier Segarra
- fix: merge dev by:Javier Segarra
- fix: notify icon style by:Javier Segarra
- fix: point 1 by:Javier Segarra
- fix: point 3 by:Javier Segarra
- fix: refs #5447 deleted console.log by:Jon
- fix: refs 6464 deleted useless class in checkbox by:Jon
- fix: refs #6464 fix error isLoading by:Jon
- fix: refs #6739 changed checkbox field by:Jon
- fix: refs #6825 css by:carlossa
- fix: refs #6826 fix redirect by:Jon
- fix: refs #6826 fix roleDescriptor by:Jon
- fix: refs #7129 fix e2e by:pablone
- fix: refs #7129 fix module routes by:pablone
- fix: refs #7129 fix some issues on load and tools by:pablone
- fix: refs #7129 remove consoleLog by:pablone
- fix: refs #7129 remove fix from claim lines by:pablone
- fix: refs #7274 fix duplicate rows by:jorgep
- fix: refs #7433 skeleton by:jorgep
- fix: refs #7623 bugs & tests by:jorgep
- fix: refs #7623 disable router update by:jorgep
- fix: refs #7623 redirect by:jorgep
- fix: refs #7623 test by:jorgep
- fix: refs #7623 update add updateRoute prop in VnPaginate by:jorgep
- fix: refs #7623 updating skip param by:jorgep
- fix: revert cypress mjs by:alexm
- fix: SkeletonTable by:alexm
- fix: state translations by:Javier Segarra
- fix: ticket order by:Javier Segarra
- fix(ticket router): typo by:alexm
- fix(TicketService): pay use selected by:alexm
- fix: TravelLog by:Javier Segarra
- fix(url): filter by:alexm
- fix(url): redirect by:alexm
- fix(VnFilter): filter with params by:alexm
- fix(VnFilterPanel): remove key by:alexm
- fix(VnTable): create scss by:alexm
- fix(VnTable): duplicate fetch by:alexm
- fix(VnTable): Qtable v-bind by:alexm
- fix(VnTable): refs #6825 checkbox align and color by:alexm
- fix(VnTable): refs #6825 fix click sticky column by:alexm
- fix(VnTable): refs #6825 fix events and css by:alexm
- fix(VnTable): refs #6825 VnInputDate by:alexm
- fix(VnTable): showLabel by:alexm
- fix(VnTable): warns by:alexm
- fix: WorkerNotificationsManager test by:alexm
- fix: WorkerSelect option format by:Javier Segarra
- refactor: refs #5447 fixed filter if continent not selected by:Jon
- refactor: refs #5447 fix request by:Jon
- refactor: refs #6739 fix redirect transferInvoice by:Jon
- refactor: refs #6896 fixed styles by:Jon
- refactor: refs #6896 fix qdrawer by:Jon
- refactor: refs #6977 fix VnImg props (origin/6977-ClonedURL) by:Jon
- refs #6504 fix formModel claimFilter claimCard (origin/6504-fixCardClaim) by:carlossa
- refs #7406 fix components by:carlossa
- refs #7406 fix pr by:carlossa
- refs #7406 fix props by:carlossa
- refs #7406 fix Tb components create by:carlossa
- refs #7406 fix trad by:carlossa
- refs #7406 fix url by:carlossa
- refs #7406 fix VnTable columns by:carlossa
- refs #7409 fix balance and formation by:carlossa
- refs #7409 fix trad by:carlossa
- Revert "feat: fixes #7196" by:alexm
- test: fix intermitent e2e by:alexm
- test: fix vnSearchbar adapt to vnTable (origin/7648_dev_customerEntries) by:alexm
# Version 24.24 - 2024-06-11
### Added 🆕

View File

@ -1,6 +1,6 @@
{
"name": "salix-front",
"version": "24.26.2",
"version": "24.28.1",
"description": "Salix frontend",
"productName": "Salix",
"author": "Verdnatura",

View File

@ -1,11 +1,10 @@
import axios from 'axios';
import { Notify } from 'quasar';
import { useSession } from 'src/composables/useSession';
import { Router } from 'src/router';
import { i18n } from './i18n';
import useNotify from 'src/composables/useNotify.js';
const session = useSession();
const { t } = i18n.global;
const { notify } = useNotify();
axios.defaults.baseURL = '/api/';
@ -27,10 +26,7 @@ const onResponse = (response) => {
const isSaveRequest = method === 'patch';
if (isSaveRequest) {
Notify.create({
message: t('globals.dataSaved'),
type: 'positive',
});
notify('globals.dataSaved', 'positive');
}
return response;
@ -67,10 +63,7 @@ const onResponseError = (error) => {
return Promise.reject(error);
}
Notify.create({
message: t(message),
type: 'negative',
});
notify(message, 'negative');
return Promise.reject(error);
};

View File

@ -27,7 +27,6 @@ export default {
this.$el.addEventListener('keyup', function (evt) {
if (evt.key === 'Enter') {
const input = evt.target;
console.log('input', input);
if (input.type == 'textarea' && evt.shiftKey) {
evt.preventDefault();
let { selectionStart, selectionEnd } = input;

View File

@ -67,6 +67,10 @@ const $props = defineProps({
default: '',
description: 'It is used for redirect on click "save and continue"',
},
hasSubtoolbar: {
type: Boolean,
default: true,
},
});
const isLoading = ref(false);
@ -75,6 +79,7 @@ const originalData = ref();
const vnPaginateRef = ref();
const formData = ref();
const saveButtonRef = ref(null);
const watchChanges = ref();
const formUrl = computed(() => $props.url);
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
@ -89,6 +94,7 @@ defineExpose({
saveChanges,
getChanges,
formData,
vnPaginateRef,
});
async function fetch(data) {
@ -97,19 +103,26 @@ async function fetch(data) {
data.map((d) => (d.$index = $index++));
}
originalData.value = data && JSON.parse(JSON.stringify(data));
formData.value = data && JSON.parse(JSON.stringify(data));
watch(formData, () => (hasChanges.value = true), { deep: true });
resetData(data);
emit('onFetch', data);
return data;
}
function resetData(data) {
if (!data) return;
originalData.value = JSON.parse(JSON.stringify(data));
formData.value = JSON.parse(JSON.stringify(data));
if (watchChanges.value) watchChanges.value(); //destoy watcher
watchChanges.value = watch(formData, () => (hasChanges.value = true), { deep: true });
}
async function reset() {
await fetch(originalData.value);
hasChanges.value = false;
}
// eslint-disable-next-line vue/no-dupe-keys
function filter(value, update, filterOptions) {
update(
() => {
@ -271,8 +284,9 @@ function isEmpty(obj) {
if (obj.length > 0) return false;
}
async function reload() {
vnPaginateRef.value.fetch();
async function reload(params) {
const data = await vnPaginateRef.value.fetch(params);
fetch(data);
}
watch(formUrl, async () => {
@ -284,10 +298,11 @@ watch(formUrl, async () => {
<VnPaginate
:url="url"
:limit="limit"
v-bind="$attrs"
@on-fetch="fetch"
@on-change="resetData"
:skeleton="false"
ref="vnPaginateRef"
v-bind="$attrs"
>
<template #body v-if="formData">
<slot
@ -298,8 +313,8 @@ watch(formUrl, async () => {
></slot>
</template>
</VnPaginate>
<SkeletonTable v-if="!formData" />
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<SkeletonTable v-if="!formData" :columns="$attrs.columns?.length" />
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubtoolbar">
<QBtnGroup push style="column-gap: 10px">
<slot name="moreBeforeActions" />
<QBtn

View File

@ -83,6 +83,10 @@ const $props = defineProps({
default: '',
description: 'It is used for redirect on click "save and continue"',
},
reload: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed(
@ -201,6 +205,7 @@ async function save() {
if ($props.urlCreate) notify('globals.dataCreated', 'positive');
updateAndEmit('onDataSaved', formData.value, response?.data);
if ($props.reload) await arrayData.fetch({});
} catch (err) {
console.error(err);
notify('errors.writeRequest', 'negative');
@ -257,7 +262,7 @@ defineExpose({
<template>
<div class="column items-center full-width">
<QForm
v-if="formData"
@submit="save"
@reset="reset"
class="q-pa-md"
@ -265,11 +270,13 @@ defineExpose({
>
<QCard>
<slot
v-if="formData"
name="form"
:data="formData"
:validate="validate"
:filter="filter"
/>
<SkeletonForm v-else/>
</QCard>
</QForm>
</div>
@ -332,7 +339,7 @@ defineExpose({
</QBtnGroup>
</div>
</Teleport>
<SkeletonForm v-if="!formData" />
<QInnerLoading
:showing="isLoading"
:label="t('globals.pleaseWait')"

View File

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

View File

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

View File

@ -0,0 +1,55 @@
<script setup>
defineProps({
columns: {
type: Array,
required: true,
},
row: {
type: Object,
default: null,
},
});
function stopEventPropagation(event) {
event.preventDefault();
event.stopPropagation();
}
</script>
<template>
<slot name="beforeChip" :row="row"></slot>
<span
v-for="col of columns"
:key="col.name"
@click="stopEventPropagation"
class="cursor-text"
>
<QChip
v-if="col.chip.condition(row[col.name], row)"
:title="col.label"
:class="[
col.chip.color
? col.chip.color(row)
: !col.chip.icon && 'bg-chip-secondary',
col.chip.icon && 'q-px-none',
]"
dense
square
>
<span v-if="!col.chip.icon">{{ row[col.name] }}</span>
<QIcon v-else :name="col.chip.icon" color="primary-light" />
</QChip>
</span>
<slot name="afterChip" :row="row"></slot>
</template>
<style lang="scss">
.bg-chip-secondary {
background-color: var(--vn-page-color);
color: var(--vn-text-color);
}
.cursor-text {
pointer-events: all;
cursor: text;
user-select: all;
}
</style>

View File

@ -0,0 +1,161 @@
<script setup>
import { markRaw, computed, defineModel } from 'vue';
import { QIcon, QCheckbox } from 'quasar';
import { dashIfEmpty } from 'src/filters';
/* basic input */
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnComponent from 'components/common/VnComponent.vue';
const model = defineModel(undefined, { required: true });
const $props = defineProps({
column: {
type: Object,
required: true,
},
row: {
type: Object,
default: () => {},
},
default: {
type: [Object, String],
default: null,
},
componentProp: {
type: String,
default: null,
},
isEditable: {
type: Boolean,
default: true,
},
components: {
type: Object,
default: null,
},
showLabel: {
type: Boolean,
default: null,
},
});
const defaultComponents = {
input: {
component: markRaw(VnInput),
attrs: {
disable: !$props.isEditable,
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
number: {
component: markRaw(VnInput),
attrs: {
disable: !$props.isEditable,
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
date: {
component: markRaw(VnInputDate),
attrs: {
readonly: true,
disable: !$props.isEditable,
style: 'min-width: 125px',
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
checkbox: {
component: markRaw(QCheckbox),
attrs: (prop) => {
const defaultAttrs = {
disable: !$props.isEditable,
'model-value': Boolean(prop),
class: 'no-padding',
};
if (typeof prop == 'number') {
defaultAttrs['true-value'] = 1;
defaultAttrs['false-value'] = 0;
}
return defaultAttrs;
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
select: {
component: markRaw(VnSelect),
attrs: {
disable: !$props.isEditable,
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
icon: {
component: markRaw(QIcon),
},
};
const value = computed(() => {
return $props.column.format
? $props.column.format($props.row, dashIfEmpty)
: dashIfEmpty($props.row[$props.column.name]);
});
const col = computed(() => {
let newColumn = { ...$props.column };
const specific = newColumn[$props.componentProp];
if (specific) {
newColumn = {
...newColumn,
...specific,
...specific.attrs,
...specific.forceAttrs,
};
}
if (
(newColumn.name.startsWith('is') || newColumn.name.startsWith('has')) &&
!newColumn.component
)
newColumn.component = 'checkbox';
if ($props.default && !newColumn.component) newColumn.component = $props.default;
return newColumn;
});
const components = computed(() => $props.components ?? defaultComponents);
</script>
<template>
<div class="row no-wrap fit">
<VnComponent
v-if="col.before"
:prop="col.before"
:components="components"
:value="model"
v-model="model"
/>
<VnComponent
v-if="col.component"
:prop="col"
:components="components"
:value="model"
v-model="model"
/>
<span :title="value" v-else>{{ value }}</span>
<VnComponent
v-if="col.after"
:prop="col.after"
:components="components"
:value="model"
v-model="model"
/>
</div>
</template>

View File

@ -0,0 +1,146 @@
<script setup>
import { markRaw, computed, defineModel } from 'vue';
import { QCheckbox } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
/* basic input */
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue';
const $props = defineProps({
column: {
type: Object,
required: true,
},
showTitle: {
type: Boolean,
default: false,
},
dataKey: {
type: String,
required: true,
},
searchUrl: {
type: String,
default: 'params',
},
});
const model = defineModel(undefined, { required: true });
const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
const columnFilter = computed(() => $props.column?.columnFilter);
const updateEvent = { 'update:modelValue': addFilter };
const enterEvent = {
'keyup.enter': () => addFilter(model.value),
remove: () => addFilter(null),
};
const defaultAttrs = {
filled: !$props.showTitle,
class: 'q-px-sm q-pb-xs q-pt-none',
dense: true,
};
const forceAttrs = {
label: $props.showTitle ? '' : $props.column.label,
};
const components = {
input: {
component: markRaw(VnInput),
event: enterEvent,
attrs: {
...defaultAttrs,
clearable: true,
},
forceAttrs,
},
number: {
component: markRaw(VnInput),
event: enterEvent,
attrs: {
...defaultAttrs,
clearable: true,
},
forceAttrs,
},
date: {
component: markRaw(VnInputDate),
event: updateEvent,
attrs: {
...defaultAttrs,
style: 'min-width: 150px',
},
forceAttrs,
},
checkbox: {
component: markRaw(QCheckbox),
event: updateEvent,
attrs: {
dense: true,
class: $props.showTitle ? 'q-py-sm q-mt-md' : 'q-px-md q-py-xs',
'toggle-indeterminate': true,
},
forceAttrs,
},
select: {
component: markRaw(VnSelect),
event: updateEvent,
attrs: {
class: 'q-px-md q-pb-xs q-pt-none',
dense: true,
filled: !$props.showTitle,
},
forceAttrs,
},
};
async function addFilter(value) {
value ??= undefined;
if (value && typeof value === 'object') value = model.value;
value = value === '' ? undefined : value;
let field = columnFilter.value?.name ?? $props.column.name;
if (columnFilter.value?.inWhere) {
if (columnFilter.value.alias) field = columnFilter.value.alias + '.' + field;
return await arrayData.addFilterWhere({ [field]: value });
}
await arrayData.addFilter({ params: { [field]: value } });
}
function alignRow() {
switch ($props.column.align) {
case 'left':
return 'justify-start items-start';
case 'right':
return 'justify-end items-end';
default:
return 'flex-center';
}
}
const showFilter = computed(
() => $props.column?.columnFilter !== false && $props.column.name != 'tableActions'
);
</script>
<template>
<div
v-if="showTitle"
class="q-pt-sm q-px-sm ellipsis"
:class="`text-${column?.align ?? 'left'}`"
:style="!showFilter ? { 'min-height': 72 + 'px' } : ''"
>
{{ column?.label }}
</div>
<div v-if="showFilter" class="full-width" :class="alignRow()">
<VnTableColumn
:column="$props.column"
default="input"
v-model="model"
:components="components"
component-prop="columnFilter"
/>
</div>
</template>

View File

@ -0,0 +1,628 @@
<script setup>
import { ref, onMounted, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import FormModelPopup from 'components/FormModelPopup.vue';
import CrudModel from 'src/components/CrudModel.vue';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnLv from 'components/ui/VnLv.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue';
import VnTableFilter from 'components/VnTable/VnFilter.vue';
import VnTableChip from 'components/VnTable/VnChip.vue';
const $props = defineProps({
columns: {
type: Array,
required: true,
},
defaultMode: {
type: String,
default: 'card', // 'table', 'card'
},
columnSearch: {
type: Boolean,
default: true,
},
rightSearch: {
type: Boolean,
default: true,
},
rowClick: {
type: Function,
default: null,
},
redirect: {
type: String,
default: null,
},
create: {
type: Object,
default: null,
},
cardClass: {
type: String,
default: 'flex-one',
},
searchUrl: {
type: String,
default: 'table',
},
isEditable: {
type: Boolean,
default: false,
},
useModel: {
type: Boolean,
default: false,
},
});
const { t } = useI18n();
const stateStore = useStateStore();
const route = useRoute();
const router = useRouter();
const quasar = useQuasar();
const DEFAULT_MODE = 'card';
const TABLE_MODE = 'table';
const mode = ref(DEFAULT_MODE);
const selected = ref([]);
const routeQuery = JSON.parse(route?.query[$props.searchUrl] ?? '{}');
const params = ref({ ...routeQuery, ...routeQuery.filter?.where });
const CrudModelRef = ref({});
const showForm = ref(false);
const splittedColumns = ref({ columns: [] });
const tableModes = [
{
icon: 'view_column',
title: t('table view'),
value: TABLE_MODE,
},
{
icon: 'grid_view',
title: t('grid view'),
value: DEFAULT_MODE,
},
];
onMounted(() => {
mode.value = quasar.platform.is.mobile ? DEFAULT_MODE : $props.defaultMode;
stateStore.rightDrawer = true;
setUserParams(route.query[$props.searchUrl]);
});
watch(
() => $props.columns,
(value) => splitColumns(value),
{ immediate: true }
);
watch(
() => route.query[$props.searchUrl],
(val) => setUserParams(val)
);
function setUserParams(watchedParams) {
if (!watchedParams) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
const where = JSON.parse(watchedParams?.filter)?.where;
watchedParams = { ...watchedParams, ...where };
delete watchedParams.filter;
params.value = { ...params.value, ...watchedParams };
}
function splitColumns(columns) {
splittedColumns.value = {
columns: [],
chips: [],
create: [],
visible: [],
};
for (const col of columns) {
if (col.name == 'tableActions') splittedColumns.value.actions = col;
if (col.chip) splittedColumns.value.chips.push(col);
if (col.isTitle) splittedColumns.value.title = col;
if (col.create) splittedColumns.value.create.push(col);
if (col.cardVisible) splittedColumns.value.visible.push(col);
if ($props.isEditable && col.disable == null) col.disable = false;
if ($props.useModel) col.columnFilter = { ...col.columnFilter, inWhere: true };
splittedColumns.value.columns.push(col);
}
// Status column
if (splittedColumns.value.chips.length) {
splittedColumns.value.columnChips = splittedColumns.value.chips.filter(
(c) => !c.isId
);
if (splittedColumns.value.columnChips.length)
splittedColumns.value.columns.unshift({
align: 'left',
label: t('status'),
name: 'tableStatus',
columnFilter: false,
});
}
}
const rowClickFunction = computed(() => {
if ($props.rowClick) return $props.rowClick;
if ($props.redirect) return ({ id }) => redirectFn(id);
return () => {};
});
function redirectFn(id) {
router.push({ path: `/${$props.redirect}/${id}` });
}
function stopEventPropagation(event) {
event.preventDefault();
event.stopPropagation();
}
function reload(params) {
CrudModelRef.value.reload(params);
}
function columnName(col) {
const column = { ...col, ...col.columnFilter };
let name = column.name;
if (column.alias) name = column.alias + '.' + name;
return name;
}
function getColAlign(col) {
return 'text-' + (col.align ?? 'left')
}
defineExpose({
reload,
redirect: redirectFn,
});
</script>
<template>
<QDrawer
v-if="$props.rightSearch"
v-model="stateStore.rightDrawer"
side="right"
:width="256"
show-if-above
>
<QScrollArea class="fit">
<VnFilterPanel
:data-key="$attrs['data-key']"
:search-button="true"
v-model="params"
:disable-submit-event="true"
:search-url="searchUrl"
>
<template #body>
<VnTableFilter
:column="col"
:data-key="$attrs['data-key']"
v-for="col of splittedColumns.columns"
:key="col.id"
v-model="params[columnName(col)]"
:search-url="searchUrl"
/>
</template>
<slot
name="moreFilterPanel"
:params="params"
:columns="splittedColumns.columns"
/>
</VnFilterPanel>
</QScrollArea>
</QDrawer>
<!-- class in div to fix warn-->
<div class="q-px-md">
<CrudModel
v-bind="$attrs"
:limit="20"
ref="CrudModelRef"
:search-url="searchUrl"
:disable-infinite-scroll="mode == TABLE_MODE"
@save-changes="reload"
:has-subtoolbar="isEditable"
>
<template #body="{ rows }">
<QTable
v-bind="$attrs['QTable']"
class="vnTable"
:columns="splittedColumns.columns"
:rows="rows"
v-model:selected="selected"
:grid="mode != TABLE_MODE"
table-header-class="bg-header"
card-container-class="grid-three"
flat
:style="mode == TABLE_MODE && 'max-height: 90vh'"
virtual-scroll
@virtual-scroll="
(event) =>
event.index > rows.length - 2 &&
CrudModelRef.vnPaginateRef.paginate()
"
@row-click="(_, row) => rowClickFunction(row)"
>
<template #top-left>
<slot name="top-left"></slot>
</template>
<template #top-right>
<!-- <QBtn
icon="visibility"
title="asd"
class="bg-vn-section-color q-mr-md"
dense
v-if="mode == 'table'"
/> -->
<QBtnToggle
v-model="mode"
toggle-color="primary"
class="bg-vn-section-color"
dense
:options="tableModes"
/>
<QBtn
icon="filter_alt"
title="asd"
class="bg-vn-section-color q-ml-md"
dense
@click="stateStore.toggleRightDrawer()"
/>
</template>
<template #header-cell="{ col }">
<QTh
auto-width
style="min-width: 100px"
v-if="$props.columnSearch"
>
<VnTableFilter
:column="col"
:show-title="true"
:data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
:search-url="searchUrl"
/>
</QTh>
</template>
<template #header-cell-tableActions>
<QTh auto-width class="sticky" />
</template>
<template #body-cell-tableStatus="{ col, row }">
<QTd auto-width :class="getColAlign(col)">
<VnTableChip
:columns="splittedColumns.columnChips"
:row="row"
>
<template #afterChip>
<slot name="afterChip" :row="row"></slot>
</template>
</VnTableChip>
</QTd>
</template>
<template #body-cell="{ col, row }">
<!-- Columns -->
<QTd
auto-width
class="no-margin q-px-xs"
:class="getColAlign(col)"
>
<VnTableColumn
:column="col"
:row="row"
:is-editable="false"
v-model="row[col.name]"
component-prop="columnField"
/>
</QTd>
</template>
<template #body-cell-tableActions="{ col, row }">
<QTd
auto-width
:class="getColAlign(col)"
class="sticky no-padding"
@click="stopEventPropagation($event)"
>
<QBtn
v-for="(btn, index) of col.actions"
:key="index"
:title="btn.title"
:icon="btn.icon"
class="q-px-sm"
flat
:class="
btn.isPrimary
? 'text-primary-light'
: 'color-vn-text '
"
@click="btn.action(row)"
/>
</QTd>
</template>
<template #item="{ row, colsMap }">
<component
:is="$props.redirect ? 'router-link' : 'span'"
:to="`/${$props.redirect}/` + row.id"
>
<QCard
bordered
flat
class="row no-wrap justify-between cursor-pointer"
@click="
(_, row) => {
$props.rowClick && $props.rowClick(row);
}
"
>
<QCardSection
vertical
class="no-margin no-padding"
:class="colsMap.tableActions ? 'w-80' : 'fit'"
>
<!-- Chips -->
<QCardSection
v-if="splittedColumns.chips.length"
class="no-margin q-px-xs q-py-none"
>
<VnTableChip
:columns="splittedColumns.chips"
:row="row"
>
<template #afterChip>
<slot name="afterChip" :row="row"></slot>
</template>
</VnTableChip>
</QCardSection>
<!-- Title -->
<QCardSection
v-if="splittedColumns.title"
class="q-pl-sm q-py-none text-primary-light text-bold text-h6 cardEllipsis"
>
<span
:title="row[splittedColumns.title.name]"
@click="stopEventPropagation($event)"
class="cursor-text"
>
{{ row[splittedColumns.title.name] }}
</span>
</QCardSection>
<!-- Fields -->
<QCardSection
class="q-pl-sm q-pr-lg q-py-xs"
:class="$props.cardClass"
>
<div
v-for="col of splittedColumns.visible"
:key="col.name"
class="fields"
>
<VnLv
:label="
!col.component &&
col.label &&
`${col.label}:`
"
>
<template #value>
<span
@click="
stopEventPropagation($event)
"
>
<VnTableColumn
:column="col"
:row="row"
:is-editable="false"
v-model="row[col.name]"
component-prop="columnField"
:show-label="true"
/>
</span>
</template>
</VnLv>
</div>
</QCardSection>
</QCardSection>
<!-- Actions -->
<QCardSection
v-if="colsMap.tableActions"
class="column flex-center w-10 no-margin q-pa-xs q-gutter-y-xs"
@click="stopEventPropagation($event)"
>
<QBtn
v-for="(btn, index) of splittedColumns.actions
.actions"
:key="index"
:title="btn.title"
:icon="btn.icon"
class="q-pa-xs"
flat
:class="
btn.isPrimary
? 'text-primary-light'
: 'color-vn-text '
"
@click="btn.action(row)"
/>
</QCardSection>
</QCard>
</component>
</template>
</QTable>
</template>
</CrudModel>
</div>
<QPageSticky v-if="create" :offset="[20, 20]" style="z-index: 2">
<QBtn @click="showForm = !showForm" color="primary" fab icon="add" />
<QTooltip>
{{ create.title }}
</QTooltip>
</QPageSticky>
<QDialog v-model="showForm" transition-show="scale" transition-hide="scale">
<FormModelPopup
v-bind="create"
:model="$attrs['data-key'] + 'Create'"
@on-data-saved="(_, res) => create.onDataSaved(res)"
>
<template #form-inputs="{ data }">
<div class="grid-create">
<VnTableColumn
v-for="column of splittedColumns.create"
:key="column.name"
:column="column"
:row="{}"
default="input"
v-model="data[column.name]"
:show-label="true"
/>
<slot name="more-create-dialog" :data="data" />
</div>
</template>
</FormModelPopup>
</QDialog>
</template>
<i18n>
en:
status: Status
es:
status: Estados
</i18n>
<style lang="scss">
.bg-chip-secondary {
background-color: var(--vn-page-color);
color: var(--vn-text-color);
}
.bg-header {
background-color: #5d5d5d;
color: var(--vn-text-color);
}
.q-table--dark .q-table__bottom,
.q-table--dark thead,
.q-table--dark tr,
.q-table--dark th,
.q-table--dark td {
border-color: #222222;
}
.q-table__container > div:first-child {
background-color: var(--vn-page-color);
}
.grid-three {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, max-content));
max-width: 100%;
grid-gap: 20px;
margin: 0 auto;
}
.grid-create {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
max-width: 100%;
grid-gap: 20px;
margin: 0 auto;
}
.flex-one {
display: flex;
flex-flow: row wrap;
div.fields {
width: 100%;
.vn-label-value {
display: flex;
gap: 2%;
}
}
}
.q-table th {
padding: 0;
}
.vnTable {
thead tr th {
position: sticky;
z-index: 2;
}
thead tr:first-child th {
top: 0;
}
.q-table__top {
top: 0;
}
tbody {
.q-checkbox {
display: flex;
margin-bottom: 9px;
& .q-checkbox__label {
margin-left: 31px;
color: var(--vn-text-color);
}
& .q-checkbox__inner {
position: absolute;
left: 0;
color: var(--vn-label-color);
}
}
}
.sticky {
position: sticky;
right: 0;
}
td.sticky {
background-color: var(--q-dark);
z-index: 1;
}
}
.vn-label-value {
display: flex;
flex-direction: row;
color: var(--vn-text-color);
.value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: all;
cursor: text;
user-select: all;
}
}
.cardEllipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.grid-two {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
max-width: 100%;
margin: 0 auto;
overflow: scroll;
white-space: wrap;
width: 100%;
}
.w-80 {
width: 80%;
}
.w-20 {
width: 20%;
}
.cursor-text {
pointer-events: all;
cursor: text;
user-select: all;
}
</style>

View File

@ -1,6 +1,6 @@
<script setup>
import { onBeforeMount, computed, watchEffect } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import { onBeforeMount, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize';
@ -41,20 +41,6 @@ onBeforeMount(async () => {
if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id };
await arrayData.fetch({ append: false });
});
if (props.baseUrl) {
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
arrayData.store.url = `${props.baseUrl}/${to.params.id}`;
await arrayData.fetch({ append: false });
}
});
}
watchEffect(() => {
if (Array.isArray(arrayData.store.data))
arrayData.store.data = arrayData.store.data[0];
});
</script>
<template>
<QDrawer

View File

@ -0,0 +1,60 @@
<script setup>
import { computed, defineModel } from 'vue';
const model = defineModel(undefined, { required: true });
const $props = defineProps({
prop: {
type: Object,
required: true,
},
components: {
type: Object,
default: () => {},
},
value: {
type: [Object, Number, String],
default: () => {},
},
});
const componentArray = computed(() => {
if (typeof $props.prop === 'object') return [$props.prop];
return $props.prop;
});
function mix(toComponent) {
const { component, attrs, event } = toComponent;
const customComponent = $props.components[component];
return {
component: customComponent?.component ?? component,
attrs: {
...toValueAttrs(attrs),
...toValueAttrs(customComponent?.attrs),
...toComponent,
...toValueAttrs(customComponent?.forceAttrs),
},
event: event ?? customComponent?.event,
};
}
function toValueAttrs(attrs) {
if (!attrs) return;
return typeof attrs == 'function' ? attrs($props.value) : attrs;
}
</script>
<template>
<span
v-for="toComponent of componentArray"
:key="toComponent.name"
class="column flex-center fit"
>
<component
v-if="toComponent?.component"
:is="mix(toComponent).component"
v-bind="mix(toComponent).attrs"
v-on="mix(toComponent).event ?? {}"
v-model="model"
class="fit"
/>
</span>
</template>

View File

@ -2,7 +2,12 @@
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const emit = defineEmits(['update:modelValue', 'update:options', 'keyup.enter']);
const emit = defineEmits([
'update:modelValue',
'update:options',
'keyup.enter',
'remove',
]);
const $props = defineProps({
modelValue: {
@ -17,6 +22,10 @@ const $props = defineProps({
type: String,
default: '',
},
clearable: {
type: Boolean,
default: true,
},
});
const { t } = useI18n();
@ -83,7 +92,7 @@ const inputRules = [
<QIcon
name="close"
size="xs"
v-if="hover && value && !$attrs.disabled"
v-if="hover && value && !$attrs.disabled && $props.clearable"
@click="value = null"
></QIcon>
<QIcon v-if="info" name="info">

View File

@ -1,5 +1,6 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import isValidDate from 'filters/isValidDate';
const props = defineProps({
@ -24,6 +25,9 @@ const hover = ref(false);
const emit = defineEmits(['update:modelValue']);
const { t } = useI18n();
const requiredFieldRule = (val) => !!val || t('globals.fieldRequired');
const joinDateAndTime = (date, time) => {
if (!date) {
return null;
@ -91,13 +95,15 @@ const styleAttrs = computed(() => {
readonly
:model-value="displayDate(value)"
v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: $attrs.required }"
:rules="$attrs.required ? [requiredFieldRule] : null"
@click="isPopupOpen = true"
>
<template #append>
<QIcon
name="close"
size="xs"
v-if="hover && value"
v-if="hover && value && !readonly"
@click="onDateUpdate(null)"
></QIcon>
<QIcon name="event" class="cursor-pointer">

View File

@ -17,8 +17,9 @@ const props = defineProps({
default: false,
},
});
const { t } = useI18n();
const emit = defineEmits(['update:modelValue']);
const { t } = useI18n();
const requiredFieldRule = (val) => !!val || t('globals.fieldRequired');
const value = computed({
get() {
@ -71,6 +72,8 @@ const styleAttrs = computed(() => {
readonly
:model-value="formatTime(value)"
v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: $attrs.required }"
:rules="$attrs.required ? [requiredFieldRule] : null"
@click="isPopupOpen = true"
>
<template #append>

View File

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

View File

@ -1,6 +1,5 @@
<script setup>
import { ref, toRefs, computed, watch } from 'vue';
import { onMounted } from 'vue';
import { ref, toRefs, computed, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'src/components/FetchData.vue';
const emit = defineEmits(['update:modelValue', 'update:options']);
@ -16,11 +15,11 @@ const $props = defineProps({
},
optionLabel: {
type: [String],
default: '',
default: 'name',
},
optionValue: {
type: String,
default: '',
default: 'id',
},
optionFilter: {
type: String,
@ -58,6 +57,10 @@ const $props = defineProps({
type: [Number, String],
default: '30',
},
focusOnMount: {
type: Boolean,
default: false,
},
});
const { t } = useI18n();
@ -148,6 +151,10 @@ watch(modelValue, (newValue) => {
if (!myOptions.value.some((option) => option[optionValue.value] == newValue))
fetchFilter(newValue);
});
onMounted(async () => {
if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300);
});
</script>
<template>
@ -180,6 +187,7 @@ watch(modelValue, (newValue) => {
>
<template v-if="isClearable" #append>
<QIcon
v-show="value"
name="close"
@click.stop="value = null"
class="cursor-pointer"

View File

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

View File

@ -22,6 +22,10 @@ const props = defineProps({
type: String,
default: '',
},
moduleName: {
type: String,
default: null,
},
});
const emit = defineEmits(['onFetch']);
const route = useRoute();
@ -83,7 +87,7 @@ function existSummary(routes) {
v-if="showRedirectToSummaryIcon"
class="header link"
:to="{
name: `${route.meta.moduleName}Summary`,
name: `${moduleName ?? route.meta.moduleName}Summary`,
params: { id: entityId || entity.id },
}"
>
@ -183,15 +187,10 @@ function existSummary(routes) {
color: lighten($primary, 20%);
}
.q-checkbox {
display: flex;
margin-bottom: 9px;
& .q-checkbox__label {
margin-left: 31px;
color: var(--vn-text-color);
}
& .q-checkbox__inner {
position: absolute;
left: 0;
color: var(--vn-label-color);
}
}

View File

@ -1,20 +1,14 @@
<template>
<div class="q-pa-md">
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
</div>
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
</div>
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
</div>
<div class="row q-gutter-md">
<QSkeleton type="QBtn" />
<QSkeleton type="QBtn" />
</div>
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square />
</div>
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square />
</div>
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="QInput" class="col" square />
<QSkeleton type="QInput" class="col" square />
</div>
</template>

View File

@ -1,25 +1,38 @@
<script setup>
defineProps({
columns: {
type: Number,
default: 6,
},
});
</script>
<template>
<div class="q-pa-md w">
<div class="row q-gutter-md q-mb-md">
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
<QSkeleton type="rect" square />
</div>
<div class="row q-gutter-md q-mb-md" v-for="n in 5" :key="n">
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
<QSkeleton type="QInput" square />
<div class="q-pa-md q-mx-md container">
<div class="row q-gutter-md q-mb-md justify-around no-wrap">
<QSkeleton type="rect" square v-for="n in columns" :key="n" class="column" />
</div>
<div
class="row q-gutter-md q-mb-md justify-around no-wrap"
v-for="n in 5"
:key="n"
>
<QSkeleton
type="QInput"
square
v-for="m in columns"
:key="m"
class="column"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.w {
width: 80vw;
.container {
width: 100%;
overflow-x: hidden;
}
.column {
flex-shrink: 0;
width: 200px;
}
</style>

View File

@ -4,11 +4,14 @@ import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData';
import { useRoute } from 'vue-router';
import toDate from 'filters/toDate';
import useRedirect from 'src/composables/useRedirect';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
const { t } = useI18n();
const props = defineProps({
const $props = defineProps({
modelValue: {
type: Object,
default: () => {}
},
dataKey: {
type: String,
required: true,
@ -18,11 +21,6 @@ const props = defineProps({
required: false,
default: false,
},
params: {
type: Object,
required: false,
default: null,
},
showAll: {
type: Boolean,
default: true,
@ -40,12 +38,20 @@ const props = defineProps({
},
hiddenTags: {
type: Array,
default: () => [],
default: () => ['filter'],
},
customTags: {
type: Array,
default: () => [],
},
disableSubmitEvent: {
type: Boolean,
default: false,
},
searchUrl: {
type: String,
default: 'params',
},
redirect: {
type: Boolean,
default: true,
@ -54,50 +60,55 @@ const props = defineProps({
const emit = defineEmits(['refresh', 'clear', 'search', 'init', 'remove']);
const arrayData = useArrayData(props.dataKey, {
exprBuilder: props.exprBuilder,
const arrayData = useArrayData($props.dataKey, {
exprBuilder: $props.exprBuilder,
searchUrl: $props.searchUrl,
navigate: $props.redirect ? {} : null,
});
const route = useRoute();
const store = arrayData.store;
const userParams = ref({});
const { navigate } = useRedirect();
const userParams = ref({})
onMounted(() => {
if (props.params) userParams.value = JSON.parse(JSON.stringify(props.params));
if (Object.keys(store.userParams).length > 0) {
userParams.value = JSON.parse(JSON.stringify(store.userParams));
}
userParams.value = $props.modelValue ?? {}
emit('init', { params: userParams.value });
});
function setUserParams(watchedParams) {
if (!watchedParams) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
watchedParams = { ...watchedParams, ...watchedParams.filter?.where };
delete watchedParams.filter;
userParams.value = { ...userParams.value, ...watchedParams };
}
watch(
() => route.query.params,
(val) => {
if (!val) {
userParams.value = {};
} else {
const parsedParams = JSON.parse(val);
userParams.value = { ...parsedParams };
}
}
() => route.query[$props.searchUrl],
(val) => setUserParams(val)
);
watch(
() => arrayData.store.userParams,
(val) => setUserParams(val)
);
const isLoading = ref(false);
async function search() {
async function search(evt) {
if (evt && $props.disableSubmitEvent) return;
store.filter.where = {};
isLoading.value = true;
const params = { ...userParams.value };
const filter = { ...userParams.value };
store.userParamsChanged = true;
store.filter.skip = 0;
store.skip = 0;
const { params: newParams } = await arrayData.addFilter({ params });
const { params: newParams } = await arrayData.addFilter({ params: userParams.value });
userParams.value = newParams;
if (!props.showAll && !Object.values(params).length) store.data = [];
if (!$props.showAll && !Object.values(filter).length) store.data = [];
isLoading.value = false;
emit('search');
if (props.redirect) navigate(store.data, {});
}
async function reload() {
@ -105,10 +116,9 @@ async function reload() {
const params = Object.values(userParams.value).filter((param) => param);
await arrayData.fetch({ append: false });
if (!props.showAll && !params.length) store.data = [];
if (!$props.showAll && !params.length) store.data = [];
isLoading.value = false;
emit('refresh');
if (props.redirect) navigate(store.data, {});
}
async function clearFilters() {
@ -118,17 +128,18 @@ async function clearFilters() {
store.skip = 0;
// Filtrar los params no removibles
const removableFilters = Object.keys(userParams.value).filter((param) =>
props.unremovableParams.includes(param)
$props.unremovableParams.includes(param)
);
const newParams = {};
// Conservar solo los params que no son removibles
for (const key of removableFilters) {
newParams[key] = userParams.value[key];
}
userParams.value = {};
userParams.value = { ...newParams }; // Actualizar los params con los removibles
await arrayData.applyFilter({ params: userParams.value });
if (!props.showAll) {
if (!$props.showAll) {
store.data = [];
}
@ -136,36 +147,32 @@ async function clearFilters() {
emit('clear');
}
const tagsList = computed(() =>
Object.entries(userParams.value)
.filter(([key, value]) => value && !(props.hiddenTags || []).includes(key))
.map(([key, value]) => ({
label: key,
value: value,
}))
);
const tagsList = computed(() => {
const tagList = [];
for (const key of Object.keys(userParams.value)) {
const value = userParams.value[key];
if (value == null || ($props.hiddenTags || []).includes(key)) continue;
tagList.push({ label: key, value });
}
return tagList;
});
const tags = computed(() =>
tagsList.value.filter((tag) => !(props.customTags || []).includes(tag.label))
);
const tags = computed(() => {
return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.key));
});
const customTags = computed(() =>
tagsList.value.filter((tag) => (props.customTags || []).includes(tag.label))
tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.key))
);
async function remove(key) {
userParams.value[key] = null;
await arrayData.applyFilter({ params: userParams.value });
userParams.value[key] = undefined;
search();
emit('remove', key);
}
function formatValue(value) {
if (typeof value === 'boolean') {
return value ? t('Yes') : t('No');
}
if (isNaN(value) && !isNaN(Date.parse(value))) {
return toDate(value);
}
if (typeof value === 'boolean') return value ? t('Yes') : t('No');
if (isNaN(value) && !isNaN(Date.parse(value))) return toDate(value);
return `"${value}"`;
}
@ -226,7 +233,7 @@ function formatValue(value) {
<slot name="tags" :tag="chip" :format-fn="formatValue">
<div class="q-gutter-x-xs">
<strong>{{ chip.label }}:</strong>
<span>"{{ chip.value }}"</span>
<span>"{{ formatValue(chip.value) }}"</span>
</div>
</slot>
</VnFilterPanelChip>
@ -245,7 +252,7 @@ function formatValue(value) {
<QList dense class="list q-gutter-y-sm q-mt-sm">
<slot name="body" :params="userParams" :search-fn="search"></slot>
</QList>
<template v-if="props.searchButton">
<template v-if="$props.searchButton">
<QItem>
<QItemSection class="q-py-sm">
<QBtn
@ -255,7 +262,7 @@ function formatValue(value) {
dense
icon="search"
rounded
type="submit"
:type="disableSubmitEvent ? 'button' : 'submit'"
unelevated
/>
</QItemSection>
@ -269,7 +276,6 @@ function formatValue(value) {
color="primary"
/>
</template>
<style scoped lang="scss">
.list {
width: 256px;

View File

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

View File

@ -2,6 +2,7 @@
import { dashIfEmpty } from 'src/filters';
import { useI18n } from 'vue-i18n';
import { useClipboard } from 'src/composables/useClipboard';
import { computed } from 'vue';
const $props = defineProps({
label: { type: String, default: null },
@ -24,52 +25,67 @@ function copyValueText() {
},
});
}
const val = computed(() => $props.value);
</script>
<style scoped>
.label,
.value {
white-space: pre-line;
word-wrap: break-word;
}
</style>
<template>
<div class="vn-label-value">
<div v-if="$props.label || $slots.label" class="label">
<slot name="label">
<span>{{ $props.label }}</span>
</slot>
</div>
<div class="value">
<slot name="value">
<span :title="$props.value">
{{ $props.dash ? dashIfEmpty($props.value) : $props.value }}
</span>
</slot>
</div>
<div class="info" v-if="$props.info">
<QIcon name="info" class="cursor-pointer" size="xs" color="grey">
<QTooltip class="bg-dark text-white shadow-4" :offset="[10, 10]">
{{ $props.info }}
</QTooltip>
</QIcon>
</div>
<div class="copy" v-if="$props.copy && $props.value" @click="copyValueText()">
<QIcon name="Content_Copy" color="primary">
<QTooltip>{{ t('globals.copyClipboard') }}</QTooltip>
</QIcon>
</div>
<QCheckbox
v-if="typeof value === 'boolean'"
v-model="val"
:label="label"
disable
dense
/>
<template v-else>
<div v-if="label || $slots.label" class="label">
<slot name="label">
<span>{{ label }}</span>
</slot>
</div>
<div class="value">
<slot name="value">
<span :title="value">
{{ dash ? dashIfEmpty(value) : value }}
</span>
</slot>
</div>
<div class="info" v-if="info">
<QIcon name="info" class="cursor-pointer" size="xs" color="grey">
<QTooltip class="bg-dark text-white shadow-4" :offset="[10, 10]">
{{ info }}
</QTooltip>
</QIcon>
</div>
<div class="copy" v-if="copy && value" @click="copyValueText()">
<QIcon name="Content_Copy" color="primary">
<QTooltip>{{ t('globals.copyClipboard') }}</QTooltip>
</QIcon>
</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.vn-label-value:hover .copy {
visibility: visible;
cursor: pointer;
.vn-label-value {
&:hover .copy {
visibility: visible;
cursor: pointer;
}
.label,
.value {
white-space: pre-line;
word-wrap: break-word;
}
.copy {
visibility: hidden;
}
.info {
margin-left: 5px;
}
}
.copy {
visibility: hidden;
}
.info {
margin-left: 5px;
:deep(.q-checkbox.disabled) {
opacity: 1 !important;
}
</style>

View File

@ -21,7 +21,7 @@ const currentUser = ref(state.getUser());
const newNote = ref('');
const vnPaginateRef = ref();
function handleKeyUp(event) {
if (event.key === 'Enter') {
if (event.key === 'Enter') {
event.preventDefault();
if (!event.shiftKey) insert();
}
@ -78,6 +78,7 @@ async function insert() {
ref="vnPaginateRef"
class="show"
v-bind="$attrs"
search-url="notes"
>
<template #body="{ rows }">
<TransitionGroup name="list" tag="div" class="column items-center full-width">

View File

@ -58,14 +58,19 @@ const props = defineProps({
type: Function,
default: null,
},
searchUrl: {
type: String,
default: null,
},
disableInfiniteScroll: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['onFetch', 'onPaginate']);
const emit = defineEmits(['onFetch', 'onPaginate', 'onChange']);
const isLoading = ref(false);
const mounted = ref(false);
const pagination = ref({
sortBy: props.order,
rowsPerPage: props.limit,
@ -81,11 +86,13 @@ const arrayData = useArrayData(props.dataKey, {
userParams: props.userParams,
exprBuilder: props.exprBuilder,
keepOpts: props.keepOpts,
searchUrl: props.searchUrl,
});
const store = arrayData.store;
onMounted(() => {
if (props.autoLoad) fetch();
onMounted(async () => {
if (props.autoLoad) await fetch();
mounted.value = true;
});
watch(
@ -95,11 +102,22 @@ watch(
}
);
watch(
() => store.data,
(data) => emit('onChange', data)
);
watch(
() => props.url,
(url) => fetch({ url })
);
const addFilter = async (filter, params) => {
await arrayData.addFilter({ filter, params });
};
async function fetch() {
async function fetch(params) {
useArrayData(props.dataKey, params);
store.filter.skip = 0;
store.skip = 0;
await arrayData.fetch({ append: false });
@ -107,6 +125,7 @@ async function fetch() {
isLoading.value = false;
}
emit('onFetch', store.data);
return store.data;
}
async function paginate() {
@ -138,7 +157,7 @@ function endPagination() {
emit('onPaginate');
}
async function onLoad(index, done) {
if (!store.data) return done();
if (!store.data || !mounted.value) return done();
if (store.data.length === 0 || !props.url) return done(false);
@ -150,7 +169,7 @@ async function onLoad(index, done) {
done(isDone);
}
defineExpose({ fetch, addFilter });
defineExpose({ fetch, addFilter, paginate });
</script>
<template>
@ -199,12 +218,6 @@ defineExpose({ fetch, addFilter });
<QSpinner color="orange" size="md" />
</div>
</QInfiniteScroll>
<div
v-if="!isLoading && store.hasMoreData"
class="w-full flex justify-center q-mt-md"
>
<QBtn color="primary" :label="t('Load more data')" @click="paginate()" />
</div>
</template>
<style lang="scss" scoped>

View File

@ -3,7 +3,6 @@ import { onMounted, ref, watch } from 'vue';
import { useQuasar } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
import VnInput from 'src/components/common/VnInput.vue';
import useRedirect from 'src/composables/useRedirect';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'src/stores/useStateStore';
@ -18,17 +17,14 @@ const props = defineProps({
},
label: {
type: String,
required: false,
default: 'Search',
},
info: {
type: String,
required: false,
default: '',
},
redirect: {
type: Boolean,
required: false,
default: true,
},
url: {
@ -73,10 +69,20 @@ const props = defineProps({
},
});
let arrayData = useArrayData(props.dataKey, { ...props });
let store = arrayData.store;
const searchText = ref('');
const { navigate } = useRedirect();
let arrayDataProps = { ...props };
if (props.redirect)
arrayDataProps = {
...props,
...{
navigate: {
customRouteRedirectName: props.customRouteRedirectName,
searchText: searchText.value,
},
},
};
let arrayData = useArrayData(props.dataKey, arrayDataProps);
let store = arrayData.store;
watch(
() => props.dataKey,
@ -106,13 +112,6 @@ async function search() {
search: searchText.value,
},
});
if (!props.redirect) return;
navigate(store.data, {
customRouteRedirectName: props.customRouteRedirectName,
searchText: searchText.value,
});
}
</script>
<template>

View File

@ -1,5 +1,5 @@
import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useRouter, useRoute } from 'vue-router';
import axios from 'axios';
import { useArrayDataStore } from 'stores/useArrayDataStore';
import { buildFilter } from 'filters/filterPanel';
@ -13,6 +13,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
const store = arrayDataStore.get(key);
const route = useRoute();
const router = useRouter();
let canceller = null;
const page = ref(1);
@ -22,8 +23,13 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
store.skip = 0;
const query = route.query;
if (query.params) {
store.userParams = JSON.parse(query.params);
const searchUrl = store.searchUrl;
if (query[searchUrl]) {
const params = JSON.parse(query[searchUrl]);
const filter = params?.filter;
delete params.filter;
store.userParams = { ...params, ...store.userParams };
store.userFilter = { ...JSON.parse(filter ?? '{}'), ...store.userFilter };
}
});
@ -40,13 +46,15 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
'userParams',
'userFilter',
'exprBuilder',
'searchUrl',
'navigate',
];
if (typeof userOptions === 'object') {
for (const option in userOptions) {
const isEmpty = userOptions[option] == null || userOptions[option] === '';
if (isEmpty || !allowedOptions.includes(option)) continue;
if (Object.prototype.hasOwnProperty.call(store, option)) {
if (Object.hasOwn(store, option)) {
const defaultOpts = userOptions[option];
store[option] = userOptions.keepOpts?.includes(option)
? Object.assign(defaultOpts, store[option])
@ -65,7 +73,6 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
const filter = {
order: store.order,
limit: store.limit,
skip: store.skip,
};
let exprFilter;
@ -80,15 +87,15 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
}
Object.assign(filter, store.userFilter, exprFilter);
Object.assign(store.filter, filter);
Object.assign(store.filter, { ...filter, skip: store.skip });
const params = {
filter: JSON.stringify(store.filter),
};
Object.assign(params, userParams);
store.isLoading = true;
store.currentFilter = params;
store.isLoading = true;
const response = await axios.get(store.url, {
signal: canceller.signal,
params,
@ -118,6 +125,10 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
}
}
function deleteOption(option) {
delete store[option];
}
function cancelRequest() {
if (canceller) {
canceller.abort();
@ -128,7 +139,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
async function applyFilter({ filter, params }) {
if (filter) store.userFilter = filter;
store.filter = {};
if (params) store.userParams = Object.assign({}, params);
if (params) store.userParams = { ...params };
const response = await fetch({ append: false });
return response;
@ -137,7 +148,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
async function addFilter({ filter, params }) {
if (filter) store.userFilter = Object.assign(store.userFilter, filter);
let userParams = Object.assign({}, store.userParams, params);
let userParams = { ...store.userParams, ...params };
userParams = sanitizerParams(userParams, store?.exprBuilder);
store.userParams = userParams;
@ -149,15 +160,20 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
return { filter, params };
}
async function addFilterWhere(where) {
const storedFilter = { ...store.userFilter };
if (!storedFilter?.where) storedFilter.where = {};
where = { ...storedFilter.where, ...where };
await addFilter({ filter: { where } });
}
function sanitizerParams(params, exprBuilder) {
for (const param in params) {
if (params[param] === '' || params[param] === null) {
delete store.userParams[param];
delete params[param];
if (store.filter?.where) {
const key = Object.keys(
exprBuilder && exprBuilder(param) ? exprBuilder(param) : param
);
const key = Object.keys(exprBuilder ? exprBuilder(param) : param);
if (key[0]) delete store.filter.where[key[0]];
if (Object.keys(store.filter.where).length === 0) {
delete store.filter.where;
@ -182,22 +198,34 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
}
function updateStateParams() {
const query = {};
if (store.order) query.order = store.order;
if (store.limit) query.limit = store.limit;
if (store.skip) query.skip = store.skip;
if (store.userParams && Object.keys(store.userParams).length !== 0)
query.params = JSON.stringify(store.userParams);
const newUrl = { path: route.path, query: { ...(route.query ?? {}) } };
newUrl.query[store.searchUrl] = JSON.stringify(store.currentFilter);
const url = new URL(window.location.href);
const { hash: currentHash } = url;
const [currentRoute] = currentHash.split('?');
if (store.navigate) {
const { customRouteRedirectName, searchText } = store.navigate;
if (customRouteRedirectName)
return router.push({
name: customRouteRedirectName,
params: { id: searchText },
});
const { matched: matches } = router.currentRoute.value;
const { path } = matches.at(-1);
const params = new URLSearchParams();
for (const param in query) params.append(param, query[param]);
const to =
store?.data?.length === 1
? path.replace(/\/(list|:id)|-list/, `/${store.data[0].id}`)
: path.replace(/:id.*/, '');
url.hash = currentRoute + '?' + params.toString();
window.history.pushState({}, '', url.hash);
if (route.path != to) {
const pushUrl = { path: to };
if (to.endsWith('/list') || to.endsWith('/'))
pushUrl.query = newUrl.query;
destroy();
return router.push(pushUrl);
}
}
router.replace(newUrl);
}
const totalRows = computed(() => (store.data && store.data.length) || 0);
@ -207,6 +235,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
fetch,
applyFilter,
addFilter,
addFilterWhere,
refresh,
destroy,
loadMore,
@ -214,5 +243,6 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
totalRows,
updateStateParams,
isLoading,
deleteOption,
};
}

View File

@ -1,25 +0,0 @@
import { useRouter } from 'vue-router';
export default function useRedirect() {
const router = useRouter();
const navigate = (data, { customRouteRedirectName, searchText }) => {
if (customRouteRedirectName)
return router.push({
name: customRouteRedirectName,
params: { id: searchText },
});
const { matched: matches } = router.currentRoute.value;
const { path } = matches.at(-1);
const to =
data.length === 1
? path.replace(/\/(list|:id)|-list/, `/${data[0].id}`)
: path.replace(/:id.*/, '');
router.push({ path: to });
};
return { navigate };
}

View File

@ -115,6 +115,13 @@ select:-webkit-autofill {
background-color: var(--vn-accent-color);
}
.text-primary-light {
color: $primary-light !important;
}
.bg-primary-light {
background: $primary-light !important;
}
.fill-icon {
font-variation-settings: 'FILL' 1;
}
@ -145,6 +152,15 @@ select:-webkit-autofill {
color: var(--vn-label-color);
}
.disabled {
& .q-checkbox__label {
color: var(--vn-text-color);
}
& .q-checkbox__inner {
color: var(--vn-label-color);
}
}
.q-chip,
.q-notification__message,
.q-notification__icon {
@ -189,3 +205,26 @@ input::-webkit-inner-spin-button {
.q-scrollarea__content {
max-width: 100%;
}
/* ===== Scrollbar CSS ===== /
/ Firefox */
* {
scrollbar-width: auto;
scrollbar-color: var(--vn-label-color) transparent;
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-thumb {
background-color: var(--vn-label-color);
border-radius: 10px;
}
*::-webkit-scrollbar-track {
background: transparent;
}

View File

@ -28,7 +28,7 @@ $color-link: #66bfff;
$color-spacer-light: #a3a3a31f;
$color-spacer: #7979794d;
$border-thin-light: 1px solid $color-spacer-light;
$primary-light: lighten($primary, 35%);
$primary-light: #f5b351;
$dark-shadow-color: black;
$layout-shadow-dark: 0 0 10px 2px #00000033, 0 0px 10px #0000003d;
$spacing-md: 16px;

View File

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

View File

@ -280,8 +280,8 @@ customer:
extendedList:
tableVisibleColumns:
id: Identifier
name: Name
socialName: Social name
name: Comercial name
socialName: Business name
fi: Tax number
salesPersonFk: Salesperson
credit: Credit
@ -444,6 +444,14 @@ ticket:
sms: Sms
notes: Notes
sale: Sale
volume: Volume
observation: Notes
ticketAdvance: Advance tickets
futureTickets: Future tickets
purchaseRequest: Purchase request
weeklyTickets: Weekly tickets
services: Service
tracking: Tracking
list:
nickname: Nickname
state: State
@ -845,7 +853,8 @@ worker:
calendar: Calendar
timeControl: Time control
locker: Locker
balance: Balance
formation: Formation
list:
name: Name
email: Email
@ -915,7 +924,24 @@ worker:
payMethods: Pay method
iban: IBAN
bankEntity: Swift / BIC
formation:
tableVisibleColumns:
course: Curso
startDate: Fecha Inicio
endDate: Fecha Fin
center: Centro Formación
invoice: Factura
amount: Importe
remark: Bonficado
hasDiploma: Diploma
imageNotFound: Image not found
balance:
tableVisibleColumns:
paymentDate: Date
incomeType: Type
debit: Debt
credit: Have
concept: Concept
wagon:
pageTitles:
wagons: Wagons
@ -993,6 +1019,18 @@ route:
shipped: Preparation date
viewCmr: View CMR
downloadCmrs: Download CMRs
columnLabels:
Id: Id
vehicle: Vehicle
description: Description
isServed: Served
worker: Worker
date: Date
started: Started
actions: Actions
agency: Agency
volume: Volume
finished: Finished
supplier:
pageTitles:
suppliers: Suppliers

View File

@ -107,6 +107,7 @@ globals:
aliasUsers: Usuarios
subRoles: Subroles
inheritedRoles: Roles heredados
workers: Trabajadores
created: Fecha creación
worker: Trabajador
now: Ahora
@ -278,7 +279,7 @@ customer:
extendedList:
tableVisibleColumns:
id: Identificador
name: Nombre
name: Nombre Comercial
socialName: Razón social
fi: NIF / CIF
salesPersonFk: Comercial
@ -442,6 +443,14 @@ ticket:
sms: Sms
notes: Notas
sale: Lineas del pedido
volume: Volumen
observation: Notas
ticketAdvance: Adelantar tickets
futureTickets: Tickets a futuro
purchaseRequest: Petición de compra
weeklyTickets: Tickets programados
services: Servicios
tracking: Estados
list:
nickname: Alias
state: Estado
@ -840,6 +849,8 @@ worker:
calendar: Calendario
timeControl: Control de horario
locker: Taquilla
balance: Balance
formation: Formación
list:
name: Nombre
email: Email
@ -900,7 +911,24 @@ worker:
payMethods: Método de pago
iban: IBAN
bankEntity: Swift / BIC
formation:
tableVisibleColumns:
course: Curso
startDate: Fecha Inicio
endDate: Fecha Fin
center: Centro Formación
invoice: Factura
amount: Importe
remark: Bonficado
hasDiploma: Diploma
imageNotFound: No se ha encontrado la imagen
balance:
tableVisibleColumns:
paymentDate: Fecha
incomeType: Tipo
debit: Debe
credit: Haber
concept: Concepto
wagon:
pageTitles:
wagons: Vagones
@ -978,6 +1006,18 @@ route:
shipped: Fecha preparación
viewCmr: Ver CMR
downloadCmrs: Descargar CMRs
columnLabels:
Id: Id
vehicle: Vehículo
description: Descripción
isServed: Servida
worker: Trabajador
date: Fecha
started: Iniciada
actions: Acciones
agency: Agencia
volume: Volumen
finished: Finalizada
supplier:
pageTitles:
suppliers: Proveedores

View File

@ -3,7 +3,6 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import VnSelect from 'src/components/common/VnSelect.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import { ref, watch } from 'vue';

View File

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

View File

@ -15,6 +15,10 @@ const $props = defineProps({
required: false,
default: null,
},
summary: {
type: Object,
default: null,
},
});
const route = useRoute();
@ -60,14 +64,14 @@ const removeRole = () => {
<template>
<CardDescriptor
ref="descriptor"
:url="`VnRoles`"
:url="`VnRoles/${entityId}`"
:filter="filter"
module="Role"
@on-fetch="setData"
data-key="accountData"
:title="data.title"
:subtitle="data.subtitle"
:summary="$props.summary"
>
<template #menu>
<QItem v-ripple clickable @click="removeRole()">

View File

@ -0,0 +1,17 @@
<script setup>
import RoleDescriptor from './RoleDescriptor.vue';
import RoleSummary from './RoleSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
</script>
<template>
<QPopupProxy>
<RoleDescriptor v-if="$props.id" :id="$props.id" :summary="RoleSummary" />
</QPopupProxy>
</template>

View File

@ -30,6 +30,7 @@ const filter = {
:url="`VnRoles`"
:filter="filter"
@on-fetch="(data) => (role = data)"
data-key="RoleSummary"
>
<template #header> {{ role.id }} - {{ role.name }} </template>
<template #body>

View File

@ -33,7 +33,6 @@ function exprBuilder(param, value) {
url="Agencies"
order="name"
:expr-builder="exprBuilder"
auto-load
>
<template #body="{ rows }">
<CardList

View File

@ -10,32 +10,13 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import axios from 'axios';
import { useSession } from 'src/composables/useSession';
// import { useSession } from 'src/composables/useSession';
import VnImg from 'src/components/ui/VnImg.vue';
const route = useRoute();
const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const claimFilter = {
fields: [
'id',
'clientFk',
'created',
'workerFk',
'claimStateFk',
'packages',
'pickup',
],
include: [
{
relation: 'client',
scope: {
fields: ['name'],
},
},
],
};
// const { getTokenMultimedia } = useSession();
// const token = getTokenMultimedia();
const claimStates = ref([]);
const claimStatesCopy = ref([]);
@ -87,11 +68,10 @@ const statesFilter = {
/>
<FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load />
<FormModel
:url="`Claims/${route.params.id}`"
model="Claim"
:url-update="`Claims/updateClaim/${route.params.id}`"
:filter="claimFilter"
model="claim"
auto-load
:reload="true"
>
<template #form="{ data, validate, filter }">
<VnRow class="row q-gutter-md q-mb-md">
@ -118,9 +98,11 @@ const statesFilter = {
>
<template #before>
<QAvatar color="orange">
<QImg
<VnImg
v-if="data.workerFk"
:src="`/api/Images/user/160x160/${data.workerFk}/download?access_token=${token}`"
:size="'160x160'"
:id="data.workerFk"
collection="user"
spinner-color="white"
/>
</QAvatar>

View File

@ -2,6 +2,7 @@
import VnCard from 'components/common/VnCard.vue';
import ClaimDescriptor from './ClaimDescriptor.vue';
import ClaimFilter from '../ClaimFilter.vue';
import filter from './ClaimFilter.js';
</script>
<template>
<VnCard
@ -13,5 +14,6 @@ import ClaimFilter from '../ClaimFilter.vue';
search-url="Claims/filter"
searchbar-label="Search claim"
searchbar-info="You can search by claim id or customer name"
:filter="filter"
/>
</template>

View File

@ -12,6 +12,7 @@ import useCardDescription from 'src/composables/useCardDescription';
import VnUserLink from 'src/components/ui/VnUserLink.vue';
import { getUrl } from 'src/composables/getUrl';
import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue';
import filter from './ClaimFilter.js';
const $props = defineProps({
id: {
@ -29,49 +30,6 @@ const entityId = computed(() => {
return $props.id || route.params.id;
});
const filter = {
include: [
{
relation: 'client',
scope: {
include: [
{ relation: 'salesPersonUser' },
{
relation: 'claimsRatio',
scope: {
fields: ['claimingRate'],
limit: 1,
},
},
],
},
},
{
relation: 'claimState',
},
{
relation: 'ticket',
scope: {
include: [
{ relation: 'zone' },
{
relation: 'address',
scope: {
include: { relation: 'province' },
},
},
],
},
},
{
relation: 'worker',
scope: {
include: { relation: 'user' },
},
},
],
};
const STATE_COLOR = {
pending: 'warning',
incomplete: 'info',
@ -101,7 +59,7 @@ onMounted(async () => {
:title="data.title"
:subtitle="data.subtitle"
@on-fetch="setData"
data-key="claimData"
data-key="Claim"
>
<template #menu="{ entity }">
<ClaimDescriptorMenu :claim="entity" />

View File

@ -0,0 +1,52 @@
export default {
fields: [
'id',
'clientFk',
'created',
'workerFk',
'claimStateFk',
'packages',
'pickup',
'ticketFk',
],
include: [
{
relation: 'client',
scope: {
include: [
{ relation: 'salesPersonUser' },
{
relation: 'claimsRatio',
scope: {
fields: ['claimingRate'],
limit: 1,
},
},
],
},
},
{
relation: 'claimState',
},
{
relation: 'ticket',
scope: {
include: [
{ relation: 'zone' },
{
relation: 'address',
scope: {
include: { relation: 'province' },
},
},
],
},
},
{
relation: 'worker',
scope: {
include: { relation: 'user' },
},
},
],
};

View File

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

View File

@ -10,7 +10,7 @@ import CustomerFilter from '../CustomerFilter.vue';
:descriptor="CustomerDescriptor"
:filter-panel="CustomerFilter"
search-data-key="CustomerList"
search-url="Clients/filter"
search-url="Clients/extendedListFilter"
searchbar-label="Search customer"
searchbar-info="You can search by customer id or name"
/>

View File

@ -143,10 +143,6 @@ function handleLocation(data, location) {
</VnRow>
<VnRow>
<QCheckbox
:label="t('Incoterms authorization')"
v-model="data.hasIncoterms"
/>
<QCheckbox
:label="t('Electronic invoice')"
v-model="data.hasElectronicInvoice"

View File

@ -29,7 +29,7 @@ const zones = ref();
@on-fetch="(data) => (workers = data)"
auto-load
/>
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<VnFilterPanel :data-key="props.dataKey" :search-button="true" search-url="table">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>

View File

@ -1,94 +1,439 @@
<script setup>
import { ref, computed, markRaw } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import CustomerFilter from './CustomerFilter.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import VnTable from 'components/VnTable/VnTable.vue';
import VnLocation from 'src/components/common/VnLocation.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import CustomerSummary from './Card/CustomerSummary.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import { toDate } from 'src/filters';
const router = useRouter();
const { t } = useI18n();
const router = useRouter();
const postcodesOptions = ref([]);
const tableRef = ref();
const columns = computed(() => [
{
align: 'left',
name: 'id',
label: t('customer.extendedList.tableVisibleColumns.id'),
chip: {
condition: () => true,
},
isId: true,
columnFilter: {
component: 'select',
name: 'search',
attrs: {
url: 'Clients',
fields: ['id', 'name'],
},
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.name'),
name: 'name',
isTitle: true,
create: true,
},
{
align: 'left',
name: 'socialName',
label: t('customer.extendedList.tableVisibleColumns.socialName'),
isTitle: true,
create: true,
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.fi'),
name: 'fi',
create: true,
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.salesPersonFk'),
name: 'salesPersonFk',
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
where: { role: 'salesPerson' },
},
create: true,
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.salesPerson),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.credit'),
name: 'credit',
component: 'number',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.creditInsurance'),
name: 'creditInsurance',
component: 'number',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.phone'),
name: 'phone',
cardVisible: true,
columnFilter: {
component: 'number',
},
columnField: {
component: null,
after: {
component: markRaw(VnLinkPhone),
attrs: (prop) => {
return {
'phone-number': prop,
};
},
},
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.mobile'),
name: 'mobile',
cardVisible: true,
columnFilter: {
component: 'number',
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.street'),
name: 'street',
create: true,
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.countryFk'),
name: 'countryFk',
columnFilter: {
component: 'select',
inWhere: true,
alias: 'c',
attrs: {
url: 'Countries',
},
},
format: (row, dashIfEmpty) => dashIfEmpty(row.country),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.provinceFk'),
name: 'provinceFk',
component: 'select',
attrs: {
url: 'Provinces',
},
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.province),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.city'),
name: 'city',
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.postcode'),
name: 'postcode',
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.email'),
name: 'email',
cardVisible: true,
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.created'),
name: 'created',
format: ({ created }) => toDate(created),
component: 'date',
columnFilter: {
alias: 'c',
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.businessTypeFk'),
name: 'businessTypeFk',
create: true,
component: 'select',
attrs: {
url: 'BusinessTypes',
optionLabel: 'description',
optionValue: 'code',
},
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.businessType),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.payMethodFk'),
name: 'payMethodFk',
columnFilter: {
component: 'select',
attrs: {
url: 'PayMethods',
},
inWhere: true,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.payMethod),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.sageTaxTypeFk'),
name: 'sageTaxTypeFk',
columnFilter: {
component: 'select',
attrs: {
optionLabel: 'vat',
url: 'SageTaxTypes',
},
alias: 'sti',
inWhere: true,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.sageTaxType),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.sageTransactionTypeFk'),
name: 'sageTransactionTypeFk',
columnFilter: {
component: 'select',
attrs: {
optionLabel: 'transaction',
url: 'SageTransactionTypes',
},
alias: 'stt',
inWhere: true,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.sageTransactionType),
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isActive'),
name: 'isActive',
chip: {
color: null,
condition: (value) => !value,
icon: 'vn:disabled',
},
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isVies'),
name: 'isVies',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isTaxDataChecked'),
name: 'isTaxDataChecked',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isEqualizated'),
name: 'isEqualizated',
create: true,
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isFreezed'),
name: 'isFreezed',
chip: {
color: null,
condition: (value) => value,
icon: 'vn:frozen',
},
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasToInvoice'),
name: 'hasToInvoice',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasToInvoiceByAddress'),
name: 'hasToInvoiceByAddress',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.isToBeMailed'),
name: 'isToBeMailed',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasLcr'),
name: 'hasLcr',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasCoreVnl'),
name: 'hasCoreVnl',
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.hasSepaVnl'),
name: 'hasSepaVnl',
columnFilter: {
inWhere: true,
},
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('Client ticket list'),
icon: 'vn:ticket',
action: redirectToCreateView,
isPrimary: true,
},
{
title: t('Client ticket list'),
icon: 'preview',
action: (row) => viewSummary(row.id, CustomerSummary),
},
],
},
]);
const { viewSummary } = useSummaryDialog();
function navigate(id) {
router.push({ path: `/customer/${id}` });
}
const redirectToCreateView = () => {
router.push({ name: 'CustomerCreate' });
const redirectToCreateView = (row) => {
router.push({
name: 'TicketList',
query: {
params: JSON.stringify({
clientFk: row.id,
}),
},
});
};
function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {};
data.postcode = code;
data.city = town;
data.provinceFk = provinceFk;
data.countryFk = countryFk;
}
</script>
<template>
<VnSearchbar
:info="t('You can search by customer id or name')"
:label="t('Search customer')"
data-key="CustomerList"
data-key="Customer"
/>
<RightMenu>
<template #right-panel>
<CustomerFilter data-key="CustomerList" />
</template>
</RightMenu>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnPaginate
auto-load
data-key="CustomerList"
order="id DESC"
url="/Clients/filter"
>
<template #body="{ rows }">
<CardList
:id="row.id"
:key="row.id"
:title="row.name"
@click="navigate(row.id)"
v-for="row of rows"
>
<template #list-items>
<VnLv :label="t('customer.list.email')" :value="row.email" />
<VnLv :value="row.phone">
<template #label>
{{ t('customer.list.phone') }}
<VnLinkPhone :phone-number="row.phone" />
</template>
</VnLv>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openCard')"
@click.stop="navigate(row.id)"
outline
/>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, CustomerSummary)"
color="primary"
style="margin-top: 15px"
/>
</template>
</CardList>
<VnTable
ref="tableRef"
data-key="Customer"
url="Clients/extendedListFilter"
:create="{
urlCreate: 'Clients/createWithUser',
title: 'Create client',
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {
active: true,
isEqualizated: false,
},
}"
order="id DESC"
:columns="columns"
default-mode="table"
redirect="customer"
auto-load
>
<template #more-create-dialog="{ data }">
<VnLocation
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)"
/>
<QInput v-model="data.userName" :label="t('Web user')" />
<QInput :label="t('Email')" clearable type="email" v-model="data.email">
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip max-width="400px">{{
t('customer.basicData.youCanSaveMultipleEmails')
}}</QTooltip>
</QIcon>
</template>
</VnPaginate>
</div>
<QPageSticky :offset="[20, 20]">
<QBtn @click="redirectToCreateView()" color="primary" fab icon="add" />
<QTooltip>
{{ t('New client') }}
</QTooltip>
</QPageSticky>
</QPage>
</QInput>
</template>
</VnTable>
</template>
<i18n>
es:
Search customer: Buscar cliente
You can search by customer id or name: Puedes buscar por id o nombre del cliente
New client: Nuevo cliente
Web user: Usuario Web
</i18n>
<style lang="scss" scoped>
.col-content {
border-radius: 4px;
padding: 6px;
}
</style>

View File

@ -91,7 +91,6 @@ const tableColumnComponents = {
props: (prop) => ({
disable: true,
'model-value': prop.value,
class: 'disabled-checkbox',
}),
event: () => {},
},

View File

@ -119,7 +119,7 @@ const departments = ref();
emit-value
hide-selected
map-options
option-label="country"
option-label="name"
option-value="id"
outlined
rounded

View File

@ -1,619 +0,0 @@
<script setup>
import { ref, computed, onBeforeMount, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { QBtn, QIcon } from 'quasar';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import CustomerExtendedListActions from './CustomerExtendedListActions.vue';
import CustomerExtendedListFilter from './CustomerExtendedListFilter.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'src/filters';
import RightMenu from 'src/components/common/RightMenu.vue';
const { t } = useI18n();
const router = useRouter();
const stateStore = useStateStore();
const arrayData = ref(null);
onBeforeMount(async () => {
arrayData.value = useArrayData('CustomerExtendedList', {
url: 'Clients/extendedListFilter',
limit: 0,
});
await arrayData.value.fetch({ append: false });
stateStore.rightDrawer = true;
});
onMounted(() => {
const filteredColumns = columns.value.filter(
(col) => col.name !== 'actions' && col.name !== 'customerStatus'
);
allColumnNames.value = filteredColumns.map((col) => col.name);
});
const selectedCustomerId = ref(0);
const selectedSalesPersonId = ref(0);
const allColumnNames = ref([]);
const visibleColumns = ref([]);
const tableColumnComponents = {
customerStatus: {
component: QIcon,
props: (prop) => ({
name: !prop.row.isActive
? 'vn:disabled'
: prop.row.isActive && prop.row.isFreezed
? 'vn:frozen'
: '',
color: 'primary',
size: 'sm',
}),
event: () => {},
},
id: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => {
selectCustomerId(prop.row.id);
},
},
name: {
component: 'span',
props: () => {},
event: () => {},
},
socialName: {
component: 'span',
props: () => {},
event: () => {},
},
fi: {
component: 'span',
props: () => {},
event: () => {},
},
salesPersonFk: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => selectSalesPersonId(prop.row.salesPersonFk),
},
credit: {
component: 'span',
props: () => {},
event: () => {},
},
creditInsurance: {
component: 'span',
props: () => {},
event: () => {},
},
phone: {
component: 'span',
props: () => {},
event: () => {},
},
mobile: {
component: 'span',
props: () => {},
event: () => {},
},
street: {
component: 'span',
props: () => {},
event: () => {},
},
countryFk: {
component: 'span',
props: () => {},
event: () => {},
},
provinceFk: {
component: 'span',
props: () => {},
event: () => {},
},
city: {
component: 'span',
props: () => {},
event: () => {},
},
postcode: {
component: 'span',
props: () => {},
event: () => {},
},
email: {
component: 'span',
props: () => {},
event: () => {},
},
created: {
component: 'span',
props: () => {},
event: () => {},
},
businessTypeFk: {
component: 'span',
props: () => {},
event: () => {},
},
payMethodFk: {
component: 'span',
props: () => {},
event: () => {},
},
sageTaxTypeFk: {
component: 'span',
props: () => {},
event: () => {},
},
sageTransactionTypeFk: {
component: 'span',
props: () => {},
event: () => {},
},
isActive: {
component: QIcon,
props: (prop) => ({
name: prop.row.isActive ? 'check' : 'close',
color: prop.row.isActive ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isVies: {
component: QIcon,
props: (prop) => ({
name: prop.row.isVies ? 'check' : 'close',
color: prop.row.isVies ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isTaxDataChecked: {
component: QIcon,
props: (prop) => ({
name: prop.row.isTaxDataChecked ? 'check' : 'close',
color: prop.row.isTaxDataChecked ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isEqualizated: {
component: QIcon,
props: (prop) => ({
name: prop.row.isEqualizated ? 'check' : 'close',
color: prop.row.isEqualizated ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isFreezed: {
component: QIcon,
props: (prop) => ({
name: prop.row.isFreezed ? 'check' : 'close',
color: prop.row.isFreezed ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasToInvoice: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasToInvoice ? 'check' : 'close',
color: prop.row.hasToInvoice ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasToInvoiceByAddress: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasToInvoiceByAddress ? 'check' : 'close',
color: prop.row.hasToInvoiceByAddress ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isToBeMailed: {
component: QIcon,
props: (prop) => ({
name: prop.row.isToBeMailed ? 'check' : 'close',
color: prop.row.isToBeMailed ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasLcr: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasLcr ? 'check' : 'close',
color: prop.row.hasLcr ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasCoreVnl: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasCoreVnl ? 'check' : 'close',
color: prop.row.hasCoreVnl ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasSepaVnl: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasSepaVnl ? 'check' : 'close',
color: prop.row.hasSepaVnl ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
actions: {
component: CustomerExtendedListActions,
props: (prop) => ({
id: prop.row.id,
}),
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: '',
label: '',
name: 'customerStatus',
format: () => ' ',
},
{
align: 'left',
field: 'id',
label: t('customer.extendedList.tableVisibleColumns.id'),
name: 'id',
},
{
align: 'left',
field: 'name',
label: t('customer.extendedList.tableVisibleColumns.name'),
name: 'name',
},
{
align: 'left',
field: 'socialName',
label: t('customer.extendedList.tableVisibleColumns.socialName'),
name: 'socialName',
},
{
align: 'left',
field: 'fi',
label: t('customer.extendedList.tableVisibleColumns.fi'),
name: 'fi',
},
{
align: 'left',
field: 'salesPerson',
label: t('customer.extendedList.tableVisibleColumns.salesPersonFk'),
name: 'salesPersonFk',
},
{
align: 'left',
field: 'credit',
label: t('customer.extendedList.tableVisibleColumns.credit'),
name: 'credit',
},
{
align: 'left',
field: 'creditInsurance',
label: t('customer.extendedList.tableVisibleColumns.creditInsurance'),
name: 'creditInsurance',
},
{
align: 'left',
field: 'phone',
label: t('customer.extendedList.tableVisibleColumns.phone'),
name: 'phone',
},
{
align: 'left',
field: 'mobile',
label: t('customer.extendedList.tableVisibleColumns.mobile'),
name: 'mobile',
},
{
align: 'left',
field: 'street',
label: t('customer.extendedList.tableVisibleColumns.street'),
name: 'street',
},
{
align: 'left',
field: 'country',
label: t('customer.extendedList.tableVisibleColumns.countryFk'),
name: 'countryFk',
},
{
align: 'left',
field: 'province',
label: t('customer.extendedList.tableVisibleColumns.provinceFk'),
name: 'provinceFk',
},
{
align: 'left',
field: 'city',
label: t('customer.extendedList.tableVisibleColumns.city'),
name: 'city',
},
{
align: 'left',
field: 'postcode',
label: t('customer.extendedList.tableVisibleColumns.postcode'),
name: 'postcode',
},
{
align: 'left',
field: 'email',
label: t('customer.extendedList.tableVisibleColumns.email'),
name: 'email',
},
{
align: 'left',
field: 'created',
label: t('customer.extendedList.tableVisibleColumns.created'),
name: 'created',
format: (value) => toDate(value),
},
{
align: 'left',
field: 'businessType',
label: t('customer.extendedList.tableVisibleColumns.businessTypeFk'),
name: 'businessTypeFk',
},
{
align: 'left',
field: 'payMethod',
label: t('customer.extendedList.tableVisibleColumns.payMethodFk'),
name: 'payMethodFk',
},
{
align: 'left',
field: 'sageTaxType',
label: t('customer.extendedList.tableVisibleColumns.sageTaxTypeFk'),
name: 'sageTaxTypeFk',
},
{
align: 'left',
field: 'sageTransactionType',
label: t('customer.extendedList.tableVisibleColumns.sageTransactionTypeFk'),
name: 'sageTransactionTypeFk',
},
{
align: 'left',
field: 'isActive',
label: t('customer.extendedList.tableVisibleColumns.isActive'),
name: 'isActive',
format: () => ' ',
},
{
align: 'left',
field: 'isVies',
label: t('customer.extendedList.tableVisibleColumns.isVies'),
name: 'isVies',
format: () => ' ',
},
{
align: 'left',
field: 'isTaxDataChecked',
label: t('customer.extendedList.tableVisibleColumns.isTaxDataChecked'),
name: 'isTaxDataChecked',
format: () => ' ',
},
{
align: 'left',
field: 'isEqualizated',
label: t('customer.extendedList.tableVisibleColumns.isEqualizated'),
name: 'isEqualizated',
format: () => ' ',
},
{
align: 'left',
field: 'isFreezed',
label: t('customer.extendedList.tableVisibleColumns.isFreezed'),
name: 'isFreezed',
format: () => ' ',
},
{
align: 'left',
field: 'hasToInvoice',
label: t('customer.extendedList.tableVisibleColumns.hasToInvoice'),
name: 'hasToInvoice',
format: () => ' ',
},
{
align: 'left',
field: 'hasToInvoiceByAddress',
label: t('customer.extendedList.tableVisibleColumns.hasToInvoiceByAddress'),
name: 'hasToInvoiceByAddress',
format: () => ' ',
},
{
align: 'left',
field: 'isToBeMailed',
label: t('customer.extendedList.tableVisibleColumns.isToBeMailed'),
name: 'isToBeMailed',
format: () => ' ',
},
{
align: 'left',
field: 'hasLcr',
label: t('customer.extendedList.tableVisibleColumns.hasLcr'),
name: 'hasLcr',
format: () => ' ',
},
{
align: 'left',
field: 'hasCoreVnl',
label: t('customer.extendedList.tableVisibleColumns.hasCoreVnl'),
name: 'hasCoreVnl',
format: () => ' ',
},
{
align: 'left',
field: 'hasSepaVnl',
label: t('customer.extendedList.tableVisibleColumns.hasSepaVnl'),
name: 'hasSepaVnl',
format: () => ' ',
},
{
align: 'right',
field: 'actions',
label: '',
name: 'actions',
},
]);
const stopEventPropagation = (event, col) => {
if (!['id', 'salesPersonFk'].includes(col.name)) return;
event.preventDefault();
event.stopPropagation();
};
const navigateToTravelId = (id) => router.push({ path: `/customer/${id}` });
const selectCustomerId = (id) => (selectedCustomerId.value = id);
const selectSalesPersonId = (id) => (selectedSalesPersonId.value = id);
</script>
<template>
<RightMenu>
<template #right-panel>
<CustomerExtendedListFilter
v-if="visibleColumns.length !== 0"
data-key="CustomerExtendedList"
:visible-columns="visibleColumns"
/>
</template>
</RightMenu>
<VnSubToolbar>
<template #st-data>
<TableVisibleColumns
:all-columns="allColumnNames"
table-code="clientsDetail"
labels-traductions-path="customer.extendedList.tableVisibleColumns"
@on-config-saved="
visibleColumns = ['customerStatus', ...$event, 'actions']
"
/>
</template>
</VnSubToolbar>
<QPage class="column items-center q-pa-md">
<VnPaginate
data-key="CustomerExtendedList"
url="Clients/extendedListFilter"
auto-load
>
<template #body="{ rows }">
<div class="q-pa-md">
<QTable
:columns="columns"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
:visible-columns="visibleColumns"
@row-click="(evt, row, id) => navigateToTravelId(row.id)"
>
<template #body-cell="{ col, value }">
<QTd @click="stopEventPropagation($event, col)">
{{ value }}
</QTd>
</template>
<template #body-cell-customerStatus="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
</component>
</QTd>
</template>
<template #body-cell-id="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
<CustomerDescriptorProxy :id="props.row.id" />
{{ props.row.id }}
</component>
</QTd>
</template>
<template #body-cell-salesPersonFk="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
v-if="props.row.salesPerson"
class="col-content"
:is="tableColumnComponents[props.col.name].component"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
<WorkerDescriptorProxy
:id="props.row.salesPersonFk"
/>
{{ props.row.salesPerson }}
</component>
<span class="col-content" v-else>-</span>
</QTd>
</template>
<template #body-cell-actions="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
/>
</QTd>
</template>
</QTable>
</div>
</template>
</VnPaginate>
</QPage>
</template>
<style lang="scss" scoped>
.col-content {
border-radius: 4px;
padding: 6px;
}
</style>

View File

@ -1,60 +0,0 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import CustomerSummary from '../Card/CustomerSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
const { t } = useI18n();
const router = useRouter();
const { viewSummary } = useSummaryDialog();
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
const redirectToCreateView = () => {
router.push({
name: 'TicketList',
query: {
params: JSON.stringify({
clientFk: $props.id,
}),
},
});
};
</script>
<template>
<div>
<QIcon
@click.stop="redirectToCreateView"
color="primary"
name="vn:ticket"
size="sm"
>
<QTooltip>
{{ t('Client ticket list') }}
</QTooltip>
</QIcon>
<QIcon
@click.stop="viewSummary($props.id, CustomerSummary)"
class="q-ml-md"
color="primary"
name="preview"
size="sm"
>
<QTooltip>
{{ t('Preview') }}
</QTooltip>
</QIcon>
</div>
</template>
<i18n>
es:
Client ticket list: Listado de tickets del cliente
Preview: Vista previa
</i18n>

View File

@ -1,571 +0,0 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import { dateRange } from 'src/filters';
const props = defineProps({
dataKey: {
type: String,
required: true,
},
visibleColumns: {
type: Array,
required: true,
},
});
const { t } = useI18n();
const clients = ref();
const workers = ref();
const countriesOptions = ref([]);
const provincesOptions = ref([]);
const paymethodsOptions = ref([]);
const businessTypesOptions = ref([]);
const sageTaxTypesOptions = ref([]);
const sageTransactionTypesOptions = ref([]);
const visibleColumnsSet = computed(() => new Set(props.visibleColumns));
const exprBuilder = (param, value) => {
switch (param) {
case 'created':
return {
'c.created': {
between: dateRange(value),
},
};
case 'id':
case 'name':
case 'socialName':
case 'fi':
case 'credit':
case 'creditInsurance':
case 'phone':
case 'mobile':
case 'street':
case 'city':
case 'postcode':
case 'email':
case 'isActive':
case 'isVies':
case 'isTaxDataChecked':
case 'isEqualizated':
case 'isFreezed':
case 'hasToInvoice':
case 'hasToInvoiceByAddress':
case 'isToBeMailed':
case 'hasSepaVnl':
case 'hasLcr':
case 'hasCoreVnl':
case 'countryFk':
case 'provinceFk':
case 'salesPersonFk':
case 'businessTypeFk':
case 'payMethodFk':
case 'sageTaxTypeFk':
case 'sageTransactionTypeFk':
return { [`c.${param}`]: value };
}
};
const shouldRenderColumn = (colName) => {
return visibleColumnsSet.value.has(colName);
};
</script>
<template>
<FetchData
url="Clients"
:filter="{ where: { role: 'socialName' } }"
@on-fetch="(data) => (clients = data)"
auto-load
/>
<FetchData
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<FetchData
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<FetchData
url="Countries"
:filter="{ fields: ['id', 'country'], order: 'country ASC' }"
@on-fetch="(data) => (countriesOptions = data)"
auto-load
/>
<FetchData
ref="provincesFetchDataRef"
@on-fetch="(data) => (provincesOptions = data)"
auto-load
url="Provinces"
/>
<FetchData
url="Paymethods"
@on-fetch="(data) => (paymethodsOptions = data)"
auto-load
/>
<FetchData
url="BusinessTypes"
@on-fetch="(data) => (businessTypesOptions = data)"
auto-load
/>
<FetchData
url="SageTaxTypes"
auto-load
@on-fetch="(data) => (sageTaxTypesOptions = data)"
/>
<FetchData
url="sageTransactionTypes"
auto-load
@on-fetch="(data) => (sageTransactionTypesOptions = data)"
/>
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
:expr-builder="exprBuilder"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong
>{{ t(`customer.extendedList.tableVisibleColumns.${tag.label}`) }}:
</strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<QItem v-if="shouldRenderColumn('id')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.id')"
v-model="params.id"
is-outlined
clearable
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('name')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.name')"
v-model="params.name"
is-outlined
/>
</QItemSection>
</QItem>
<!-- <QItem class="q-mb-sm">
<QItemSection v-if="!clients">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="clients">
<VnSelect
:label="t('Social name')"
v-model="params.socialName"
@update:model-value="searchFn()"
:options="clients"
option-value="socialName"
option-label="socialName"
emit-value
map-options
use-input
hide-selected
dense
outlined
rounded
:input-debounce="0"
/>
</QItemSection>
</QItem> -->
<QItem v-if="shouldRenderColumn('fi')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.fi')"
v-model="params.fi"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('salesPersonFk')">
<QItemSection v-if="!workers">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers">
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.salesPersonFk')
"
v-model="params.salesPersonFk"
@update:model-value="searchFn()"
:options="workers"
option-value="id"
option-label="name"
emit-value
map-options
use-input
hide-selected
dense
outlined
rounded
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('credit')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.credit')"
v-model="params.credit"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('creditInsurance')">
<QItemSection>
<VnInput
:label="
t('customer.extendedList.tableVisibleColumns.creditInsurance')
"
v-model="params.creditInsurance"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('phone')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.phone')"
v-model="params.phone"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('mobile')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.mobile')"
v-model="params.mobile"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('street')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.street')"
v-model="params.street"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('countryFk')">
<QItemSection>
<VnSelect
:label="t('customer.extendedList.tableVisibleColumns.countryFk')"
v-model="params.countryFk"
@update:model-value="searchFn()"
:options="countriesOptions"
option-value="id"
option-label="country"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('provinceFk')">
<QItemSection>
<VnSelect
:label="t('customer.extendedList.tableVisibleColumns.provinceFk')"
v-model="params.provinceFk"
@update:model-value="searchFn()"
:options="provincesOptions"
option-value="id"
option-label="name"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('city')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.city')"
v-model="params.city"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('postcode')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.postcode')"
v-model="params.postcode"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('email')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.email')"
v-model="params.email"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('created')">
<QItemSection>
<VnInputDate
v-model="params.created"
:label="t('customer.extendedList.tableVisibleColumns.created')"
@update:model-value="searchFn()"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('businessTypeFk')">
<QItemSection>
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.businessTypeFk')
"
v-model="params.businessTypeFk"
:options="businessTypesOptions"
@update:model-value="searchFn()"
option-value="code"
option-label="description"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('payMethodFk')">
<QItemSection>
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.payMethodFk')
"
v-model="params.payMethodFk"
:options="paymethodsOptions"
@update:model-value="searchFn()"
option-value="id"
option-label="name"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('sageTaxTypeFk')">
<QItemSection>
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.sageTaxTypeFk')
"
v-model="params.sageTaxTypeFk"
@update:model-value="searchFn()"
:options="sageTaxTypesOptions"
option-value="id"
option-label="vat"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('sageTransactionTypeFk')">
<QItemSection>
<VnSelect
:label="
t(
'customer.extendedList.tableVisibleColumns.sageTransactionTypeFk'
)
"
v-model="params.sageTransactionTypeFk"
@update:model-value="searchFn()"
:options="sageTransactionTypesOptions"
option-value="id"
option-label="transaction"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('isActive') || shouldRenderColumn('isVies')">
<QItemSection v-if="shouldRenderColumn('isActive')">
<QCheckbox
v-model="params.isActive"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.isActive')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('isVies')">
<QCheckbox
v-model="params.isVies"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.isVies')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem
v-if="
shouldRenderColumn('isEqualizated') ||
shouldRenderColumn('isTaxDataChecked')
"
>
<QItemSection v-if="shouldRenderColumn('isTaxDataChecked')">
<QCheckbox
v-model="params.isTaxDataChecked"
@update:model-value="searchFn()"
:label="
t(
'customer.extendedList.tableVisibleColumns.isTaxDataChecked'
)
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('isEqualizated')">
<QCheckbox
v-model="params.isEqualizated"
@update:model-value="searchFn()"
:label="
t('customer.extendedList.tableVisibleColumns.isEqualizated')
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem
v-if="
shouldRenderColumn('hasToInvoice') || shouldRenderColumn('isFreezed')
"
>
<QItemSection v-if="shouldRenderColumn('isFreezed')">
<QCheckbox
v-model="params.isFreezed"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.isFreezed')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('hasToInvoice')">
<QCheckbox
v-model="params.hasToInvoice"
@update:model-value="searchFn()"
:label="
t('customer.extendedList.tableVisibleColumns.hasToInvoice')
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem
v-if="
shouldRenderColumn('isToBeMailed') ||
shouldRenderColumn('hasToInvoiceByAddress')
"
>
<QItemSection v-if="shouldRenderColumn('hasToInvoiceByAddress')">
<QCheckbox
v-model="params.hasToInvoiceByAddress"
@update:model-value="searchFn()"
:label="
t(
'customer.extendedList.tableVisibleColumns.hasToInvoiceByAddress'
)
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('isToBeMailed')">
<QCheckbox
v-model="params.isToBeMailed"
@update:model-value="searchFn()"
:label="
t('customer.extendedList.tableVisibleColumns.isToBeMailed')
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem
v-if="shouldRenderColumn('hasLcr') || shouldRenderColumn('hasCoreVnl')"
>
<QItemSection v-if="shouldRenderColumn('hasLcr')">
<QCheckbox
v-model="params.hasLcr"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.hasLcr')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('hasCoreVnl')">
<QCheckbox
v-model="params.hasCoreVnl"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.hasCoreVnl')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('hasSepaVnl')">
<QItemSection>
<QCheckbox
v-model="params.hasSepaVnl"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.hasSepaVnl')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QSeparator />
</template>
</VnFilterPanel>
</template>
<i18n>
es:
Social name: Razón social
</i18n>

View File

@ -1,6 +1,6 @@
<script setup>
import DepartmentDescriptor from './DepartmentDescriptor.vue';
import DepartmentSummaryDialog from './DepartmentSummaryDialog.vue';
import DepartmentSummary from './DepartmentSummary.vue';
const $props = defineProps({
id: {
@ -15,7 +15,7 @@ const $props = defineProps({
<DepartmentDescriptor
v-if="$props.id"
:id="$props.id"
:summary="DepartmentSummaryDialog"
:summary="DepartmentSummary"
/>
</QPopupProxy>
</template>

View File

@ -32,6 +32,7 @@ onMounted(async () => {
:url="`Departments/${entityId}`"
class="full-width"
style="max-width: 900px"
module-name="Department"
>
<template #header="{ entity }">
<div>{{ entity.name }}</div>

View File

@ -11,9 +11,9 @@ import VnInput from 'src/components/common/VnInput.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency } from 'src/filters';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
@ -22,7 +22,6 @@ const quasar = useQuasar();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const stateStore = useStateStore();
const { notify } = useNotify();
const rowsSelected = ref([]);
@ -312,20 +311,22 @@ const lockIconType = (groupingMode, mode) => {
auto-load
@on-fetch="(data) => (packagingsOptions = data)"
/>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<QBtnGroup push style="column-gap: 10px">
<slot name="moreBeforeActions" />
<QBtn
:label="t('globals.remove')"
color="primary"
icon="delete"
flat
@click="openRemoveDialog()"
:disable="!rowsSelected?.length"
:title="t('globals.remove')"
/>
</QBtnGroup>
</Teleport>
<VnSubToolbar>
<template #st-actions>
<QBtnGroup push style="column-gap: 10px">
<slot name="moreBeforeActions" />
<QBtn
:label="t('globals.remove')"
color="primary"
icon="delete"
flat
@click="openRemoveDialog()"
:disable="!rowsSelected?.length"
:title="t('globals.remove')"
/>
</QBtnGroup>
</template>
</VnSubToolbar>
<VnPaginate
ref="entryBuysPaginateRef"
data-key="EntryBuys"

View File

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

View File

@ -10,8 +10,10 @@ import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue';
import { toCurrency } from 'src/filters';
import useNotify from 'src/composables/useNotify.js';
const route = useRoute();
const { notify } = useNotify();
const { t } = useI18n();
const arrayData = useArrayData();
const invoiceIn = computed(() => arrayData.store.data);
@ -69,6 +71,7 @@ const isNotEuro = (code) => code != 'EUR';
async function insert() {
await axios.post('/InvoiceInDueDays/new', { id: +invoiceId });
await invoiceInFormRef.value.reload();
notify(t('globals.dataSaved'), 'positive');
}
const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount, 0);
</script>

View File

@ -150,21 +150,19 @@ const downloadCSV = async () => {
>
<template #body-cell-clientId="{ row }">
<QTd>
<QBtn flat dense color="blue"> {{ row.clientId }}</QBtn>
<QBtn flat dense class="link"> {{ row.clientId }}</QBtn>
<CustomerDescriptorProxy :id="row.clientId" />
</QTd>
</template>
<template #body-cell-ticketId="{ row }">
<QTd>
<QBtn flat dense color="blue"> {{ row.ticketFk }}</QBtn>
<QBtn flat dense class="link"> {{ row.ticketFk }}</QBtn>
<TicketDescriptorProxy :id="row.ticketFk" />
</QTd>
</template>
<template #body-cell-worker="{ row }">
<QTd>
<QBtn class="no-uppercase" flat dense color="blue">{{
row.workerName
}}</QBtn>
<QBtn class="no-uppercase link" flat dense>{{ row.workerName }}</QBtn>
<WorkerDescriptorProxy :id="row.comercialId" />
</QTd>
</template>

View File

@ -20,6 +20,7 @@ const props = defineProps({
:data-key="props.dataKey"
:search-button="true"
:unremovable-params="['from', 'to']"
:hidden-tags="['from', 'to']"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">

View File

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

View File

@ -3,8 +3,7 @@ import { ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import EditPictureForm from 'components/EditPictureForm.vue';
import { useSession } from 'src/composables/useSession';
import VnImg from 'src/components/ui/VnImg.vue';
import axios from 'axios';
const $props = defineProps({
@ -27,19 +26,12 @@ const $props = defineProps({
});
const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const image = ref(null);
const editPhotoFormDialog = ref(null);
const showEditPhotoForm = ref(false);
const warehouseName = ref(null);
const getItemAvatar = async () => {
const token = getTokenMultimedia();
const timeStamp = `timestamp=${Date.now()}`;
image.value = `/api/Images/catalog/200x200/${$props.entityId}/download?access_token=${token}&${timeStamp}`;
};
const toggleEditPictureForm = () => {
showEditPhotoForm.value = !showEditPhotoForm.value;
};
@ -62,14 +54,17 @@ const getWarehouseName = async (warehouseFk) => {
};
onMounted(async () => {
getItemAvatar();
getItemConfigs();
});
const handlePhotoUpdated = (evt = false) => {
image.value.reload(evt);
};
</script>
<template>
<div class="relative-position">
<QImg :src="image" spinner-color="primary" style="min-height: 256px">
<VnImg ref="image" :id="$props.entityId" @refresh="handlePhotoUpdated(true)">
<template #error>
<div class="absolute-full picture text-center q-pa-md flex flex-center">
<div>
@ -82,7 +77,7 @@ onMounted(async () => {
</div>
</div>
</template>
</QImg>
</VnImg>
<QBtn
v-if="showEditButton"
color="primary"
@ -97,7 +92,7 @@ onMounted(async () => {
collection="catalog"
:id="entityId"
@close-form="toggleEditPictureForm()"
@on-photo-uploaded="getItemAvatar()"
@on-photo-uploaded="handlePhotoUpdated"
/>
</QDialog>
</QBtn>

View File

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

View File

@ -3,14 +3,15 @@ import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import axios from 'axios';
import VnInput from 'components/common/VnInput.vue';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
import { useValidator } from 'src/composables/useValidator';
import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n();
const route = useRoute();
const props = defineProps({
dataKey: {
@ -21,32 +22,34 @@ const props = defineProps({
type: Array,
required: true,
},
tagValue: {
type: Array,
required: true,
},
});
const categoryList = ref(null);
const selectedCategoryFk = ref(null);
const typeList = ref(null);
const selectedTypeFk = ref(null);
const validationsStore = useValidator();
const selectedOrder = ref(null);
const selectedOrderField = ref(null);
const moreFields = ref([]);
const moreFieldsOrder = ref([]);
const createValue = (val, done) => {
if (val.length > 2) {
if (!tagOptions.value.includes(val)) {
done(tagOptions.value, 'add-unique');
}
tagValues.value.push({ value: val });
}
};
const resetCategory = () => {
selectedCategoryFk.value = null;
typeList.value = null;
};
const selectedOrder = ref(null);
const orderList = [
{ way: 'ASC', name: 'Ascendant' },
{ way: 'DESC', name: 'Descendant' },
];
const selectedOrderField = ref(null);
const OrderFields = [
{ field: 'relevancy DESC, name', name: 'Relevancy', priority: 999 },
{ field: 'showOrder, price', name: 'Color and price', priority: 999 },
{ field: 'name', name: 'Name', priority: 999 },
{ field: 'price', name: 'Price', priority: 999 },
];
const clearFilter = (key) => {
if (key === 'categoryFk') {
resetCategory();
@ -72,21 +75,6 @@ const loadTypes = async (categoryFk) => {
typeList.value = data;
};
const onFilterInit = async ({ params }) => {
if (params.typeFk) {
selectedTypeFk.value = params.typeFk;
}
if (params.categoryFk) {
await loadTypes(params.categoryFk);
selectedCategoryFk.value = params.categoryFk;
}
if (params.orderBy) {
orderByParam.value = JSON.parse(params.orderBy);
selectedOrder.value = orderByParam.value?.way;
selectedOrderField.value = orderByParam.value?.field;
}
};
const selectedCategory = computed(() =>
(categoryList.value || []).find(
(category) => category?.id === selectedCategoryFk.value
@ -109,10 +97,7 @@ function exprBuilder(param, value) {
const selectedTag = ref(null);
const tagValues = ref([{}]);
const tagOptions = ref(null);
const isButtonDisabled = computed(
() => !selectedTag.value || tagValues.value.some((item) => !item.value)
);
const tagOptions = ref([]);
const applyTagFilter = (params, search) => {
if (!tagValues.value?.length) {
@ -125,12 +110,12 @@ const applyTagFilter = (params, search) => {
}
params.tagGroups.push(
JSON.stringify({
values: tagValues.value,
values: tagValues.value.filter((obj) => Object.keys(obj).length > 0),
tagSelection: {
...selectedTag.value,
orgShowField: selectedTag.value.name,
orgShowField: selectedTag?.value?.name,
},
tagFk: selectedTag.value.tagFk,
tagFk: selectedTag?.value?.tagFk,
})
);
search();
@ -147,20 +132,48 @@ const removeTagChip = (selection, params, search) => {
search();
};
const orderByParam = ref(null);
const onOrderFieldChange = (value, params, search) => {
const orderBy = Object.assign({}, orderByParam.value, { field: value.field });
params.orderBy = JSON.stringify(orderBy);
search();
const onOrderChange = (value, params) => {
const tagObj = JSON.parse(params.orderBy);
tagObj.way = value.name;
params.orderBy = JSON.stringify(tagObj);
};
const onOrderChange = (value, params, search) => {
const orderBy = Object.assign({}, orderByParam.value, { way: value.way });
params.orderBy = JSON.stringify(orderBy);
search();
const onOrderFieldChange = (value, params) => {
const tagObj = JSON.parse(params.orderBy); // esto donde va
const fields = {
Relevancy: (value) => value + ' DESC, name',
ColorAndPrice: 'showOrder, price',
Name: 'name',
Price: 'price',
};
let tagField = fields[value];
if (!tagField) return;
if (typeof tagField === 'function') tagField = tagField(value);
tagObj.field = tagField;
params.orderBy = JSON.stringify(tagObj);
switch (value) {
case 'Relevancy':
tagObj.field = value + ' DESC, name';
params.orderBy = JSON.stringify(tagObj);
break;
case 'ColorAndPrice':
tagObj.field = 'showOrder, price';
params.orderBy = JSON.stringify(tagObj);
break;
case 'Name':
tagObj.field = 'name';
params.orderBy = JSON.stringify(tagObj);
break;
case 'Price':
tagObj.field = 'price';
params.orderBy = JSON.stringify(tagObj);
break;
}
};
const _moreFields = ['ASC', 'DESC'];
const _moreFieldsTypes = ['Relevancy', 'ColorAndPrice', 'Name', 'Price'];
const setCategoryList = (data) => {
categoryList.value = (data || [])
.filter((category) => category.display)
@ -168,6 +181,8 @@ const setCategoryList = (data) => {
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
moreFields.value = useLang(_moreFields);
moreFieldsOrder.value = useLang(_moreFieldsTypes);
};
const getCategoryClass = (category, params) => {
@ -175,6 +190,20 @@ const getCategoryClass = (category, params) => {
return 'active';
}
};
const useLang = (values) => {
const { models } = validationsStore;
const properties = models.Item?.properties || {};
return values.map((name) => {
let prop = properties[name];
const label = t(`params.${name}`);
return {
name,
label,
type: prop ? prop.type : null,
};
});
};
</script>
<template>
@ -182,10 +211,11 @@ const getCategoryClass = (category, params) => {
<VnFilterPanel
:data-key="props.dataKey"
:hidden-tags="['orderFk', 'orderBy']"
:unremovable-params="['orderFk', 'orderBy']"
:expr-builder="exprBuilder"
:custom-tags="['tagGroups']"
@init="onFilterInit"
@remove="clearFilter"
:redirect="false"
>
<template #tags="{ tag, formatFn }">
<strong v-if="tag.label === 'categoryFk'">
@ -274,40 +304,29 @@ const getCategoryClass = (category, params) => {
<QItem class="q-my-md">
<QItemSection>
<VnSelect
:label="t('params.order')"
:label="t('Order')"
v-model="selectedOrder"
:options="orderList || []"
option-value="way"
option-label="name"
:options="moreFields"
option-label="label"
dense
outlined
rounded
:emit-value="false"
use-input
:is-clearable="false"
@update:model-value="
(value) => onOrderChange(value, params, searchFn)
"
@update:model-value="(value) => onOrderChange(value, params)"
/>
</QItemSection>
</QItem>
<QItem class="q-mb-md">
<QItemSection>
<VnSelect
:label="t('params.order')"
:label="t('Order by')"
v-model="selectedOrderField"
:options="OrderFields || []"
option-value="field"
option-label="name"
:options="moreFieldsOrder"
option-label="label"
option-value="name"
dense
outlined
rounded
:emit-value="false"
use-input
:is-clearable="false"
@update:model-value="
(value) => onOrderFieldChange(value, params, searchFn)
"
@update:model-value="(value) => onOrderFieldChange(value, params)"
/>
</QItemSection>
</QItem>
@ -333,15 +352,30 @@ const getCategoryClass = (category, params) => {
:key="value"
class="q-mt-md filter-value"
>
<VnInput
v-if="selectedTag?.isFree"
v-model="value.value"
:label="t('params.value')"
is-outlined
class="filter-input"
<FetchData
v-if="selectedTag"
:url="`Tags/${selectedTag}/filterValue`"
limit="30"
auto-load
@on-fetch="(data) => (tagOptions = data)"
/>
<VnSelect
v-else
v-if="!selectedTag"
:label="t('params.value')"
v-model="value.value"
:options="tagValue || []"
option-value="value"
option-label="value"
dense
outlined
rounded
emit-value
use-input
class="filter-input"
@new-value="createValue"
/>
<VnSelect
v-else-if="selectedTag === 1"
:label="t('params.value')"
v-model="value.value"
:options="tagOptions || []"
@ -352,18 +386,18 @@ const getCategoryClass = (category, params) => {
rounded
emit-value
use-input
:disable="!selectedTag"
class="filter-input"
@new-value="createValue"
/>
<VnInput
v-else
:label="t('params.value')"
v-model="value.value"
dense
outlined
rounded
class="filter-input"
/>
<FetchData
v-if="selectedTag && !selectedTag.isFree"
:url="`Tags/${selectedTag?.id}/filterValue`"
limit="30"
auto-load
@on-fetch="(data) => (tagOptions = data)"
/>
<QIcon
name="delete"
class="filter-icon"
@ -388,7 +422,6 @@ const getCategoryClass = (category, params) => {
rounded
type="button"
unelevated
:disable="isButtonDisabled"
@click.stop="applyTagFilter(params, searchFn)"
/>
</QItemSection>
@ -453,6 +486,12 @@ en:
tag: Tag
value: Value
order: Order
ASC: Ascendant
DESC: Descendant
Relevancy: Relevancy
ColorAndPrice: Color and price
Name: Name
Price: Price
es:
params:
type: Tipo
@ -460,6 +499,14 @@ es:
tag: Etiqueta
value: Valor
order: Orden
ASC: Ascendiente
DESC: Descendiente
Relevancy: Relevancia
ColorAndPrice: Color y precio
Name: Nombre
Price: Precio
Order: Orden
Order by: Ordenar por
Plant: Planta
Flower: Flor
Handmade: Confección

View File

@ -3,16 +3,14 @@ import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnLv from 'components/ui/VnLv.vue';
import VnImg from 'src/components/ui/VnImg.vue';
import OrderCatalogItemDialog from 'pages/Order/Card/OrderCatalogItemDialog.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import { useSession } from 'composables/useSession';
import toCurrency from '../../../filters/toCurrency';
const DEFAULT_PRICE_KG = 0;
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const { t } = useI18n();
defineProps({
@ -29,14 +27,7 @@ const dialog = ref(null);
<div class="container order-catalog-item overflow-hidden">
<QCard class="card shadow-6">
<div class="img-wrapper">
<QImg
:src="`/api/Images/catalog/200x200/${item.id}/download?access_token=${token}`"
spinner-color="primary"
:ratio="1"
height="192"
width="192"
class="image"
/>
<VnImg :id="item.id" class="image" />
<div v-if="item.hex" class="item-color-container">
<div
class="item-color"
@ -59,7 +50,10 @@ const dialog = ref(null);
</template>
<div class="footer">
<div class="price">
<p>{{ item.available }} {{ t('to') }} {{ item.price }}</p>
<p>
{{ item.available }} {{ t('to') }}
{{ toCurrency(item.price) }}
</p>
<QIcon name="add_circle" class="icon">
<QTooltip>{{ t('globals.add') }}</QTooltip>
<QPopupProxy ref="dialog">

View File

@ -1,5 +1,5 @@
<script setup>
import { useRoute } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
@ -16,6 +16,7 @@ const route = useRoute();
const state = useState();
const ORDER_MODEL = 'order';
const router = useRouter();
const isNew = Boolean(!route.params.id);
const initialFormState = reactive({
clientFk: null,
@ -26,22 +27,19 @@ const initialFormState = reactive({
const clientList = ref([]);
const agencyList = ref([]);
const addressList = ref([]);
const clientId = ref(null);
const onClientsFetched = async (data) => {
try {
clientList.value = data;
initialFormState.clientFk = Number(route.query?.clientFk) || null;
const onClientsFetched = (data) => {
clientList.value = data;
initialFormState.clientFk = Number(route.query?.clientFk) || null;
clientId.value = initialFormState.clientFk;
if (initialFormState.clientFk) {
const { defaultAddressFk } = clientList.value.find(
(client) => client.id === initialFormState.clientFk
);
if (defaultAddressFk) await fetchAddressList(defaultAddressFk);
}
} catch (err) {
console.error('Error fetching clients', err);
}
const client = clientList.value.find(
(client) => client.id === initialFormState.clientFk
);
if (!client?.defaultAddressFk)
throw new Error(t(`No default address found for the client`));
fetchAddressList(client.defaultAddressFk);
};
const fetchAddressList = async (addressId) => {
@ -55,7 +53,6 @@ const fetchAddressList = async (addressId) => {
},
});
addressList.value = data;
// Set address by default
if (addressList.value?.length === 1) {
state.get(ORDER_MODEL).addressFk = addressList.value[0].id;
}
@ -121,6 +118,19 @@ const orderFilter = {
},
],
};
const onClientChange = async (clientId) => {
try {
const { data } = await axios.get(`Clients/${clientId}`);
await fetchAddressList(data.defaultAddressFk);
} catch (error) {
console.error('Error al cambiar el cliente:', error);
}
};
async function onDataSaved(data) {
await router.push({ path: `/order/${data}/catalog` });
}
</script>
<template>
@ -134,13 +144,15 @@ const orderFilter = {
<div class="q-pa-md">
<FormModel
:url="!isNew ? `Orders/${route.params.id}` : null"
:url-create="isNew ? 'Orders/new' : null"
url-create="Orders/new"
@on-data-saved="onDataSaved"
:model="ORDER_MODEL"
:form-initial-data="isNew ? initialFormState : null"
:observe-form-changes="!isNew"
:mapper="isNew ? orderMapper : null"
:filter="orderFilter"
@on-fetch="fetchOrderDetails"
auto-load
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
@ -151,9 +163,7 @@ const orderFilter = {
option-value="id"
option-label="name"
hide-selected
@update:model-value="
(client) => fetchAddressList(client.defaultAddressFk)
"
@update:model-value="onClientChange"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
@ -170,12 +180,10 @@ const orderFilter = {
v-model="data.addressFk"
:options="addressList"
option-value="id"
option-label="nickname"
option-label="street"
hide-selected
:disable="!addressList?.length"
@update:model-value="
() => fetchAgencyList(data.landed, data.addressFk)
"
@update:model-value="onAddressChange"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
@ -216,3 +224,8 @@ const orderFilter = {
</FormModel>
</div>
</template>
<i18n>
es:
No default address found for the client: No hay ninguna dirección asociada a este cliente.
</i18n>

View File

@ -4,7 +4,6 @@ import { useRoute } from 'vue-router';
import { onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import OrderCatalogItem from 'pages/Order/Card/OrderCatalogItem.vue';
import OrderCatalogFilter from 'pages/Order/Card/OrderCatalogFilter.vue';
@ -35,38 +34,31 @@ function extractTags(items) {
});
});
tags.value = resultTags;
extractValueTags(items);
}
const tagValue = ref([]);
function extractValueTags(items) {
const resultValueTags = items.flatMap((x) =>
Object.keys(x)
.filter((k) => /^value\d+$/.test(k))
.map((v) => x[v])
.filter((v) => v)
.sort()
);
tagValue.value = resultValueTags;
}
</script>
<template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
data-key="OrderCatalogList"
url="Orders/CatalogFilter"
:limit="50"
:user-params="catalogParams"
:static-params="['orderFk', 'orderBy']"
:redirect="false"
/>
</Teleport>
<Teleport v-if="stateStore.isHeaderMounted()" to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click.stop="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<OrderCatalogFilter data-key="OrderCatalogList" :tags="tags" />
<OrderCatalogFilter
data-key="OrderCatalogList"
:tag-value="tagValue"
:tags="tags"
/>
</QScrollArea>
</QDrawer>
<QPage class="column items-center q-pa-md">

View File

@ -7,19 +7,17 @@ import { useQuasar } from 'quasar';
import VnPaginate from 'components/ui/VnPaginate.vue';
import FetchData from 'components/FetchData.vue';
import VnLv from 'components/ui/VnLv.vue';
import CardList from 'components/ui/CardList.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnImg from 'components/ui/VnImg.vue';
import { toCurrency, toDate } from 'src/filters';
import { useSession } from 'composables/useSession';
import axios from 'axios';
import ItemDescriptorProxy from '../Item/Card/ItemDescriptorProxy.vue';
const route = useRoute();
const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const quasar = useQuasar();
const token = getTokenMultimedia();
const orderSummary = ref({
total: null,
vat: null,
@ -61,6 +59,56 @@ async function confirmOrder() {
type: 'positive',
});
}
const detailsColumns = ref([
{
name: 'img',
label: '',
field: (row) => row?.item?.id,
},
{
name: 'item',
label: t('order.summary.item'),
field: (row) => row?.item?.id,
sortable: true,
},
{
name: 'description',
label: t('globals.description'),
field: (row) => row?.item?.name,
},
{
name: 'warehouse',
label: t('warehouse'),
field: (row) => row?.warehouse?.name,
sortable: true,
},
{
name: 'shipped',
label: t('shipped'),
field: (row) => toDate(row?.shipped),
},
{
name: 'quantity',
label: t('order.summary.quantity'),
field: (row) => row?.quantity,
},
{
name: 'price',
label: t('order.summary.price'),
field: (row) => toCurrency(row?.price),
},
{
name: 'amount',
label: t('order.summary.amount'),
field: (row) => toCurrency(row?.quantity * row?.price),
},
{
name: 'actions',
label: '',
field: (row) => row?.id,
},
]);
</script>
<template>
@ -83,30 +131,33 @@ async function confirmOrder() {
auto-load
/>
<QPage :key="componentKey" class="column items-center q-pa-md">
<div class="vn-card-list">
<div class="order-list full-width">
<div v-if="!orderSummary.total" class="no-result">
{{ t('globals.noResults') }}
</div>
<QCard v-else class="order-lines-summary q-pa-lg">
<p class="header text-right block">
{{ t('summary') }}
</p>
<VnLv
v-if="orderSummary.vat && orderSummary.total"
:label="t('subtotal')"
:value="toCurrency(orderSummary.total - orderSummary.vat)"
/>
<VnLv
v-if="orderSummary.vat"
:label="t('VAT')"
:value="toCurrency(orderSummary?.vat)"
/>
<VnLv
v-if="orderSummary.total"
:label="t('total')"
:value="toCurrency(orderSummary?.total)"
/>
</QCard>
<QDrawer side="right" :width="270" show-if-above>
<QCard class="order-lines-summary q-pa-lg">
<p class="header text-right block">
{{ t('summary') }}
</p>
<VnLv
v-if="orderSummary.vat && orderSummary.total"
:label="t('subtotal')"
:value="toCurrency(orderSummary.total - orderSummary.vat)"
/>
<VnLv
v-if="orderSummary.vat"
:label="t('VAT')"
:value="toCurrency(orderSummary?.vat)"
/>
<VnLv
v-if="orderSummary.total"
:label="t('total')"
:value="toCurrency(orderSummary?.total)"
/>
</QCard>
</QDrawer>
<VnPaginate
data-key="OrderLines"
url="OrderRows"
@ -125,74 +176,71 @@ async function confirmOrder() {
}"
>
<template #body="{ rows }">
<div class="catalog-list q-mt-xl">
<CardList
v-for="row in rows"
:key="row.id"
:id="row.id"
:title="row?.item?.name"
class="cursor-inherit"
<div class="q-pa-md">
<QTable
:columns="detailsColumns"
:rows="rows"
flat
class="full-width"
style="text-align: center"
>
<template #title>
<div class="flex items-center">
<div class="image-wrapper q-mr-md">
<QImg
:src="`/api/Images/catalog/50x50/${row?.item?.id}/download?access_token=${token}`"
spinner-color="primary"
:ratio="1"
height="50"
width="50"
class="image"
/>
</div>
<div
class="title text-primary text-weight-bold text-h5"
<template #header="props">
<QTr class="tr-header" :props="props">
<QTh
v-for="col in props.cols"
:key="col.name"
:props="props"
style="text-align: center"
>
{{ row?.item?.name }}
{{ t(col.label) }}
</QTh>
</QTr>
</template>
<template #body-cell-img="{ value }">
<QTd>
<div class="image-wrapper">
<VnImg :id="value" class="rounded" />
</div>
<QChip class="q-chip-color" outline size="sm">
{{ t('ID') }}: {{ row.id }}
</QChip>
</div>
</QTd>
</template>
<template #list-items>
<div class="q-mb-sm">
<span class="text-uppercase subname">
{{ row.item.subName }}
<template #body-cell-item="{ value }">
<QTd class="item">
<span class="link">
<QBtn flat>
{{ value }}
</QBtn>
<ItemDescriptorProxy :id="value" />
</span>
<FetchedTags :item="row.item" :max-length="5" />
</div>
<VnLv :label="t('item')" :value="String(row.item.id)" />
<VnLv
:label="t('warehouse')"
:value="row.warehouse.name"
/>
<VnLv
:label="t('shipped')"
:value="toDate(row.shipped)"
/>
<VnLv
:label="t('quantity')"
:value="String(row.quantity)"
/>
<VnLv
:label="t('price')"
:value="toCurrency(row.price)"
/>
<VnLv
:label="t('amount')"
:value="toCurrency(row.price * row.quantity)"
/>
</QTd>
</template>
<template #actions v-if="!order?.isConfirmed">
<QBtn
:label="t('remove')"
@click.stop="confirmRemove(row)"
color="primary"
style="margin-top: 15px"
/>
<template #body-cell-description="{ row, value }">
<QTd>
<div
class="row column full-width justify-between items-start"
>
{{ value }}
<div v-if="value" class="subName">
{{ value.toUpperCase() }}
</div>
</div>
<FetchedTags :item="row.item" :max-length="6" />
</QTd>
</template>
</CardList>
<template #body-cell-actions="{ value }">
<QTd>
<QIcon
name="delete"
color="primary"
size="sm"
class="cursor-pointer"
@click.stop="confirmRemove(value)"
>
<QTooltip>{{ t('Remove thermograph') }}</QTooltip>
</QIcon>
</QTd>
</template>
</QTable>
</div>
</template>
</VnPaginate>
@ -239,14 +287,7 @@ async function confirmOrder() {
.image-wrapper {
height: 50px;
width: 50px;
.image {
border-radius: 50%;
}
}
.subname {
color: var(--vn-label-color);
margin-left: 30%;
}
.no-result {
@ -255,6 +296,11 @@ async function confirmOrder() {
color: var(--vn-label-color);
text-align: center;
}
.subName {
text-transform: uppercase;
color: var(--vn-label-color);
}
</style>
<i18n>
en:

View File

@ -240,4 +240,5 @@ es:
From: Desde
To: Hasta
Served: Servida
Days Onward: Días en adelante
</i18n>

View File

@ -3,14 +3,15 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'components/ui/VnLv.vue';
import { QIcon } from 'quasar';
import { dashIfEmpty, toCurrency, toDate, toHour } from 'src/filters';
import { openBuscaman } from 'src/utils/buscaman';
import CardSummary from 'components/ui/CardSummary.vue';
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
import { openBuscaman } from 'src/utils/buscaman';
import VnLv from 'components/ui/VnLv.vue';
import VnTitle from 'src/components/common/VnTitle.vue';
const $props = defineProps({
id: {
@ -128,8 +129,14 @@ const ticketColumns = ref([
<span>{{ `${entity?.route.id} - ${entity?.route?.description}` }}</span>
</template>
<template #body="{ entity }">
<QCard class="vn-max">
<VnTitle
:url="`#/route/${entityId}/basic-data`"
:text="t('globals.pageTitles.basicData')"
/>
</QCard>
<QCard class="vn-one">
<VnLv :label="t('ID')" :value="entity?.route.id" />
<VnLv
:label="t('route.summary.date')"
:value="toDate(entity?.route.created)"
@ -154,24 +161,6 @@ const ticketColumns = ref([
:label="t('route.summary.cost')"
:value="toCurrency(entity.route?.cost)"
/>
</QCard>
<QCard class="vn-one">
<VnLv
:label="t('route.summary.started')"
:value="toHour(entity?.route.started)"
/>
<VnLv
:label="t('route.summary.finished')"
:value="toHour(entity?.route.finished)"
/>
<VnLv
:label="t('route.summary.kmStart')"
:value="dashIfEmpty(entity?.route?.kmStart)"
/>
<VnLv
:label="t('route.summary.kmEnd')"
:value="dashIfEmpty(entity?.route?.kmEnd)"
/>
<VnLv
:label="t('route.summary.volume')"
:value="`${dashIfEmpty(entity?.route?.m3)} / ${dashIfEmpty(
@ -193,19 +182,32 @@ const ticketColumns = ref([
/>
</QCard>
<QCard class="vn-one">
<div class="header">
{{ t('globals.description') }}
</div>
<p>
{{ dashIfEmpty(entity?.route?.description) }}
</p>
<VnLv
:label="t('route.summary.started')"
:value="toHour(entity?.route.started)"
/>
<VnLv
:label="t('route.summary.finished')"
:value="toHour(entity?.route.finished)"
/>
<VnLv
:label="t('route.summary.kmStart')"
:value="dashIfEmpty(entity?.route?.kmStart)"
/>
<VnLv
:label="t('route.summary.kmEnd')"
:value="dashIfEmpty(entity?.route?.kmEnd)"
/>
<VnLv
:label="t('globals.description')"
:value="dashIfEmpty(entity?.route?.description)"
/>
</QCard>
<QCard class="vn-max">
<a class="header" :href="`#/route/${entityId}/tickets`">
{{ t('route.summary.tickets') }}
<QIcon name="open_in_new" color="primary" />
</a>
<VnTitle
:url="`#/route/${entityId}/tickets`"
:text="t('route.summary.tickets')"
/>
<QTable
:columns="ticketColumns"
:rows="entity?.tickets"

View File

@ -114,13 +114,7 @@ function downloadPdfs() {
</RightMenu>
<div class="column items-center">
<div class="list">
<VnPaginate
data-key="CmrList"
:url="`Routes/cmrs`"
order="cmrFk DESC"
limit="null"
auto-load
>
<VnPaginate data-key="CmrList" :url="`Routes/cmrs`" order="cmrFk DESC">
<template #body="{ rows }">
<QTable
:columns="columns"

View File

@ -67,6 +67,7 @@ const filter = {
},
},
],
where: { id: entityId },
};
const openAddStopDialog = () => {
@ -84,7 +85,7 @@ const openAddStopDialog = () => {
<CardSummary
data-key="RoadmapSummary"
ref="summary"
:url="`Roadmaps/${entityId}`"
:url="`Roadmaps`"
:filter="filter"
>
<template #header-left>

View File

@ -39,7 +39,7 @@ const selectedRows = ref([]);
const columns = computed(() => [
{
name: 'ID',
label: t('ID'),
label: 'Id',
field: (row) => row.routeFk,
sortable: true,
align: 'left',
@ -117,7 +117,9 @@ const columns = computed(() => [
const refreshKey = ref(0);
const total = computed(() => selectedRows.value.reduce((item) => item?.price || 0, 0));
const total = computed(() => {
return selectedRows.value.reduce((sum, item) => sum + item.price, 0);
});
const openDmsUploadDialog = async () => {
dmsDialog.value.rowsToCreateInvoiceIn = selectedRows.value
@ -212,7 +214,6 @@ function navigateToRouteSummary(event, row) {
data-key="RouteAutonomousList"
url="AgencyTerms/filter"
:limit="20"
auto-load
>
<template #body="{ rows }">
<div class="q-pa-md">
@ -306,6 +307,13 @@ function navigateToRouteSummary(event, row) {
cursor: pointer;
}
}
th:last-child,
td:last-child {
background-color: var(--vn-section-color);
position: sticky;
right: 0;
}
</style>
<i18n>
es:

View File

@ -1,40 +1,50 @@
<script setup>
import VnPaginate from 'components/ui/VnPaginate.vue';
import { useStateStore } from 'stores/useStateStore';
import { useI18n } from 'vue-i18n';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { dashIfEmpty, toHour } from 'src/filters';
import VnSelect from 'components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import { useValidator } from 'composables/useValidator';
import { useSession } from 'composables/useSession';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useArrayData } from 'composables/useArrayData';
import { useQuasar } from 'quasar';
import axios from 'axios';
import RouteSearchbar from 'pages/Route/Card/RouteSearchbar.vue';
import FetchData from 'components/FetchData.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import RouteSummary from 'pages/Route/Card/RouteSummary.vue';
import RouteFilter from 'pages/Route/Card/RouteFilter.vue';
import RouteListTicketsDialog from 'pages/Route/Card/RouteListTicketsDialog.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import axios from 'axios';
import RouteSearchbar from 'pages/Route/Card/RouteSearchbar.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import RouteFilter from 'pages/Route/Card/RouteFilter.vue';
import RouteSummary from 'pages/Route/Card/RouteSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useSession } from 'composables/useSession';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import RouteListTicketsDialog from 'pages/Route/Card/RouteListTicketsDialog.vue';
import { useQuasar } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
import RightMenu from 'src/components/common/RightMenu.vue';
const stateStore = useStateStore();
const { t } = useI18n();
const { validate } = useValidator();
const { viewSummary } = useSummaryDialog();
const quasar = useQuasar();
const session = useSession();
const { viewSummary } = useSummaryDialog();
const paginate = ref();
const visibleColumns = ref([]);
const selectedRows = ref([]);
const workers = ref([]);
const agencyList = ref([]);
const vehicleList = ref([]);
const allColumnNames = ref([]);
const confirmationDialog = ref(false);
const startingDate = ref(null);
const refreshKey = ref(0);
const columns = computed(() => [
{
name: 'ID',
label: t('ID'),
name: 'Id',
label: t('Id'),
field: (row) => row.id,
sortable: true,
align: 'center',
@ -109,14 +119,12 @@ const columns = computed(() => [
align: 'right',
},
]);
const arrayData = useArrayData('EntryLatestBuys', {
url: 'Buys/latestBuysFilter',
order: ['itemFk DESC'],
});
const refreshKey = ref(0);
const workers = ref([]);
const agencyList = ref([]);
const vehicleList = ref([]);
const updateRoute = async (route) => {
try {
return await axios.patch(`Routes/${route.id}`, route);
@ -124,9 +132,6 @@ const updateRoute = async (route) => {
return err;
}
};
const allColumnNames = ref([]);
const confirmationDialog = ref(false);
const startingDate = ref(null);
const cloneRoutes = () => {
axios.post('Routes/clone', {
@ -135,6 +140,7 @@ const cloneRoutes = () => {
});
refreshKey.value++;
startingDate.value = null;
paginate.value.fetch();
};
const showRouteReport = () => {
@ -154,15 +160,13 @@ const showRouteReport = () => {
window.open(url, '_blank');
};
const markAsServed = () => {
selectedRows.value.forEach((row) => {
if (row?.id) {
axios.patch(`Routes/${row?.id}`, { isOk: true });
}
function markAsServed() {
selectedRows.value.forEach(async (row) => {
if (row?.id) await axios.patch(`Routes/${row?.id}`, { isOk: true });
});
refreshKey.value++;
startingDate.value = null;
};
}
const openTicketsDialog = (id) => {
if (!id) {
@ -179,11 +183,9 @@ const openTicketsDialog = (id) => {
};
onMounted(async () => {
stateStore.rightDrawer = true;
allColumnNames.value = columns.value.map((col) => col.name);
await arrayData.fetch({ append: false });
});
onUnmounted(() => (stateStore.rightDrawer = false));
</script>
<template>
@ -210,6 +212,10 @@ onUnmounted(() => (stateStore.rightDrawer = false));
<QBtn flat :label="t('Cancel')" v-close-popup class="text-primary" />
<QBtn color="primary" v-close-popup @click="cloneRoutes">
{{ t('globals.clone') }}
<VnLv
:label="t('route.summary.packages')"
:value="getTotalPackages(entity.tickets)"
/>
</QBtn>
</QCardActions>
</QCard>
@ -228,7 +234,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
class="LeftIcon"
:all-columns="allColumnNames"
table-code="routesList"
labels-traductions-path="globals"
labels-traductions-path="route.columnLabels"
@on-config-saved="visibleColumns = [...$event]"
/>
</template>
@ -256,7 +262,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="markAsServed"
@click="markAsServed()"
>
<QTooltip>{{ t('Mark as served') }}</QTooltip>
</QBtn>
@ -269,7 +275,6 @@ onUnmounted(() => (stateStore.rightDrawer = false));
url="Routes/filter"
:order="['created ASC', 'started ASC', 'id ASC']"
:limit="20"
auto-load
>
<template #body="{ rows }">
<div class="q-pa-md route-table">
@ -500,7 +505,6 @@ en:
hourStarted: Started hour
hourFinished: Finished hour
es:
ID: ID
Worker: Trabajador
Agency: Agencia
Vehicle: Vehículo
@ -521,4 +525,6 @@ es:
Summary: Resumen
Route is closed: La ruta está cerrada
Route is not served: La ruta no está servida
hourStarted: Hora de inicio
hourFinished: Hora de fin
</i18n>

View File

@ -128,8 +128,8 @@ function confirmRemove() {
.onOk(() => refreshKey.value++);
}
function navigateToRoadmapSummary(event, row) {
router.push({ name: 'RoadmapSummary', params: { id: row.id } });
function navigateToRoadmapSummary(_, { id }) {
router.push({ name: 'RoadmapSummary', params: { id } });
}
</script>
@ -193,7 +193,6 @@ function navigateToRoadmapSummary(event, row) {
url="Roadmaps"
:limit="20"
:filter="filter"
auto-load
>
<template #body="{ rows }">
<div class="q-pa-md">

View File

@ -141,7 +141,7 @@ const setOrderedPriority = async () => {
};
const sortRoutes = async () => {
await axios.get(`Routes/${route.params?.id}/guessPriority/`);
await axios.patch(`Routes/${route.params?.id}/guessPriority/`);
refreshKey.value++;
};

View File

@ -104,10 +104,14 @@ const totalEntryPrice = (rows) => {
for (const row of rows) {
let total = 0;
let quantity = 0;
for (const buy of row.buys) {
total = total + buy.total;
quantity = quantity + buy.quantity;
if (row.buys) {
for (const buy of row.buys) {
total = total + buy.total;
quantity = quantity + buy.quantity;
}
}
row.total = total;
row.quantity = quantity;
totalPrice = totalPrice + total;

View File

@ -50,7 +50,7 @@ const itemCategoriesOptions = ref([]);
@on-fetch="(data) => (itemCategoriesOptions = data)"
auto-load
/>
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<VnFilterPanel :data-key="props.dataKey" :search-button="true" :redirect="false">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>

View File

@ -0,0 +1,263 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import FetchData from 'components/FetchData.vue';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency } from 'filters/index';
import { useRole } from 'src/composables/useRole';
const $props = defineProps({
formData: {
type: Object,
required: true,
},
haveNegatives: {
type: Boolean,
required: true,
default: false,
},
});
const emit = defineEmits(['updateForm', 'update:haveNegatives']);
const stateStore = useStateStore();
const { t } = useI18n();
const { hasAny } = useRole();
const _ticketData = ref($props.formData);
const ticketUpdateActions = ref(null);
const haveNegatives = computed({
get: () => $props.haveNegatives,
set: (val) => emit('update:haveNegatives', val),
});
const rows = computed(() => _ticketData.value?.sale?.items || []);
watch(
() => _ticketData.value,
(val) => emit('updateForm', val),
{ deep: true }
);
const columns = computed(() => [
{
required: true,
label: t('basicData.item'),
name: 'item',
align: 'left',
format: (val) => val.name,
},
{
required: true,
label: t('basicData.description'),
name: 'description',
align: 'left',
hidden: true,
},
{
label: t('basicData.movable'),
name: 'movable',
align: 'left',
},
{
required: true,
label: t('basicData.quantity'),
name: 'quantity',
field: 'quantity',
align: 'left',
},
{
required: true,
label: t('basicData.pricePPU'),
name: 'price',
field: 'price',
align: 'left',
format: (val) => toCurrency(val),
},
{
required: true,
label: t('basicData.newPricePPU'),
name: 'newPrice',
field: (row) => row.component.newPrice,
align: 'left',
format: (val) => toCurrency(val),
},
{
required: true,
label: t('basicData.difference'),
name: 'difference',
field: (row) => row.component.difference,
align: 'left',
format: (val) => toCurrency(val),
},
]);
const loadDefaultTicketAction = () => {
const isSalesAssistant = hasAny(['salesAssistant']);
_ticketData.value.option = isSalesAssistant ? 'mana' : 'renewPrices';
};
const totalPrice = computed(() => {
return rows.value.reduce((acc, item) => acc + item.price * item.quantity, 0);
});
const totalNewPrice = computed(() => {
return rows.value.reduce(
(acc, item) => acc + item.component.newPrice * item.quantity,
0
);
});
const totalDifference = computed(() => {
return rows.value.reduce((acc, item) => acc + item.component?.difference || 0, 0);
});
const showMovablecolumn = computed(() => (haveDifferences.value > 0 ? ['movable'] : []));
const haveDifferences = computed(() => _ticketData.value.sale?.haveDifferences);
const ticketHaveNegatives = () => {
let _haveNegatives = false;
let haveNotNegatives = false;
_ticketData.value.withoutNegatives = false;
_ticketData.value?.sale?.items.forEach((item) => {
if (item.quantity > item.movable) _haveNegatives = true;
else haveNotNegatives = true;
});
haveNegatives.value = _haveNegatives && haveNotNegatives && haveDifferences.value;
if (haveNegatives.value) _ticketData.value.withoutNegatives = true;
};
onMounted(() => {
stateStore.rightDrawer = true;
loadDefaultTicketAction();
ticketHaveNegatives();
});
onUnmounted(() => (stateStore.rightDrawer = false));
</script>
<template>
<FetchData
url="TicketUpdateActions"
@on-fetch="(data) => (ticketUpdateActions = data)"
auto-load
/>
<RightMenu>
<template #right-panel>
<QCard
class="q-pa-md q-mb-md q-ma-md color-vn-text"
bordered
flat
style="border-color: black"
>
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('basicData.total') }}
</span>
</QCardSection>
<QCardSection class="column items-center" horizontal>
<span>
{{ t('basicData.price') }}:
{{ toCurrency(totalPrice) }}
</span>
</QCardSection>
<QCardSection class="column items-center" horizontal>
<span>
{{ t('basicData.newPrice') }}: {{ toCurrency(totalNewPrice) }}
</span>
</QCardSection>
<QCardSection class="column items-center" horizontal>
<span>
{{ t('basicData.difference') }}: {{ toCurrency(totalDifference) }}
</span>
</QCardSection>
</QCard>
<QCard
v-if="totalDifference"
class="q-pa-md q-mb-md q-ma-md color-vn-text"
bordered
flat
style="border-color: black"
>
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('basicData.chargeDifference') }}
</span>
</QCardSection>
<QCardSection
v-for="(action, index) in ticketUpdateActions"
:key="index"
horizontal
>
<QRadio
v-model="_ticketData.option"
:val="action.code"
:label="action.description"
dense
/>
</QCardSection>
</QCard>
<QCard
v-if="haveNegatives"
class="q-pa-md q-mb-md q-ma-md color-vn-text"
bordered
flat
style="border-color: black"
>
<QCardSection horizontal class="flex row items-center">
<QCheckbox
:label="t('basicData.withoutNegatives')"
v-model="_ticketData.withoutNegatives"
:toggle-indeterminate="false"
/>
<QIcon name="info" size="xs" class="q-ml-sm">
<QTooltip max-width="350px">
{{ t('basicData.withoutNegativesInfo') }}
</QTooltip>
</QIcon>
</QCardSection>
</QCard>
</template>
</RightMenu>
<QTable
:visible-columns="showMovablecolumn"
:rows="rows"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
flat
>
<template #body-cell-item="{ row }">
<QTd>
<QBtn flat color="primary">
{{ row.itemFk }}
<ItemDescriptorProxy :id="row.itemFk" />
</QBtn>
</QTd>
</template>
<template #body-cell-description="{ row }">
<QTd>
<div class="column">
<span>{{ row.item.name }}</span>
<span class="color-vn-label">{{ row.item.subName }}</span>
<FetchedTags :item="row.item" :max-length="6" />
</div>
</QTd>
</template>
<template #body-cell-movable="{ row }">
<QTd>
<QBadge
v-if="_ticketData?.sale?.haveDifferences"
:text-color="row.quantity > row.movable ? 'black' : 'white'"
:color="row.quantity > row.movable ? 'negative' : 'transparent'"
:label="row.movable"
/>
</QTd>
</template>
</QTable>
</template>

View File

@ -0,0 +1,468 @@
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import { toTimeFormat } from 'filters/date.js';
const $props = defineProps({
formData: {
type: Object,
required: true,
default: () => ({}),
},
});
const emit = defineEmits(['updateForm']);
const { notify } = useNotify();
const router = useRouter();
const { t } = useI18n();
const agencyFetchRef = ref(null);
const zonesFetchRef = ref(null);
const clientsOptions = ref([]);
const warehousesOptions = ref([]);
const companiesOptions = ref([]);
const agenciesOptions = ref([]);
const zonesOptions = ref([]);
const addresses = ref([]);
const formData = ref($props.formData);
watch(
() => formData.value,
(val) => emit('updateForm', val),
{ deep: true }
);
const agencyByWarehouseFilter = computed(() => ({
fields: ['id', 'name'],
order: 'name ASC',
where: {
warehouseFk: warehouseId.value,
},
}));
const zonesFilter = computed(() => ({
fields: ['id', 'name'],
order: 'name ASC',
where: formData.value?.agencyModeFk
? {
shipped: formData.value?.shipped,
addressFk: formData.value?.addressFk,
agencyModeFk: formData.value?.agencyModeFk,
warehouseFk: formData.value?.warehouseFk,
}
: {},
}));
const getLanded = async (params) => {
try {
const validParams =
shipped.value && addressId.value && agencyModeId.value && warehouseId.value;
if (!validParams) return;
formData.value.zoneFk = null;
zonesOptions.value = [];
const { data } = await axios.get(`Agencies/getLanded`, { params });
if (data) {
formData.value.zoneFk = data.zoneFk;
formData.value.landed = data.landed;
formData.value.shipped = params.shipped;
}
} catch (error) {
console.error(error);
notify(t('basicData.noDeliveryZoneAvailable'), 'negative');
}
};
const getShipped = async (params) => {
try {
const validParams =
landed.value && addressId.value && agencyModeId.value && warehouseId.value;
if (!validParams) return;
formData.value.zoneFk = null;
zonesOptions.value = [];
const { data } = await axios.get(`Agencies/getShipped`, { params });
if (data) {
formData.value.zoneFk = data.zoneFk;
formData.value.landed = params.landed;
formData.value.shipped = data.shipped;
} else {
notify(t('basicData.noDeliveryZoneAvailable'), 'negative');
}
} catch (error) {
console.error(error);
notify(t('basicData.noDeliveryZoneAvailable'), 'negative');
}
};
const onChangeZone = async (zoneId) => {
try {
formData.value.agencyModeFk = null;
const { data } = await axios.get(`Zones/${zoneId}`);
formData.value.agencyModeFk = data.agencyModeFk;
} catch (error) {
console.error(error);
}
};
const onChangeAddress = async (addressId) => {
try {
formData.value.nickname = null;
const { data } = await axios.get(`Addresses/${addressId}`);
formData.value.nickname = data.nickname;
} catch (error) {
console.error(error);
}
};
const getClientDefaultAddress = async (clientId) => {
try {
const { data } = await axios.get(`Clients/${clientId}`);
if (data) addressId.value = data.defaultAddressFk;
} catch (error) {
console.error(error);
}
};
const clientAddressesList = async (value) => {
let filter = {
include: [
{
relation: 'province',
scope: {
fields: ['name'],
},
},
{
relation: 'agencyMode',
scope: {
fields: ['name'],
},
},
],
};
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get(`Clients/${value}/addresses`, { params });
if (data) addresses.value = data;
};
const addressId = computed({
get: () => formData.value?.addressFk,
set: (val) => {
if (val != formData.value?.addressFk) {
formData.value.addressFk = val;
onChangeAddress(val);
getShipped({
landed: formData.value?.landed,
addressFk: val,
agencyModeFk: formData.value?.agencyModeFk,
warehouseFk: formData.value?.warehouseFk,
});
}
},
});
const clientId = computed({
get: () => formData.value?.clientFk,
set: (val) => {
formData.value.clientFk = val;
formData.value.addressFk = null;
if (!val) return;
getClientDefaultAddress(val);
clientAddressesList(val);
},
});
const landed = computed({
get: () => formData.value?.landed,
set: (val) => {
formData.value.landed = val;
getShipped({
landed: val,
addressFk: formData.value?.addressFk,
agencyModeFk: formData.value?.agencyModeFk,
warehouseFk: formData.value?.warehouseFk,
});
},
});
const agencyModeId = computed({
get: () => formData.value.agencyModeFk,
set: (val) => {
if (val != formData.value.agencyModeFk) {
formData.value.agencyModeFk = val;
if (!val) return;
const agencyMode = agenciesOptions.value.find((a) => a.id == val);
formData.value.warehouseFk = agencyMode.warehouseFk;
getLanded({
shipped: formData.value?.shipped,
addressFk: formData.value?.addressFk,
agencyModeFk: val,
warehouseFk: formData.value?.warehouseFk,
});
}
},
});
const zoneId = computed({
get: () => formData.value?.zoneFk,
set: (val) => {
if (val != formData.value?.zoneFk) {
formData.value.zoneFk = val;
onChangeZone(val);
}
},
});
const warehouseId = computed({
get: () => formData.value?.warehouseFk,
set: (val) => {
if (val != formData.value?.warehouseFk) {
formData.value.warehouseFk = val;
getShipped({
landed: formData.value?.landed,
addressFk: formData.value?.addressFk,
agencyModeFk: formData.value?.agencyModeFk,
warehouseFk: val,
}).then(() => {
if (zoneId.value == null) formData.value.agencyModeFk = null;
});
}
},
});
const shipped = computed({
get: () => formData.value?.shipped,
set: (val) => {
if (new Date(formData.value?.shipped).toDateString() != val.toDateString())
val.setHours(0, 0, 0, 0);
formData.value.shipped = val;
getLanded({
shipped: val,
addressFk: formData.value?.addressFk,
agencyModeFk: formData.value?.agencyModeFk,
warehouseFk: formData.value?.warehouseFk,
});
},
});
const onFormModelInit = () => {
if (formData.value?.clientFk) clientAddressesList(formData.value?.clientFk);
};
const redirectToCustomerAddress = () => {
router.push({
name: 'CustomerAddressEditCard',
params: { id: clientId.value, addressId: addressId.value },
});
};
onMounted(() => onFormModelInit());
</script>
<template>
<FetchData
url="Clients"
:filter="{
fields: ['id', 'name'],
order: 'id',
}"
@on-fetch="(data) => (clientsOptions = data)"
auto-load
/>
<FetchData
url="Warehouses"
@on-fetch="(data) => (warehousesOptions = data)"
auto-load
/>
<FetchData
url="Companies"
:filter="{
fields: ['id', 'code'],
order: 'code ASC',
}"
@on-fetch="(data) => (companiesOptions = data)"
auto-load
/>
<FetchData
ref="agencyFetchRef"
url="AgencyModes/byWarehouse"
:filter="agencyByWarehouseFilter"
@on-fetch="(data) => (agenciesOptions = data)"
auto-load
/>
<FetchData
ref="zonesFetchRef"
url="Zones/includingExpired"
:filter="zonesFilter"
@on-fetch="(data) => (zonesOptions = data)"
auto-load
/>
<QForm>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelect
:label="t('basicData.client')"
v-model="clientId"
option-value="id"
option-label="name"
:options="clientsOptions"
hide-selected
map-options
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel
>#{{ scope.opt?.id }} {{ scope.opt?.name }}</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
:label="t('basicData.warehouse')"
v-model="warehouseId"
option-value="id"
option-label="name"
:options="warehousesOptions"
hide-selected
map-options
:required="true"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnSelect
:label="t('basicData.address')"
v-model="addressId"
option-value="id"
option-label="nickname"
:options="addresses"
hide-selected
map-options
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel
:class="{
'color-vn-label': !scope.opt?.isActive,
}"
>
{{
`${
!scope.opt?.isActive
? t('basicData.inactive')
: ''
} `
}}
<span> {{ scope.opt?.nickname }}</span>
<span
v-if="
scope.opt?.province ||
scope.opt?.city ||
scope.opt?.street
"
>, {{ scope.opt?.street }}, {{ scope.opt?.city }},
{{ scope.opt?.province?.name }} -
{{ scope.opt?.agencyMode?.name }}</span
>
</QItemLabel>
</QItemSection>
</QItem>
</template>
<template #append>
<QIcon
name="edit"
color="primary"
size="sm"
class="fill-icon cursor-pointer"
@click.stop="redirectToCustomerAddress()"
>
<QTooltip>{{ t('basicData.editAddress') }}</QTooltip>
</QIcon>
</template>
</VnSelect>
<VnInput
:label="t('basicData.alias')"
v-model="formData.nickname"
:required="true"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md no-wrap">
<VnSelect
:label="t('basicData.company')"
v-model="formData.companyFk"
option-value="id"
option-label="code"
:options="companiesOptions"
hide-selected
map-options
:required="true"
/>
<VnSelect
:label="t('basicData.agency')"
v-model="agencyModeId"
option-value="id"
option-label="name"
:options="agenciesOptions"
hide-selected
map-options
@focus="agencyFetchRef.fetch()"
/>
<VnSelect
:label="t('basicData.zone')"
v-model="zoneId"
option-value="id"
option-label="name"
:options="zonesOptions"
hide-selected
map-options
:required="true"
@focus="zonesFetchRef.fetch()"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel
>{{ scope.opt?.name }} - Max.
{{ toTimeFormat(scope.opt?.hour) }}
h.</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelect>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInputDate
:label="t('basicData.shipped')"
v-model="formData.shipped"
:required="true"
/>
<VnInputTime
:label="t('basicData.shippedHour')"
v-model="formData.shipped"
:required="true"
/>
<VnInputDate
:label="t('basicData.landed')"
v-model="formData.landed"
:required="true"
/>
</VnRow>
</QForm>
</template>

View File

@ -0,0 +1,195 @@
<script setup>
import { ref, onBeforeMount } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import BasicDataTable from './BasicDataTable.vue';
import TicketBasicDataForm from './TicketBasicDataForm.vue';
import { useVnConfirm } from 'composables/useVnConfirm';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const { notify } = useNotify();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const stepperRef = ref(null);
const { openConfirmationModal } = useVnConfirm();
const step = ref(1);
const formData = ref({});
const initialDataLoaded = ref(false);
const haveNegatives = ref(false);
const ticketFilter = {
include: [
{ relation: 'address' },
{
relation: 'client',
scope: {
fields: [
'salesPersonFk',
'name',
'isActive',
'isFreezed',
'isTaxDataChecked',
'credit',
'email',
'phone',
'mobile',
'hasElectronicInvoice',
],
include: {
relation: 'salesPersonUser',
scope: { fields: ['id', 'name'] },
},
},
},
{ relation: 'invoiceOut' },
],
};
const getTicketData = async () => {
const params = { filter: JSON.stringify(ticketFilter) };
const { data } = await axios.get(`tickets/${route.params.id}`, { params });
formData.value = data;
initialDataLoaded.value = true;
};
const isFormInvalid = () => {
return (
!formData.value.clientFk ||
!formData.value.addressFk ||
!formData.value.agencyModeFk ||
!formData.value.companyFk ||
!formData.value.shipped ||
!formData.value.landed ||
!formData.value.zoneFk
);
};
const getPriceDifference = async () => {
try {
const params = {
landed: formData.value.landed,
addressId: formData.value.addressFk,
agencyModeId: formData.value.agencyModeFk,
zoneId: formData.value.zoneFk,
warehouseId: formData.value.warehouseFk,
shipped: formData.value.shipped,
};
const { data } = await axios.post(
`tickets/${formData.value.id}/priceDifference`,
params
);
formData.value.sale = data;
} catch (error) {
console.error(error);
}
};
const submit = async () => {
try {
if (!formData.value.option)
return notify(t('basicData.chooseAnOption'), 'negative');
const params = {
clientFk: formData.value.clientFk,
nickname: formData.value.nickname,
agencyModeFk: formData.value.agencyModeFk,
addressFk: formData.value.addressFk,
zoneFk: formData.value.zoneFk,
warehouseFk: formData.value.warehouseFk,
companyFk: formData.value.companyFk,
shipped: formData.value.shipped,
landed: formData.value.landed,
isDeleted: formData.value.isDeleted,
option: formData.value.option,
isWithoutNegatives: formData.value.withoutNegatives,
withWarningAccept: formData.value.withWarningAccept,
keepPrice: false,
};
const { data } = await axios.post(
`tickets/${formData.value.id}/componentUpdate`,
params
);
if (!data) return;
const ticketToMove = data.id;
notify(t('basicData.unroutedTicket'), 'positive');
router.push({ name: 'TicketSummary', params: { id: ticketToMove } });
} catch (error) {
console.error(error);
}
};
const submitWithNegatives = async () => {
formData.value.withWarningAccept = true;
submit();
};
const onNextStep = async () => {
if (step.value === 1) {
if (isFormInvalid())
return notify(t('basicData.someFieldsAreInvalid'), 'negative');
await getPriceDifference();
stepperRef.value.next();
} else if (step.value === 2) {
if (haveNegatives.value && !formData.value.withoutNegatives)
openConfirmationModal(
t('basicData.negativesConfirmTitle'),
t('basicData.negativesConfirmMessage'),
submitWithNegatives
);
else submit();
}
};
onBeforeMount(async () => await getTicketData());
</script>
<template>
<QStepper
v-model="step"
ref="stepperRef"
color="primary"
animated
keep-alive
style="max-width: 800px; margin: auto"
>
<QStep :name="1" :title="t('globals.pageTitles.basicData')" :done="step > 1">
<TicketBasicDataForm
v-if="initialDataLoaded"
@update-form="($event) => (formData = $event)"
:form-data="formData"
/>
</QStep>
<QStep :name="2" :title="t('basicData.priceDifference')">
<BasicDataTable
:form-data="formData"
v-model:haveNegatives="haveNegatives"
@update-form="($event) => (formData = $event)"
/>
</QStep>
<template #navigation>
<QStepperNavigation class="flex justify-between">
<QBtn
flat
color="primary"
@click="stepperRef.previous()"
:label="t('basicData.back')"
class="q-ml-sm"
:class="{ invisible: step === 1 }"
/>
<QBtn
@click="onNextStep()"
color="primary"
:label="step === 2 ? t('basicData.finalize') : t('basicData.next')"
/>
</QStepperNavigation>
</template>
</QStepper>
</template>

View File

@ -1,3 +0,0 @@
<template>
<QCard>Basic Data</QCard>
</template>

View File

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

View File

@ -0,0 +1,69 @@
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import FormModelPopup from 'components/FormModelPopup.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
const emit = defineEmits(['onRequestCreated']);
const route = useRoute();
const { t } = useI18n();
const attendersOptions = ref([]);
</script>
<template>
<FetchData
url="TicketRequests/getItemTypeWorker"
:filter="{ fields: ['id', 'nickname'], order: 'nickname ASC' }"
auto-load
@on-fetch="(data) => (attendersOptions = data)"
/>
<FormModelPopup
:title="t('Create request')"
url-create="TicketRequests"
model="CreateTicketRequest"
:form-initial-data="{ ticketFk: route.params.id }"
@on-data-saved="() => emit('onRequestCreated')"
>
<template #form-inputs="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
v-model="data.description"
:label="t('purchaseRequest.description')"
/>
<VnSelect
:label="t('purchaseRequest.atender')"
v-model="data.attenderFk"
:options="attendersOptions"
hide-selected
option-label="nickname"
option-value="id"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
v-model="data.quantity"
:label="t('purchaseRequest.quantity')"
type="number"
min="1"
/>
<VnInput
v-model="data.price"
:label="t('purchaseRequest.price')"
type="number"
min="0"
/>
</VnRow>
</template>
</FormModelPopup>
</template>
<i18n>
es:
Create request: Crear petición de compra
</i18n>

View File

@ -0,0 +1,49 @@
<script setup>
import { reactive, ref, onMounted, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import VnInput from 'src/components/common/VnInput.vue';
import VnRow from 'components/ui/VnRow.vue';
import FormModelPopup from 'components/FormModelPopup.vue';
const { t } = useI18n();
const emit = defineEmits(['onDataSaved']);
const nameInputRef = ref(null);
const serviceFormData = reactive({});
const onDataSaved = (formData, requestResponse) => {
emit('onDataSaved', formData, requestResponse);
};
onMounted(async () => {
await nextTick();
nameInputRef.value.focus();
});
</script>
<template>
<FormModelPopup
url-create="TicketServiceTypes"
model="TicketServiceType"
:title="t('New service type')"
:form-initial-data="serviceFormData"
@on-data-saved="onDataSaved"
>
<template #form-inputs="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
ref="nameInputRef"
:label="t('service.description')"
v-model="data.name"
:required="true"
/>
</VnRow>
</template>
</FormModelPopup>
</template>
<i18n>
es:
New service type: Nuevo tipo de servicio
</i18n>

View File

@ -0,0 +1,86 @@
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import FormModelPopup from 'components/FormModelPopup.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import { useState } from 'src/composables/useState';
const emit = defineEmits(['onRequestCreated']);
const route = useRoute();
const { t } = useI18n();
const state = useState();
const user = state.getUser();
const stateFetchDataRef = ref(null);
const statesOptions = ref([]);
const workersOptions = ref([]);
const onStateFkChange = (formData) => (formData.userFk = user.value.id);
</script>
<template>
<FetchData
ref="stateFetchDataRef"
url="States"
auto-load
@on-fetch="(data) => (statesOptions = data)"
/>
<FetchData
url="Workers/search"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
auto-load
@on-fetch="(data) => (workersOptions = data)"
/>
<FormModelPopup
:title="t('Create tracking')"
url-create="Tickets/state"
model="CreateTicketTracking"
:form-initial-data="{ ticketFk: route.params.id }"
@on-data-saved="() => emit('onRequestCreated')"
>
<template #form-inputs="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<VnSelect
v-model="data.stateFk"
:label="t('tracking.state')"
:options="statesOptions"
@update:model-value="onStateFkChange(data)"
hide-selected
option-label="name"
option-value="id"
/>
<VnSelect
:label="t('tracking.worker')"
v-model="data.userFk"
:options="workersOptions"
hide-selected
option-label="name"
option-value="id"
>
<template #option="{ opt, itemProps }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>
{{ opt.name }}
</QItemLabel>
<QItemLabel caption>
{{ opt.nickname }}, {{ opt.code }}
</QItemLabel>
</QItemSection>
</QItem>
</template></VnSelect
>
</VnRow>
</template>
</FormModelPopup>
</template>
<i18n>
es:
Create tracking: Crear estado
</i18n>

View File

@ -71,7 +71,7 @@ const filter = {
const data = ref(useCardDescription());
const setData = (entity) =>
(data.value = useCardDescription(entity.client.name, entity.id));
(data.value = useCardDescription(entity.client?.name, entity.id));
</script>
<template>
@ -92,7 +92,7 @@ const setData = (entity) =>
<template #value>
<span class="link">
{{ entity.clientFk }}
<CustomerDescriptorProxy :id="entity.client.id" />
<CustomerDescriptorProxy :id="entity.client?.id" />
</span>
</template>
</VnLv>
@ -109,8 +109,8 @@ const setData = (entity) =>
<VnLv :label="t('ticket.summary.salesPerson')">
<template #value>
<VnUserLink
:name="entity.client.salesPersonUser?.name"
:worker-id="entity.client.salesPersonFk"
:name="entity.client?.salesPersonUser?.name"
:worker-id="entity.client?.salesPersonFk"
/>
</template>
</VnLv>

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

@ -0,0 +1,10 @@
<script setup>
import VnLog from 'src/components/common/VnLog.vue';
import { useRoute } from 'vue-router';
const route = useRoute();
</script>
<template>
<VnLog model="Ticket" url="/TicketLogs" :key="route.params.id"></VnLog>
</template>

View File

@ -0,0 +1,106 @@
<script setup>
import { ref, watch, computed, reactive } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CrudModel from 'components/CrudModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import { useArrayData } from 'src/composables/useArrayData';
const route = useRoute();
const { t } = useI18n();
const ticketNotesCrudRef = ref(null);
const observationTypes = ref([]);
const arrayData = useArrayData('TicketNotes');
const { store } = arrayData;
const crudModelFilter = reactive({
where: { ticketFk: route.params.id },
fields: ['id', 'ticketFk', 'observationTypeFk', 'description'],
});
const crudModelRequiredData = computed(() => ({ ticketFk: route.params.id }));
watch(
() => route.params.id,
async () => {
crudModelFilter.where.ticketFk = route.params.id;
store.filter = crudModelFilter;
await ticketNotesCrudRef.value.reload();
}
);
</script>
<template>
<FetchData
@on-fetch="(data) => (observationTypes = data)"
auto-load
url="ObservationTypes"
/>
<div class="flex justify-center">
<CrudModel
ref="ticketNotesCrudRef"
data-key="TicketNotes"
url="TicketObservations"
model="TicketNotes"
:filter="crudModelFilter"
:data-required="crudModelRequiredData"
:default-remove="false"
auto-load
style="max-width: 800px"
>
<template #body="{ rows }">
<QCard class="q-px-lg q-py-md">
<div
v-for="(row, index) in rows"
:key="index"
class="q-mb-md row items-center q-gutter-x-md"
>
<VnSelect
:label="t('ticketNotes.observationType')"
:options="observationTypes"
hide-selected
option-label="description"
option-value="id"
v-model="row.observationTypeFk"
:disable="!!row.id"
/>
<VnInput
:label="t('ticketNotes.description')"
v-model="row.description"
class="col"
/>
<QIcon
name="delete"
size="sm"
class="cursor-pointer"
color="primary"
@click="ticketNotesCrudRef.remove([row])"
>
<QTooltip>
{{ t('ticketNotes.removeNote') }}
</QTooltip>
</QIcon>
</div>
<VnRow v-if="observationTypes.length > rows.length">
<QIcon
name="add_circle"
class="fill-icon-on-hover q-ml-md"
size="sm"
color="primary"
@click="ticketNotesCrudRef.insert()"
>
<QTooltip>
{{ t('ticketNotes.addNote') }}
</QTooltip>
</QIcon>
</VnRow>
</QCard>
</template>
</CrudModel>
</div>
</template>

View File

@ -0,0 +1,267 @@
<script setup>
import { ref, computed, watch, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnInput from 'src/components/common/VnInput.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import CrudModel from 'src/components/CrudModel.vue';
import TicketCreateRequest from './TicketCreateRequest.vue';
import { dashIfEmpty } from 'src/filters';
import { toDateFormat } from 'src/filters/date.js';
const route = useRoute();
const { t } = useI18n();
const createTicketRequestDialogRef = ref(null);
const crudModelRef = ref(null);
watch(
() => route.params.id,
async (val) => {
crudModelFilter.where.ticketFk = val;
crudModelRef.value.reload();
}
);
const crudModelFilter = reactive({
include: [
{
relation: 'atender',
scope: {
include: {
relation: 'user',
scope: {
fields: ['nickname'],
},
},
},
},
{
relation: 'requester',
scope: {
include: {
relation: 'user',
scope: {
fields: ['nickname'],
},
},
},
},
{
relation: 'sale',
},
],
fields: [
'id',
'description',
'created',
'requesterFk',
'attenderFk',
'quantity',
'price',
'saleFk',
'isOk',
],
order: ['created ASC'],
where: {
ticketFk: route.params.id,
},
});
const columns = computed(() => [
{
label: t('purchaseRequest.id'),
name: 'id',
field: 'id',
align: 'left',
columnFilter: null,
},
{
label: t('purchaseRequest.description'),
name: 'description',
field: 'description',
align: 'left',
format: (val) => dashIfEmpty(val),
},
{
label: t('purchaseRequest.created'),
name: 'created',
field: 'created',
align: 'left',
format: (val) => toDateFormat(val),
},
{
label: t('purchaseRequest.requester'),
name: 'requester',
align: 'left',
sortable: true,
},
{
label: t('purchaseRequest.atender'),
name: 'atender',
align: 'left',
},
{
label: t('purchaseRequest.quantity'),
name: 'quantity',
align: 'left',
},
{
label: t('purchaseRequest.price'),
name: 'price',
align: 'left',
},
{
label: t('purchaseRequest.saleFk'),
name: 'saleFk',
align: 'left',
},
{
label: t('purchaseRequest.state'),
name: 'state',
field: 'isOk',
align: 'left',
format: (val) => t(getRequestState(val)),
},
{
label: '',
name: 'actions',
align: 'left',
columnFilter: null,
},
]);
const getRequestState = (state) => {
switch (state) {
case null:
return 'New';
case false:
return 'Denied';
case true:
return 'Acepted';
}
};
const isEditable = (isOk) => isOk !== null;
const removeLine = async (row) => crudModelRef.value.remove([row]);
const openCreateModal = () => createTicketRequestDialogRef.value.show();
</script>
<template>
<QPage class="column items-center q-pa-md">
<CrudModel
data-key="PurchaseRequests"
url="TicketRequests"
ref="crudModelRef"
:filter="crudModelFilter"
:order="['created ASC']"
:default-remove="false"
:default-save="false"
:default-reset="false"
:limit="0"
auto-load
>
<template #body="{ rows }">
<QTable
:rows="rows"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
@row-click="(_, row) => redirectToTicketSummary(row.ticketFk)"
>
<template #body-cell-description="{ row }">
<QTd @click.stop>
<VnInput
v-model="row.description"
@blur="crudModelRef.saveChanges()"
:disable="isEditable(row.isOk)"
/>
</QTd>
</template>
<template #body-cell-requester="{ row }">
<QTd @click.stop>
<QBtn flat color="primary">
{{ row.requester?.user?.nickname }}
<WorkerDescriptorProxy :id="row.requesterFk" />
</QBtn>
</QTd>
</template>
<template #body-cell-atender="{ row }">
<QTd @click.stop>
<QBtn flat color="primary">
{{ row.atender?.user?.nickname }}
<WorkerDescriptorProxy :id="row.attenderFk" />
</QBtn>
</QTd>
</template>
<template #body-cell-quantity="{ row }">
<QTd @click.stop>
<VnInput
v-model="row.quantity"
@blur="crudModelRef.saveChanges()"
:disable="isEditable(row.isOk)"
/>
</QTd>
</template>
<template #body-cell-price="{ row }">
<QTd @click.stop>
<VnInput
v-model="row.price"
@blur="crudModelRef.saveChanges()"
:disable="isEditable(row.isOk)"
/>
</QTd>
</template>
<template #body-cell-saleFk="{ row }">
<QTd @click.stop>
<QBtn v-if="row.saleFk" flat color="primary">
{{ row.sale.itemFk }}
<ItemDescriptorProxy :id="row.sale.itemFk" />
</QBtn>
</QTd>
</template>
<template #body-cell-actions="{ row }">
<QTd>
<QIcon
@click.stop="removeLine(row)"
class="q-ml-sm cursor-pointer"
color="primary"
name="delete"
size="sm"
>
<QTooltip>
{{ t('globals.delete') }}
</QTooltip>
</QIcon>
</QTd>
</template>
</QTable>
</template>
</CrudModel>
<QDialog
ref="createTicketRequestDialogRef"
transition-show="scale"
transition-hide="scale"
>
<TicketCreateRequest @on-request-created="crudModelRef.reload()" />
</QDialog>
<QPageSticky :offset="[20, 20]">
<QBtn @click="openCreateModal()" color="primary" fab icon="add" />
<QTooltip class="text-no-wrap">
{{ t('purchaseRequest.newRequest') }}
</QTooltip>
</QPageSticky>
</QPage>
</template>
<i18n>
es:
New: Nueva
Denied: Denegada
Accepted: Aceptada
</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) => {
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>
<QBtn
v-if="row.$hasLogs"
@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,190 @@
<script setup>
import { ref, watch, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CrudModel from 'components/CrudModel.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import FetchData from 'components/FetchData.vue';
import TicketCreateServiceType from './TicketCreateServiceType.vue';
import VnInput from 'src/components/common/VnInput.vue';
import { useArrayData } from 'src/composables/useArrayData';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const ticketServiceTypeFetchRef = ref(null);
const ticketServiceCrudRef = ref(null);
const ticketServiceOptions = ref([]);
const arrayData = useArrayData('TicketNotes');
const { store } = arrayData;
const { notify } = useNotify();
const selected = ref([]);
const defaultTaxClass = ref(null);
const crudModelFilter = computed(() => ({
where: { ticketFk: route.params.id },
}));
const crudModelRequiredData = computed(() => ({
ticketFk: route.params.id,
taxClassFk: defaultTaxClass.value?.id,
}));
watch(
() => route.params.id,
async () => {
store.filter = crudModelFilter.value;
await ticketServiceCrudRef.value.reload();
}
);
onMounted(async () => await getDefaultTaxClass());
const createRefund = async () => {
try {
if (!selected.value.length) return;
const params = {
servicesIds: selected.value.map((s) => +s.ticketFk),
withWarehouse: false,
negative: true,
};
const { data } = await axios.post('Sales/clone', params);
const [refundTicket] = data;
notify(
t('service.createRefundSuccess', {
ticketId: refundTicket.id,
}),
'positive'
);
router.push({ name: 'TicketSale', params: { id: refundTicket.id } });
} catch (error) {
console.error(error);
}
};
const getDefaultTaxClass = async () => {
try {
let filter = {
where: { code: 'G' },
};
const { data } = await axios.get('TaxClasses/findOne', {
params: { filter: JSON.stringify(filter) },
});
defaultTaxClass.value = data;
console.log('defaultTaxClass', defaultTaxClass.value);
} catch (error) {
console.error(error);
}
};
const columns = computed(() => [
{
name: 'description',
label: t('service.description'),
field: (row) => row.ticketServiceTypeFk,
sortable: true,
align: 'left',
},
{
name: 'quantity',
label: t('service.quantity'),
field: (row) => row.quantity,
sortable: true,
align: 'left',
},
{
name: 'price',
label: t('service.price'),
field: (row) => row.price,
sortable: true,
align: 'left',
},
]);
</script>
<template>
<FetchData
ref="ticketServiceTypeFetchRef"
@on-fetch="(data) => (ticketServiceOptions = data)"
auto-load
url="TicketServiceTypes"
/>
<CrudModel
ref="ticketServiceCrudRef"
data-key="TicketService"
url="TicketServices"
model="TicketService"
:filter="crudModelFilter"
:data-required="crudModelRequiredData"
auto-load
v-model:selected="selected"
>
<template #moreBeforeActions>
<QBtn
color="primary"
:label="t('service.pay')"
:disabled="!selected.length"
@click.stop="createRefund()"
/>
</template>
<template #body="{ rows }">
<QTable
:columns="columns"
:rows="rows"
row-key="$index"
selection="multiple"
v-model:selected="selected"
table-header-class="text-left"
>
<template #body-cell-description="{ row, col }">
<QTd auto-width>
<VnSelectDialog
:label="col.label"
v-model="row.ticketServiceTypeFk"
:options="ticketServiceOptions"
option-label="name"
option-value="id"
hide-selected
>
<template #form>
<TicketCreateServiceType
@on-data-saved="ticketServiceTypeFetchRef.fetch()"
/>
</template>
</VnSelectDialog>
</QTd>
</template>
<template #body-cell-quantity="{ row, col }">
<QTd auto-width>
<VnInput
:label="col.label"
v-model.number="row.quantity"
type="number"
min="0"
:info="t('service.quantityInfo')"
/>
</QTd>
</template>
<template #body-cell-price="{ row, col }">
<QTd auto-width>
<VnInput
:label="col.label"
v-model.number="row.price"
type="number"
min="0"
/>
</QTd>
</template>
</QTable>
</template>
</CrudModel>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="ticketServiceCrudRef.insert()" />
</QPageSticky>
</template>

View File

@ -0,0 +1,121 @@
<script setup>
import { ref, computed, watch, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import TicketCreateTracking from './TicketCreateTracking.vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import { toDateTimeFormat } from 'src/filters/date.js';
const route = useRoute();
const { t } = useI18n();
const createTrackingDialogRef = ref(null);
const paginateRef = ref(null);
watch(
() => route.params.id,
async (val) => {
paginateFilter.where.ticketFk = val;
paginateRef.value.fetch();
}
);
const paginateFilter = reactive({
include: [
{
relation: 'user',
scope: {
fields: ['id', 'name'],
include: {
relation: 'worker',
scope: {
fields: ['id'],
},
},
},
},
{
relation: 'state',
scope: {
fields: ['name'],
},
},
],
order: ['created DESC'],
where: {
ticketFk: route.params.id,
},
});
const columns = computed(() => [
{
label: t('tracking.state'),
name: 'state',
field: 'state',
align: 'left',
format: (val) => val.name,
},
{
label: t('tracking.worker'),
name: 'worker',
align: 'left',
},
{
label: t('tracking.created'),
name: 'created',
field: 'created',
align: 'left',
format: (val) => toDateTimeFormat(val),
},
]);
const openCreateModal = () => createTrackingDialogRef.value.show();
</script>
<template>
<QPage class="column items-center q-pa-md">
<VnPaginate
ref="paginateRef"
data-key="TicketTracking"
:filter="paginateFilter"
url="TicketTrackings"
auto-load
>
<template #body="{ rows }">
<QTable
:rows="rows"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
>
<template #body-cell-worker="{ row }">
<QTd @click.stop>
<QBtn flat color="primary">
{{ row.user?.name }}
<WorkerDescriptorProxy :id="row.user?.worker?.id" />
</QBtn>
</QTd>
</template>
</QTable>
</template>
</VnPaginate>
<QDialog
ref="createTrackingDialogRef"
transition-show="scale"
transition-hide="scale"
>
<TicketCreateTracking @on-request-created="paginateRef.fetch()" />
</QDialog>
<QPageSticky :offset="[20, 20]">
<QBtn @click="openCreateModal()" color="primary" fab icon="add" />
<QTooltip class="text-no-wrap">
{{ t('tracking.addState') }}
</QTooltip>
</QPageSticky>
</QPage>
</template>

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>

Some files were not shown because too many files have changed in this diff Show More