0
0
Fork 0

Merge branch '6825-vnTable' of https://gitea.verdnatura.es/verdnatura/salix-front into 6553-workerBusiness

This commit is contained in:
Carlos Satorres 2024-06-19 06:50:20 +02:00
commit b6836fc59e
36 changed files with 1166 additions and 947 deletions

View File

@ -1,3 +1,100 @@
# Version 24.24 - 2024-06-11
### Added 🆕
- feat: 6942 hashtag in key : value summary by:jgallego
- feat: #6957: Rename FetchedTags instance tag by:Javier Segarra
- feat: refactor template by:Javier Segarra
- feat: refs #6600 Add option to add comment for photo motivation by:jorgep
- feat: refs #6942 test e2e tobook & toUnbook by:jorgep
- feat: refs #6942 to book summary button & reactive value by:jorgep
- feat: refs #6942 to unbook by:jorgep
- feat: refs #6942 url update by:jorgep
- feat: refs #6942 use correct currency in InvoiceIn components by:jorgep
- feat: refs #6942 vat rate total by:jorgep
- feat: refs #7494 new icons (7494-icons) by:alexm
- feat: refs #7494 new icons by:alexm
- feat: refs #7542 drop space by:jorgep
- feat: refs #7542 empty by:jorgep
- fix: refs #6942 changes and new features by:jorgep
- fix: style by:Javier Segarra
- style: color transparent when is fetive by:Javier Segarra
- style: fix color when is empty by:Javier Segarra
- style: reset poc style (6957_refactorFetechedTags) by:Javier Segarra
- style: reset poc style by:Javier Segarra
- style updates by:Javier Segarra
### Changed 📦
- feat: refactor template by:Javier Segarra
- perf: 6957 add color as new shared variable by:Javier Segarra
- perf: 6957 change fetchedTags color by:Javier Segarra
- perf: remove local tree variable by:Javier Segarra
- refactor: add flat by:alexm
- refactor: refs #6600 replace QInput to VnInput by:jorgep
- refactor: refs #6652 improved defaulter section by:Jon
- refactor: refs #6942 Fix getTotalAmount function to correctly calculate the total amount in InvoiceInDueDay.vue by:jorgep
- refactor: refs #6942 new summary layout by:jorgep
- refactor: refs #6942 store key & actions by:jorgep
- refactor: refs #6942 summary by:jorgep
- refactor: refs #6942 use router hook by:jorgep
- refactor: refs #6942 WIP summary layout by:jorgep
### Fixed 🛠️
- fix: 9-12 by:Javier Segarra
- fix: defaulter icon by:alexm
- fix: refs #5186 validation by:jorgep
- fix: refs #6095 add reFfk null on search by:pablone
- fix: refs #6942 cardDescriptor use store if its popup or different source data by:jorgep
- fix: refs #6942 changes and new features by:jorgep
- fix: refs #6942 drop comments by:jorgep
- fix: refs #6942 drop console by:jorgep
- fix: refs #6942 drop console.log by:jorgep
- fix: refs #6942 e2e test (origin/6942-warmfix-fixFormModel) by:jorgep
- fix: refs #6942 e2e tests by:jorgep
- fix: refs #6942 e2e tests by:jorgep
- fix: refs #6942 fix emit on data saved by:jorgep
- fix: refs #6942 fix emit on reset by:jorgep
- fix: refs #6942 fix vncard by:jorgep
- fix: refs #6942 formModel & CardDescriptor by:jorgep
- fix: refs #6942 formModel watch changes & invoiceInCreate by:jorgep
- fix: refs #6942 import by:jorgep
- fix: refs #6942 reloading by:jorgep
- fix: refs #6942 rollback by:jorgep
- fix: refs #6942 selectable expense by:jorgep
- fix: refs #6942 skip e2e tests by:jorgep
- fix: refs #6942 table bottom highlight & drop isBooked field by:jorgep
- fix: refs #6942 tests e2e by:jorgep
- fix: refs #6942 tests & summary table spacing by:jorgep
- fix: refs #6942 unit tests by:jorgep
- fix: refs #6942 vnLocation by:jorgep
- fix: refs #6942 wip: formModel by:jorgep
- fix: refs #7542 use right panel by:jorgep
- fix: searchbar redirect by:alexm
- fix: style by:Javier Segarra
- fix: WorkerCalendarItem by:Javier Segarra
- mini fix by:wbuezas
- refs #6111 clean code fix changes by:carlossa
- refs #6111 fix merge, fix column by:carlossa
- refs #6111 fix qtable, actions, scroll by:carlossa
- refs #6111 fix routeList by:carlossa
- refs #6111 fix sticky by:carlossa
- refs #6111 fix trad remove logs by:carlossa
- refs #6111 fix visibleColumns by:carlossa
- refs #6111 routeList fix by:carlossa
- refs #6332 fix calendar by:carlossa
- refs #6332 fix colors by:carlossa
- refs #6332 fix festive by:carlossa
- refs #6820 fix BasicData Tickets by:carlossa
- refs #6820 fix error front by:carlossa
- refs #6820 fix traduction by:carlossa
- refs #7391 fix textarea by:carlossa
- refs #7396 fix summary by:carlossa
- Search childs fix by:wbuezas
- small fix by:wbuezas
- style: fix color when is empty by:Javier Segarra
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.

4
Jenkinsfile vendored
View File

@ -94,7 +94,7 @@ pipeline {
sh 'quasar build' sh 'quasar build'
script { script {
def packageJson = readJSON file: 'package.json' def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
} }
dockerBuild() dockerBuild()
} }
@ -106,7 +106,7 @@ pipeline {
steps { steps {
script { script {
def packageJson = readJSON file: 'package.json' def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
} }
withKubeConfig([ withKubeConfig([
serverUrl: "$KUBERNETES_API", serverUrl: "$KUBERNETES_API",

34
changelog.sh Normal file
View File

@ -0,0 +1,34 @@
features_types=(chore feat style)
changes_types=(refactor perf)
fix_types=(fix revert)
file="CHANGELOG.md"
file_tmp="temp_log.txt"
file_current_tmp="temp_current_log.txt"
setType(){
echo "### $1" >> $file_tmp
arr=("$@")
echo "" > $file_current_tmp
for i in "${arr[@]}"
do
git log --grep="$i" --oneline --no-merges --format="- %s %d by:%an" master..test >> $file_current_tmp
done
# remove duplicates
sort -o $file_current_tmp -u $file_current_tmp
cat $file_current_tmp >> $file_tmp
echo "" >> $file_tmp
# remove tmp current file
[ -e $file_current_tmp ] && rm $file_current_tmp
}
echo "# Version XX.XX - XXXX-XX-XX" >> $file_tmp
echo "" >> $file_tmp
setType "Added 🆕" "${features_types[@]}"
setType "Changed 📦" "${changes_types[@]}"
setType "Fixed 🛠️" "${fix_types[@]}"
cat $file >> $file_tmp
mv $file_tmp $file

View File

@ -67,6 +67,10 @@ const $props = defineProps({
default: '', default: '',
description: 'It is used for redirect on click "save and continue"', description: 'It is used for redirect on click "save and continue"',
}, },
hasSubtoolbar: {
type: Boolean,
default: true,
},
}); });
const isLoading = ref(false); const isLoading = ref(false);
@ -310,7 +314,7 @@ watch(formUrl, async () => {
</template> </template>
</VnPaginate> </VnPaginate>
<SkeletonTable v-if="!formData" :columns="$attrs.columns?.length" /> <SkeletonTable v-if="!formData" :columns="$attrs.columns?.length" />
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()"> <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubtoolbar">
<QBtnGroup push style="column-gap: 10px"> <QBtnGroup push style="column-gap: 10px">
<slot name="moreBeforeActions" /> <slot name="moreBeforeActions" />
<QBtn <QBtn

View File

@ -12,6 +12,7 @@ import SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue'; import VnConfirm from './ui/VnConfirm.vue';
import { tMobile } from 'src/composables/tMobile'; import { tMobile } from 'src/composables/tMobile';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { useRoute } from 'vue-router';
const { push } = useRouter(); const { push } = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
@ -20,6 +21,7 @@ const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const { validate } = useValidator(); const { validate } = useValidator();
const { notify } = useNotify(); const { notify } = useNotify();
const route = useRoute();
const $props = defineProps({ const $props = defineProps({
url: { url: {
@ -28,7 +30,7 @@ const $props = defineProps({
}, },
model: { model: {
type: String, type: String,
default: '', default: null,
}, },
filter: { filter: {
type: Object, type: Object,
@ -82,17 +84,18 @@ const $props = defineProps({
description: 'It is used for redirect on click "save and continue"', description: 'It is used for redirect on click "save and continue"',
}, },
}); });
const emit = defineEmits(['onFetch', 'onDataSaved']); const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed(
() => $props.model ?? `formModel_${route?.meta?.title ?? route.name}`
).value;
const componentIsRendered = ref(false); const componentIsRendered = ref(false);
const arrayData = useArrayData($props.model); const arrayData = useArrayData(modelValue);
const isLoading = ref(false); const isLoading = ref(false);
// Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas // Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas
const isResetting = ref(false); const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges); const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({}); const originalData = ref({});
const formData = computed(() => state.get($props.model)); const formData = computed(() => state.get(modelValue));
const formUrl = computed(() => $props.url); const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({ const defaultButtons = computed(() => ({
save: { save: {
@ -114,11 +117,12 @@ onMounted(async () => {
nextTick(() => (componentIsRendered.value = true)); nextTick(() => (componentIsRendered.value = true));
// Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla // Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla
state.set($props.model, $props.formInitialData); state.set(modelValue, $props.formInitialData);
if ($props.autoLoad && !$props.formInitialData && $props.url) await fetch();
else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data);
if (!$props.formInitialData) {
if ($props.autoLoad && $props.url) await fetch();
else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data);
}
if ($props.observeFormChanges) { if ($props.observeFormChanges) {
watch( watch(
() => formData.value, () => formData.value,
@ -161,8 +165,8 @@ onBeforeRouteLeave((to, from, next) => {
onUnmounted(() => { onUnmounted(() => {
// Restauramos los datos originales en el store si se realizaron cambios en el formulario pero no se guardaron, evitando modificaciones erróneas. // Restauramos los datos originales en el store si se realizaron cambios en el formulario pero no se guardaron, evitando modificaciones erróneas.
if (hasChanges.value) return state.set($props.model, originalData.value); if (hasChanges.value) return state.set(modelValue, originalData.value);
if ($props.clearStoreOnUnmount) state.unset($props.model); if ($props.clearStoreOnUnmount) state.unset(modelValue);
}); });
async function fetch() { async function fetch() {
@ -174,7 +178,7 @@ async function fetch() {
updateAndEmit('onFetch', data); updateAndEmit('onFetch', data);
} catch (e) { } catch (e) {
state.set($props.model, {}); state.set(modelValue, {});
originalData.value = {}; originalData.value = {};
} }
} }
@ -235,11 +239,11 @@ function filter(value, update, filterOptions) {
} }
function updateAndEmit(evt, val, res) { function updateAndEmit(evt, val, res) {
state.set($props.model, val); state.set(modelValue, val);
originalData.value = val && JSON.parse(JSON.stringify(val)); originalData.value = val && JSON.parse(JSON.stringify(val));
if (!$props.url) arrayData.store.data = val; if (!$props.url) arrayData.store.data = val;
emit(evt, state.get($props.model), res); emit(evt, state.get(modelValue), res);
} }
defineExpose({ save, isLoading, hasChanges }); defineExpose({ save, isLoading, hasChanges });

View File

@ -134,26 +134,28 @@ const col = computed(() => {
const components = computed(() => $props.components ?? defaultComponents); const components = computed(() => $props.components ?? defaultComponents);
</script> </script>
<template> <template>
<VnComponent <div class="row no-wrap fit">
v-if="col.before" <VnComponent
:prop="col.before" v-if="col.before"
:components="components" :prop="col.before"
:value="model" :components="components"
v-model="model" :value="model"
/> v-model="model"
<VnComponent />
v-if="col.component" <VnComponent
:prop="col" v-if="col.component"
:components="components" :prop="col"
:value="model" :components="components"
v-model="model" :value="model"
/> v-model="model"
<span :title="value" v-else>{{ value }}</span> />
<VnComponent <span :title="value" v-else>{{ value }}</span>
v-if="col.after" <VnComponent
:prop="col.after" v-if="col.after"
:components="components" :prop="col.after"
:value="model" :components="components"
v-model="model" :value="model"
/> v-model="model"
/>
</div>
</template> </template>

View File

@ -53,7 +53,7 @@ const $props = defineProps({
}, },
isEditable: { isEditable: {
type: Boolean, type: Boolean,
default: null, default: false,
}, },
useModel: { useModel: {
type: Boolean, type: Boolean,
@ -83,7 +83,6 @@ const tableModes = [
icon: 'grid_view', icon: 'grid_view',
title: t('grid view'), title: t('grid view'),
value: 'card', value: 'card',
disable: () => console.log('called'),
}, },
]; ];
@ -212,232 +211,245 @@ defineExpose({
</VnFilterPanel> </VnFilterPanel>
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<CrudModel <!-- class in div to fix warn-->
v-bind="$attrs" <div class="q-px-md">
class="q-px-md" <CrudModel
:limit="20" v-bind="$attrs"
ref="CrudModelRef" :limit="20"
:search-url="searchUrl" ref="CrudModelRef"
:disable-infinite-scroll="mode == 'table'" :search-url="searchUrl"
@save-changes="reload" :disable-infinite-scroll="mode == 'table'"
> @save-changes="reload"
<template #body="{ rows }"> :has-subtoolbar="isEditable"
<QTable >
class="vnTable" <template #body="{ rows }">
:columns="splittedColumns.columns" <QTable
:rows="rows" v-bind="$attrs['QTable']"
row-key="id" class="vnTable"
selection="multiple" :columns="splittedColumns.columns"
v-model:selected="selected" :rows="rows"
:grid="mode != 'table'" v-model:selected="selected"
table-header-class="bg-header" :grid="mode != 'table'"
card-container-class="grid-three" table-header-class="bg-header"
flat card-container-class="grid-three"
:style="mode == 'table' && 'max-height: 92vh'" flat
virtual-scroll :style="mode == 'table' && 'max-height: 90vh'"
@virtual-scroll=" virtual-scroll
(event) => @virtual-scroll="
event.index > rows.length - 2 && (event) =>
CrudModelRef.vnPaginateRef.paginate() event.index > rows.length - 2 &&
" CrudModelRef.vnPaginateRef.paginate()
@row-click="(_, row) => rowClickFunction(row)" "
> @row-click="(_, row) => rowClickFunction(row)"
<template #top-left> >
<slot name="top-left"></slot> <template #top-left>
</template> <slot name="top-left"></slot>
<template #top-right> </template>
<!-- <QBtn <template #top-right>
<!-- <QBtn
icon="visibility" icon="visibility"
title="asd" title="asd"
class="bg-vn-section-color q-mr-md" class="bg-vn-section-color q-mr-md"
dense dense
v-if="mode == 'table'" v-if="mode == 'table'"
/> --> /> -->
<QBtnToggle <QBtnToggle
v-model="mode" v-model="mode"
toggle-color="primary" toggle-color="primary"
class="bg-vn-section-color" class="bg-vn-section-color"
dense dense
:options="tableModes" :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="`text-${col.align ?? 'left'}`">
<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="`text-${col.align ?? 'left'}`"
>
<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="`text-${col.align ?? 'left'}`"
class="sticky no-padding"
@click="stopEventPropagation($event)"
>
<QBtn <QBtn
v-for="(btn, index) of col.actions" icon="filter_alt"
:key="index" title="asd"
:title="btn.title" class="bg-vn-section-color q-ml-md"
:icon="btn.icon" dense
class="q-px-sm" @click="stateStore.toggleRightDrawer()"
flat
:class="
btn.isPrimary ? 'text-primary-light' : 'color-vn-text '
"
@click="btn.action(row)"
/> />
</QTd> </template>
</template> <template #header-cell="{ col }">
<template #item="{ row, colsMap }"> <QTh
<component auto-width
:is="$props.redirect ? 'router-link' : 'span'" style="min-width: 100px"
:to="`/${$props.redirect}/` + row.id" v-if="$props.columnSearch"
>
<QCard
bordered
flat
class="row no-wrap justify-between cursor-pointer"
@click="
(_, row) => {
$props.rowClick && $props.rowClick(row);
}
"
> >
<QCardSection <VnTableFilter
vertical :column="col"
class="no-margin no-padding" :show-title="true"
:class="colsMap.tableActions ? 'w-80' : 'fit'" :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="`text-${col.align ?? 'left'}`">
<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="`text-${col.align ?? 'left'}`"
>
<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="`text-${col.align ?? 'left'}`"
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);
}
"
> >
<!-- Chips -->
<QCardSection <QCardSection
v-if="splittedColumns.chips.length" vertical
class="no-margin q-px-xs q-py-none" class="no-margin no-padding"
:class="colsMap.tableActions ? 'w-80' : 'fit'"
> >
<VnTableChip <!-- Chips -->
:columns="splittedColumns.chips" <QCardSection
:row="row" v-if="splittedColumns.chips.length"
class="no-margin q-px-xs q-py-none"
> >
<template #afterChip> <VnTableChip
<slot name="afterChip" :row="row"></slot> :columns="splittedColumns.chips"
</template> :row="row"
</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> <template #afterChip>
<span <slot name="afterChip" :row="row"></slot>
@click="stopEventPropagation($event)"
>
<VnTableColumn
:column="col"
:row="row"
:is-editable="false"
v-model="row[col.name]"
component-prop="columnField"
:show-label="true"
/>
</span>
</template> </template>
</VnLv> </VnTableChip>
</div> </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> </QCardSection>
</QCardSection> <!-- Actions -->
<!-- Actions --> <QCardSection
<QCardSection v-if="colsMap.tableActions"
v-if="colsMap.tableActions" class="column flex-center w-10 no-margin q-pa-xs q-gutter-y-xs"
class="column flex-center w-10 no-margin q-pa-xs q-gutter-y-xs" @click="stopEventPropagation($event)"
@click="stopEventPropagation($event)" >
> <QBtn
<QBtn v-for="(btn, index) of splittedColumns.actions
v-for="(btn, index) of splittedColumns.actions .actions"
.actions" :key="index"
:key="index" :title="btn.title"
:title="btn.title" :icon="btn.icon"
:icon="btn.icon" class="q-pa-xs"
class="q-pa-xs" flat
flat :class="
:class=" btn.isPrimary
btn.isPrimary ? 'text-primary-light'
? 'text-primary-light' : 'color-vn-text '
: 'color-vn-text ' "
" @click="btn.action(row)"
@click="btn.action(row)" />
/> </QCardSection>
</QCardSection> </QCard>
</QCard> </component>
</component> </template>
</template> </QTable>
</QTable> </template>
</template> </CrudModel>
</CrudModel> </div>
<QPageSticky v-if="create" :offset="[20, 20]" style="z-index: 2"> <QPageSticky v-if="create" :offset="[20, 20]" style="z-index: 2">
<QBtn @click="showForm = !showForm" color="primary" fab icon="add" /> <QBtn @click="showForm = !showForm" color="primary" fab icon="add" />
<QTooltip> <QTooltip>
@ -446,7 +458,7 @@ defineExpose({
</QPageSticky> </QPageSticky>
<QDialog v-model="showForm" transition-show="scale" transition-hide="scale"> <QDialog v-model="showForm" transition-show="scale" transition-hide="scale">
<FormModelPopup <FormModelPopup
v-bind="{ ...$attrs, ...create }" v-bind="create"
:model="$attrs['data-key'] + 'Create'" :model="$attrs['data-key'] + 'Create'"
@on-data-saved="(_, res) => create.onDataSaved(res)" @on-data-saved="(_, res) => create.onDataSaved(res)"
> >
@ -468,11 +480,11 @@ defineExpose({
</QDialog> </QDialog>
</template> </template>
<i18n> <i18n>
en: en:
status: Status status: Status
es: es:
status: Estados status: Estados
</i18n> </i18n>
<style lang="scss"> <style lang="scss">
.bg-chip-secondary { .bg-chip-secondary {

View File

@ -1,14 +1,3 @@
<template>
<span v-for="toComponent of componentArray" :key="toComponent.name">
<component
v-if="toComponent?.component"
:is="mix(toComponent).component"
v-bind="mix(toComponent).attrs"
v-on="mix(toComponent).event ?? {}"
v-model="model"
/>
</span>
</template>
<script setup> <script setup>
import { computed, defineModel } from 'vue'; import { computed, defineModel } from 'vue';
@ -53,3 +42,19 @@ function toValueAttrs(attrs) {
return typeof attrs == 'function' ? attrs($props.value) : attrs; return typeof attrs == 'function' ? attrs($props.value) : attrs;
} }
</script> </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

@ -115,13 +115,13 @@ const emit = defineEmits(['onFetch']);
</QBtn> </QBtn>
</RouterLink> </RouterLink>
<QBtn <QBtn
v-if="$slots.menu"
color="white" color="white"
dense dense
flat flat
icon="more_vert" icon="more_vert"
round round
size="md" size="md"
:class="{ invisible: !$slots.menu }"
> >
<QTooltip> <QTooltip>
{{ t('components.cardDescriptor.moreOptions') }} {{ t('components.cardDescriptor.moreOptions') }}

View File

@ -147,7 +147,7 @@ const tagsList = computed(() => {
const tagList = []; const tagList = [];
for (const key of Object.keys(params.value)) { for (const key of Object.keys(params.value)) {
const value = params.value[key]; const value = params.value[key];
if (!value || ($props.hiddenTags || []).includes(key)) continue; if (value == null || ($props.hiddenTags || []).includes(key)) continue;
tagList.push({ label: key, value }); tagList.push({ label: key, value });
} }
return tagList; return tagList;
@ -161,21 +161,14 @@ const customTags = computed(() =>
); );
async function remove(key) { async function remove(key) {
delete params.value[key];
delete params.value.filter?.where?.[key];
params.value[key] = undefined; params.value[key] = undefined;
await arrayData.applyFilter({ params: params.value }); search();
emit('remove', key); emit('remove', key);
} }
function formatValue(value) { function formatValue(value) {
if (typeof value === 'boolean') { if (typeof value === 'boolean') return value ? t('Yes') : t('No');
return value ? t('Yes') : t('No'); if (isNaN(value) && !isNaN(Date.parse(value))) return toDate(value);
}
if (isNaN(value) && !isNaN(Date.parse(value))) {
return toDate(value);
}
return `"${value}"`; return `"${value}"`;
} }
@ -236,7 +229,7 @@ function formatValue(value) {
<slot name="tags" :tag="chip" :format-fn="formatValue"> <slot name="tags" :tag="chip" :format-fn="formatValue">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
<strong>{{ chip.label }}:</strong> <strong>{{ chip.label }}:</strong>
<span>"{{ chip.value }}"</span> <span>"{{ formatValue(chip.value) }}"</span>
</div> </div>
</slot> </slot>
</VnFilterPanelChip> </VnFilterPanelChip>

View File

@ -70,6 +70,7 @@ const props = defineProps({
const emit = defineEmits(['onFetch', 'onPaginate', 'onChange']); const emit = defineEmits(['onFetch', 'onPaginate', 'onChange']);
const isLoading = ref(false); const isLoading = ref(false);
const mounted = ref(false);
const pagination = ref({ const pagination = ref({
sortBy: props.order, sortBy: props.order,
rowsPerPage: props.limit, rowsPerPage: props.limit,
@ -89,8 +90,9 @@ const arrayData = useArrayData(props.dataKey, {
}); });
const store = arrayData.store; const store = arrayData.store;
onMounted(() => { onMounted(async () => {
if (props.autoLoad) fetch(); if (props.autoLoad) await fetch();
mounted.value = true;
}); });
watch( watch(
@ -155,7 +157,7 @@ function endPagination() {
emit('onPaginate'); emit('onPaginate');
} }
async function onLoad(index, done) { 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); if (store.data.length === 0 || !props.url) return done(false);

View File

@ -1,4 +1,4 @@
import { onMounted, ref, computed, onUnmounted } from 'vue'; import { onMounted, ref, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import { useArrayDataStore } from 'stores/useArrayDataStore'; import { useArrayDataStore } from 'stores/useArrayDataStore';
@ -221,6 +221,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
const pushUrl = { path: to }; const pushUrl = { path: to };
if (to.endsWith('/list') || to.endsWith('/')) if (to.endsWith('/list') || to.endsWith('/'))
pushUrl.query = newUrl.query; pushUrl.query = newUrl.query;
destroy();
return router.push(pushUrl); return router.push(pushUrl);
} }
} }

View File

@ -396,6 +396,7 @@ entry:
type: Type type: Type
color: Color color: Color
id: ID id: ID
printedStickers: Printed stickers
notes: notes:
observationType: Observation type observationType: Observation type
descriptor: descriptor:
@ -420,6 +421,7 @@ entry:
buyingValue: Buying value buyingValue: Buying value
freightValue: Freight value freightValue: Freight value
comissionValue: Commission value comissionValue: Commission value
description: Description
packageValue: Package value packageValue: Package value
isIgnored: Is ignored isIgnored: Is ignored
price2: Grouping price2: Grouping
@ -582,6 +584,9 @@ claim:
created: Created created: Created
state: State state: State
pickup: Pick up pickup: Pick up
null: No
agency: Agency
delivery: Delivery
photo: photo:
fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}' fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}'
noData: 'There are no images/videos, click here or drag and drop the file' noData: 'There are no images/videos, click here or drag and drop the file'

View File

@ -394,6 +394,7 @@ entry:
type: Tipo type: Tipo
color: Color color: Color
id: ID id: ID
printedStickers: Etiquetas impresas
notes: notes:
observationType: Tipo de observación observationType: Tipo de observación
descriptor: descriptor:
@ -418,6 +419,7 @@ entry:
buyingValue: Coste buyingValue: Coste
freightValue: Porte freightValue: Porte
comissionValue: Comisión comissionValue: Comisión
description: Descripción
packageValue: Embalaje packageValue: Embalaje
isIgnored: Ignorado isIgnored: Ignorado
price2: Grouping price2: Grouping

View File

@ -1,94 +1,433 @@
<script setup> <script setup>
import { ref, computed, markRaw } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnLocation from 'src/components/common/VnLocation.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 CustomerSummary from './Card/CustomerSummary.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 { 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(); const { viewSummary } = useSummaryDialog();
const redirectToCreateView = (row) => {
function navigate(id) { router.push({
router.push({ path: `/customer/${id}` }); name: 'TicketList',
} query: {
params: JSON.stringify({
const redirectToCreateView = () => { clientFk: row.id,
router.push({ name: 'CustomerCreate' }); }),
},
});
}; };
function handleLocation(data, location) {
const { town, code, provinceFk, countryFk } = location ?? {};
data.postcode = code;
data.city = town;
data.provinceFk = provinceFk;
data.countryFk = countryFk;
}
</script> </script>
<template> <template>
<VnSearchbar <VnTable
:info="t('You can search by customer id or name')" ref="tableRef"
:label="t('Search customer')" data-key="CustomerExtendedList"
data-key="CustomerList" url="Clients/extendedListFilter"
/> :create="{
<RightMenu> urlCreate: 'Clients/createWithUser',
<template #right-panel> title: 'Create client',
<CustomerFilter data-key="CustomerList" /> onDataSaved: ({ id }) => tableRef.redirect(id),
</template> formInitialData: {
</RightMenu> active: true,
<QPage class="column items-center q-pa-md"> isEqualizated: false,
<div class="vn-card-list"> },
<VnPaginate }"
auto-load order="id DESC"
data-key="CustomerList" :columns="columns"
order="id DESC" default-mode="table"
url="/Clients/filter" redirect="customer"
> auto-load
<template #body="{ rows }"> >
<CardList <template #more-create-dialog="{ data }">
:id="row.id" <VnLocation
:key="row.id" :roles-allowed-to-create="['deliveryAssistant']"
:title="row.name" :options="postcodesOptions"
@click="navigate(row.id)" v-model="data.location"
v-for="row of rows" @update:model-value="(location) => handleLocation(data, location)"
> />
<template #list-items> <QInput v-model="data.userName" :label="t('Web user')" />
<VnLv :label="t('customer.list.email')" :value="row.email" /> <QInput :label="t('Email')" clearable type="email" v-model="data.email">
<VnLv :value="row.phone"> <template #append>
<template #label> <QIcon name="info" class="cursor-info">
{{ t('customer.list.phone') }} <QTooltip max-width="400px">{{
<VnLinkPhone :phone-number="row.phone" /> t('customer.basicData.youCanSaveMultipleEmails')
</template> }}</QTooltip>
</VnLv> </QIcon>
</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>
</template> </template>
</VnPaginate> </QInput>
</div> </template>
<QPageSticky :offset="[20, 20]"> </VnTable>
<QBtn @click="redirectToCreateView()" color="primary" fab icon="add" />
<QTooltip>
{{ t('New client') }}
</QTooltip>
</QPageSticky>
</QPage>
</template> </template>
<i18n> <i18n>
es: es:
Search customer: Buscar cliente Web user: Usuario Web
You can search by customer id or name: Puedes buscar por id o nombre del cliente
New client: Nuevo cliente
</i18n> </i18n>
<style lang="scss" scoped>
.col-content {
border-radius: 4px;
padding: 6px;
}
</style>

View File

@ -1,428 +0,0 @@
<script setup>
import { ref, computed, markRaw } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnTable from 'components/VnTable/VnTable.vue';
import VnLocation from 'src/components/common/VnLocation.vue';
import CustomerSummary from '../Card/CustomerSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import { toDate } from 'src/filters';
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,
after: {
component: markRaw(VnLinkPhone),
props: (prop) => ({
'phone-number': prop.phone,
}),
},
component: 'number',
columnField: {
component: null,
},
},
{
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',
created: 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();
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>
<VnTable
ref="tableRef"
data-key="CustomerExtendedList"
url="Clients/extendedListFilter"
url-create="Clients/createWithUser"
: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 }">
<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>
</QInput>
<QInput v-model="data.userName" :label="t('Web user')" />
<VnLocation
:roles-allowed-to-create="['deliveryAssistant']"
:options="postcodesOptions"
v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)"
>
</VnLocation>
</template>
</VnTable>
</template>
<style lang="scss" scoped>
.col-content {
border-radius: 4px;
padding: 6px;
}
</style>

View File

@ -82,6 +82,16 @@ const tableColumnComponents = computed(() => ({
}, },
event: getInputEvents, event: getInputEvents,
}, },
printedStickers: {
component: VnInput,
props: {
type: 'number',
min: 0,
class: 'input-number',
dense: true,
},
event: getInputEvents,
},
weight: { weight: {
component: VnInput, component: VnInput,
props: { props: {
@ -147,7 +157,7 @@ const entriesTableColumns = computed(() => {
return [ return [
{ {
label: t('entry.summary.item'), label: t('entry.summary.item'),
field: 'id', field: 'itemFk',
name: 'item', name: 'item',
align: 'left', align: 'left',
}, },
@ -169,6 +179,12 @@ const entriesTableColumns = computed(() => {
name: 'stickers', name: 'stickers',
align: 'left', align: 'left',
}, },
{
label: t('entry.buys.printedStickers'),
field: 'printedStickers',
name: 'printedStickers',
align: 'left',
},
{ {
label: t('entry.summary.weight'), label: t('entry.summary.weight'),
field: 'weight', field: 'weight',
@ -216,7 +232,6 @@ const entriesTableColumns = computed(() => {
}); });
const copyOriginalRowsData = (rows) => { const copyOriginalRowsData = (rows) => {
// el objetivo de esto es guardar los valores iniciales de todas las rows para evitar guardar cambios si la data no cambió al disparar los eventos
originalRowDataCopy.value = JSON.parse(JSON.stringify(rows)); originalRowDataCopy.value = JSON.parse(JSON.stringify(rows));
}; };
@ -386,19 +401,16 @@ const lockIconType = (groupingMode, mode) => {
</template> </template>
<ItemDescriptorProxy <ItemDescriptorProxy
v-if="col.name === 'item'" v-if="col.name === 'item'"
:id="props.row.id" :id="props.row.item.id"
/> />
</component> </component>
</QTd> </QTd>
</QTr> </QTr>
<QTr no-hover> <QTr no-hover class="full-width infoRow" style="column-span: all">
<QTd /> <QTd />
<QTd> <QTd cols>
<span>{{ props.row.item.itemType.code }}</span> <span>{{ props.row.item.itemType.code }}</span>
</QTd> </QTd>
<QTd>
<span>{{ props.row.item.id }}</span>
</QTd>
<QTd> <QTd>
<span>{{ props.row.item.size }}</span> <span>{{ props.row.item.size }}</span>
</QTd> </QTd>
@ -413,10 +425,6 @@ const lockIconType = (groupingMode, mode) => {
<FetchedTags :item="props.row.item" :max-length="5" /> <FetchedTags :item="props.row.item" :max-length="5" />
</QTd> </QTd>
</QTr> </QTr>
<!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys -->
<QTr v-if="props.rowIndex !== rows.length - 1" class="separation-row">
<QTd colspan="12" class="vn-table-separation-row" />
</QTr>
</template> </template>
<template #item="props"> <template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition"> <div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
@ -466,11 +474,13 @@ const lockIconType = (groupingMode, mode) => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.separation-row { .q-table--horizontal-separator tbody tr:nth-child(odd) > td {
background-color: var(--vn-section-color) !important; border-bottom-width: 0px;
border-top-width: 2px;
border-color: var(--vn-text-color);
} }
.grid-style-transition { .infoRow > td {
transition: transform 0.28s, background-color 0.28s; color: var(--vn-label-color);
} }
</style> </style>

View File

@ -135,14 +135,19 @@ watch;
<template #icons> <template #icons>
<QCardActions class="q-gutter-x-md"> <QCardActions class="q-gutter-x-md">
<QIcon <QIcon
v-if="currentEntry.isExcludedFromAvailable" v-if="currentEntry?.isExcludedFromAvailable"
name="vn:inventory" name="vn:inventory"
color="primary" color="primary"
size="xs" size="xs"
> >
<QTooltip>{{ t('Inventory entry') }}</QTooltip> <QTooltip>{{ t('Inventory entry') }}</QTooltip>
</QIcon> </QIcon>
<QIcon v-if="currentEntry.isRaid" name="vn:net" color="primary" size="xs"> <QIcon
v-if="currentEntry?.isRaid"
name="vn:net"
color="primary"
size="xs"
>
<QTooltip>{{ t('Virtual entry') }}</QTooltip> <QTooltip>{{ t('Virtual entry') }}</QTooltip>
</QIcon> </QIcon>
</QCardActions> </QCardActions>

View File

@ -167,7 +167,7 @@ const columns = computed(() => [
}, },
}, },
{ {
label: t('globals.description'), label: t('entry.latestBuys.description'),
field: 'description', field: 'description',
name: 'description', name: 'description',
align: 'left', align: 'left',
@ -653,6 +653,15 @@ onUnmounted(() => (stateStore.rightDrawer = false));
<EntryLatestBuysFilter data-key="EntryLatestBuys" /> <EntryLatestBuysFilter data-key="EntryLatestBuys" />
</template> </template>
</RightMenu> </RightMenu>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">
<QTable <QTable
:rows="rows" :rows="rows"

View File

@ -184,13 +184,6 @@ const suppliersOptions = ref([]);
@click="removeTag(index, params, searchFn)" @click="removeTag(index, params, searchFn)"
/> />
</QItem> </QItem>
<QItem class="q-mt-lg">
<QIcon
name="add_circle"
class="filter-icon"
@click="tagValues.push({})"
/>
</QItem>
</template> </template>
</ItemsFilterPanel> </ItemsFilterPanel>
</template> </template>

View File

@ -1,7 +1,8 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { onBeforeMount, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { Notify } from 'quasar'; import { Notify } from 'quasar';
import axios from 'axios';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { toDate } from 'filters/index'; import { toDate } from 'filters/index';
@ -29,7 +30,6 @@ const columns = computed(() => [
field: (row) => row.hasCmrDms, field: (row) => row.hasCmrDms,
align: 'center', align: 'center',
sortable: true, sortable: true,
headerStyle: 'padding-left: 35px',
}, },
{ {
name: 'ticketFk', name: 'ticketFk',
@ -62,7 +62,6 @@ const columns = computed(() => [
field: (row) => toDate(row.shipped), field: (row) => toDate(row.shipped),
align: 'center', align: 'center',
sortable: true, sortable: true,
headerStyle: 'padding-left: 33px',
}, },
{ {
name: 'warehouseFk', name: 'warehouseFk',
@ -77,6 +76,11 @@ const columns = computed(() => [
field: (row) => row.cmrFk, field: (row) => row.cmrFk,
}, },
]); ]);
onBeforeMount(async () => {
const { data } = await axios.get('Warehouses');
warehouses.value = data;
});
function getApiUrl() { function getApiUrl() {
return new URL(window.location).origin; return new URL(window.location).origin;
} }
@ -187,7 +191,6 @@ function downloadPdfs() {
</QPageSticky> </QPageSticky>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.list { .list {
padding-top: 15px; padding-top: 15px;
@ -204,4 +207,10 @@ function downloadPdfs() {
#false { #false {
background-color: $negative; background-color: $negative;
} }
:deep(.q-table th) {
max-width: 80px;
}
:deep(.q-table th:nth-child(3)) {
max-width: 100px;
}
</style> </style>

View File

@ -17,6 +17,10 @@ const $props = defineProps({
required: false, required: false,
default: null, default: null,
}, },
summary: {
type: Object,
default: null,
},
}); });
const route = useRoute(); const route = useRoute();
@ -106,6 +110,7 @@ const getEntryQueryParams = (supplier) => {
:filter="filter" :filter="filter"
@on-fetch="setData" @on-fetch="setData"
data-key="supplier" data-key="supplier"
:summary="$props.summary"
> >
<template #header-extra-action> <template #header-extra-action>
<QBtn <QBtn

View File

@ -1,5 +1,6 @@
<script setup> <script setup>
import SupplierDescriptor from './SupplierDescriptor.vue'; import SupplierDescriptor from './SupplierDescriptor.vue';
import SupplierSummary from './SupplierSummary.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -11,6 +12,6 @@ const $props = defineProps({
<template> <template>
<QPopupProxy> <QPopupProxy>
<SupplierDescriptor v-if="$props.id" :id="$props.id" /> <SupplierDescriptor v-if="$props.id" :id="$props.id" :summary="SupplierSummary" />
</QPopupProxy> </QPopupProxy>
</template> </template>

View File

@ -24,7 +24,7 @@ const agenciesOptions = ref([]);
<FormModel <FormModel
:url="`Travels/${route.params.id}`" :url="`Travels/${route.params.id}`"
:url-update="`Travels/${route.params.id}`" :url-update="`Travels/${route.params.id}`"
model="travel" model="Travel"
auto-load auto-load
> >
<template #form="{ data }"> <template #form="{ data }">

View File

@ -1,7 +1,40 @@
<script setup> <script setup>
import VnCard from 'components/common/VnCard.vue'; import VnCard from 'components/common/VnCard.vue';
import TravelDescriptor from './TravelDescriptor.vue'; import TravelDescriptor from './TravelDescriptor.vue';
const filter = {
fields: [
'id',
'ref',
'shipped',
'landed',
'totalEntries',
'warehouseInFk',
'warehouseOutFk',
'cargoSupplierFk',
'agencyModeFk',
],
include: [
{
relation: 'warehouseIn',
scope: {
fields: ['name'],
},
},
{
relation: 'warehouseOut',
scope: {
fields: ['name'],
},
},
],
};
</script> </script>
<template> <template>
<VnCard data-key="Travel" base-url="Travels" :descriptor="TravelDescriptor" /> <VnCard
data-key="Travel"
:filter="filter"
base-url="Travels"
:descriptor="TravelDescriptor"
/>
</template> </template>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -7,7 +7,6 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import TravelDescriptorMenuItems from './TravelDescriptorMenuItems.vue'; import TravelDescriptorMenuItems from './TravelDescriptorMenuItems.vue';
import useCardDescription from 'src/composables/useCardDescription';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
const $props = defineProps({ const $props = defineProps({
@ -52,23 +51,15 @@ const filter = {
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
const data = ref(useCardDescription());
const setData = (entity) => {
data.value = useCardDescription(entity.ref, entity.id);
};
</script> </script>
<template> <template>
<CardDescriptor <CardDescriptor
module="Travel" module="Travel"
:url="`Travels/${entityId}`" :url="`Travels/${entityId}`"
:title="data.title" title="ref"
:subtitle="data.subtitle"
:filter="filter" :filter="filter"
@on-fetch="setData" data-key="Travel"
data-key="travelData"
> >
<template #header-extra-action> <template #header-extra-action>
<QBtn <QBtn

View File

@ -32,10 +32,11 @@ const cloneTravel = () => {
redirectToCreateView(stringifiedTravelData); redirectToCreateView(stringifiedTravelData);
}; };
const cloneTravelWithEntries = () => { const cloneTravelWithEntries = async () => {
try { try {
axios.post(`Travels/${$props.travel.id}/cloneWithEntries`); const { data } = await axios.post(`Travels/${$props.travel.id}/cloneWithEntries`);
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
router.push({ name: 'TravelBasicData', params: { id: data.id } });
} catch (err) { } catch (err) {
console.err('Error cloning travel with entries'); console.err('Error cloning travel with entries');
} }

View File

@ -8,7 +8,6 @@ import VnLv from 'src/components/ui/VnLv.vue';
import VnTitle from 'src/components/common/VnTitle.vue'; import VnTitle from 'src/components/common/VnTitle.vue';
import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue'; import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
import TravelDescriptorMenuItems from './TravelDescriptorMenuItems.vue';
import { toDate, toCurrency } from 'src/filters'; import { toDate, toCurrency } from 'src/filters';
import axios from 'axios'; import axios from 'axios';
@ -222,6 +221,8 @@ async function setTravelData(travelData) {
console.error(`Error setting travel data`, err); console.error(`Error setting travel data`, err);
} }
} }
const getLink = (param) => `#/travel/${entityId.value}/${param}`;
</script> </script>
<template> <template>
@ -240,21 +241,15 @@ async function setTravelData(travelData) {
<template #header> <template #header>
<span>{{ travel.ref }} - {{ travel.id }}</span> <span>{{ travel.ref }} - {{ travel.id }}</span>
</template> </template>
<template #header-right>
<QBtn color="white" dense flat icon="more_vert" round size="md">
<QTooltip>
{{ t('components.cardDescriptor.moreOptions') }}
</QTooltip>
<QMenu>
<QList>
<TravelDescriptorMenuItems :travel="travel" />
</QList>
</QMenu>
</QBtn>
</template>
<template #body> <template #body>
<QCard class="vn-one"> <QCard class="vn-one">
<QCardSection class="q-pa-none">
<VnTitle
:url="getLink('basic-data')"
:text="t('travel.pageTitles.basicData')"
/>
</QCardSection>
<VnLv :label="t('globals.shipped')" :value="toDate(travel.shipped)" /> <VnLv :label="t('globals.shipped')" :value="toDate(travel.shipped)" />
<VnLv <VnLv
:label="t('globals.wareHouseOut')" :label="t('globals.wareHouseOut')"
@ -267,6 +262,12 @@ async function setTravelData(travelData) {
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<QCardSection class="q-pa-none">
<VnTitle
:url="getLink('basic-data')"
:text="t('travel.pageTitles.basicData')"
/>
</QCardSection>
<VnLv :label="t('globals.landed')" :value="toDate(travel.landed)" /> <VnLv :label="t('globals.landed')" :value="toDate(travel.landed)" />
<VnLv <VnLv
:label="t('globals.wareHouseIn')" :label="t('globals.wareHouseIn')"
@ -279,12 +280,18 @@ async function setTravelData(travelData) {
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<QCardSection class="q-pa-none">
<VnTitle
:url="getLink('basic-data')"
:text="t('travel.pageTitles.basicData')"
/>
</QCardSection>
<VnLv :label="t('globals.agency')" :value="travel.agency?.name" /> <VnLv :label="t('globals.agency')" :value="travel.agency?.name" />
<VnLv :label="t('globals.reference')" :value="travel.ref" /> <VnLv :label="t('globals.reference')" :value="travel.ref" />
<VnLv label="m³" :value="travel.m3" /> <VnLv label="m³" :value="travel.m3" />
<VnLv :label="t('globals.totalEntries')" :value="travel.totalEntries" /> <VnLv :label="t('globals.totalEntries')" :value="travel.totalEntries" />
</QCard> </QCard>
<QCard class="full-width" v-if="entriesTableRows.length > 0"> <QCard class="full-width">
<VnTitle :text="t('travel.summary.entries')" /> <VnTitle :text="t('travel.summary.entries')" />
<QTable <QTable
:rows="entriesTableRows" :rows="entriesTableRows"
@ -299,13 +306,15 @@ async function setTravelData(travelData) {
</QTh> </QTh>
</QTr> </QTr>
</template> </template>
<template #body-cell-isConfirmed="{ col, value }"> <template #body-cell-isConfirmed="{ col, row }">
<QTd> <QTd>
<QIcon <QCheckbox
v-if="col.name === 'isConfirmed'" v-if="col.name === 'isConfirmed'"
:name="value ? 'check' : 'close'" :label="t('travel.summary.received')"
:color="value ? 'positive' : 'negative'" :true-value="1"
size="sm" :false-value="0"
v-model="row[col.name]"
:disable="true"
/> />
</QTd> </QTd>
</template> </template>

View File

@ -53,6 +53,7 @@ const draggedRowIndex = ref(null);
const targetRowIndex = ref(null); const targetRowIndex = ref(null);
const entryRowIndex = ref(null); const entryRowIndex = ref(null);
const draggedEntry = ref(null); const draggedEntry = ref(null);
const travelKgPercentages = ref([]);
const tableColumnComponents = { const tableColumnComponents = {
id: { id: {
@ -88,6 +89,10 @@ const tableColumnComponents = {
component: 'span', component: 'span',
attrs: {}, attrs: {},
}, },
percentage: {
component: 'span',
attrs: {},
},
kg: { kg: {
component: VnInput, component: VnInput,
attrs: { dense: true, type: 'number', min: 0, class: 'input-number' }, attrs: { dense: true, type: 'number', min: 0, class: 'input-number' },
@ -179,6 +184,14 @@ const columns = computed(() => [
showValue: true, showValue: true,
sortable: true, sortable: true,
}, },
{
label: '%',
field: '',
name: 'percentage',
align: 'center',
showValue: false,
sortable: true,
},
{ {
label: t('kg'), label: t('kg'),
field: 'kg', field: 'kg',
@ -278,6 +291,8 @@ const saveFieldValue = async (val, field, index) => {
await axios.patch(`Travels/${id}`, params); await axios.patch(`Travels/${id}`, params);
// Actualizar la copia de los datos originales con el nuevo valor // Actualizar la copia de los datos originales con el nuevo valor
originalRowDataCopy.value[index][field] = val; originalRowDataCopy.value[index][field] = val;
await arrayData.fetch({ append: false });
} catch (err) { } catch (err) {
console.error('Error updating travel'); console.error('Error updating travel');
} }
@ -302,6 +317,11 @@ onMounted(async () => {
landedTo.value.setDate(landedTo.value.getDate() + 7); landedTo.value.setDate(landedTo.value.getDate() + 7);
landedTo.value.setHours(23, 59, 59, 59); landedTo.value.setHours(23, 59, 59, 59);
const { data } = await axios.get('TravelKgPercentages', {
params: { filter: JSON.stringify({ order: 'value DESC' }) },
});
travelKgPercentages.value = data;
await getData(); await getData();
}); });
@ -419,6 +439,11 @@ const handleDragScroll = (event) => {
stopScroll(); stopScroll();
} }
}; };
const getColor = (percentage) => {
for (const { value, className } of travelKgPercentages.value)
if (percentage > value) return className;
};
</script> </script>
<template> <template>
@ -460,7 +485,7 @@ const handleDragScroll = (event) => {
<template #body="props"> <template #body="props">
<QTr <QTr
:props="props" :props="props"
class="cursor-pointer bg-vn-primary-row" class="cursor-pointer bg-travel"
@click="navigateToTravelId(props.row.id)" @click="navigateToTravelId(props.row.id)"
@dragenter="handleDragEnter($event, props.rowIndex)" @dragenter="handleDragEnter($event, props.rowIndex)"
@dragover.prevent @dragover.prevent
@ -494,18 +519,32 @@ const handleDragScroll = (event) => {
: {} : {}
" "
> >
<template v-if="col.showValue"> <QChip
<span v-if="col.name === 'percentage'"
:class="[ :label="
'text-left', props.row.percentageKg
{ ? `${props.row.percentageKg}%`
'supplier-name': : '-'
col.name === 'cargoSupplierNickname', "
}, class="text-left q-py-xs q-px-sm"
]" :color="getColor(props.row.percentageKg)"
>{{ col.value }}</span />
> <span
</template> v-else-if="col.showValue"
:class="[
'text-left',
{
'supplier-name':
col.name === 'cargoSupplierNickname',
},
{
link: ['id', 'cargoSupplierNickname'].includes(
col.name
),
},
]"
v-text="col.value"
/>
<!-- Main Row Descriptors --> <!-- Main Row Descriptors -->
<TravelDescriptorProxy <TravelDescriptorProxy
v-if="col.name === 'id'" v-if="col.name === 'id'"
@ -539,11 +578,11 @@ const handleDragScroll = (event) => {
}" }"
> >
<QTd> <QTd>
<QBtn flat color="primary">{{ entry.id }} </QBtn> <QBtn flat class="link">{{ entry.id }} </QBtn>
<EntryDescriptorProxy :id="entry.id" /> <EntryDescriptorProxy :id="entry.id" />
</QTd> </QTd>
<QTd> <QTd>
<QBtn flat color="primary" dense>{{ entry.supplierName }}</QBtn> <QBtn flat class="link" dense>{{ entry.supplierName }}</QBtn>
<SupplierDescriptorProxy :id="entry.supplierFk" /> <SupplierDescriptorProxy :id="entry.supplierFk" />
</QTd> </QTd>
<QTd /> <QTd />
@ -556,6 +595,7 @@ const handleDragScroll = (event) => {
<QTd> <QTd>
<span>{{ entry.stickers }}</span> <span>{{ entry.stickers }}</span>
</QTd> </QTd>
<QTd />
<QTd></QTd> <QTd></QTd>
<QTd> <QTd>
<span>{{ entry.loadedkg }}</span> <span>{{ entry.loadedkg }}</span>
@ -574,10 +614,23 @@ const handleDragScroll = (event) => {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.q-chip {
color: var(--vn-text-color);
}
:deep(.q-table) { :deep(.q-table) {
border-collapse: collapse; border-collapse: collapse;
} }
.q-td :deep(input) {
font-weight: bold;
}
.bg-travel {
background-color: var(--vn-page-color);
border-bottom: 2px solid $primary;
}
.dashed-border { .dashed-border {
&.--left { &.--left {
border-left: 1px dashed #ccc; border-left: 1px dashed #ccc;

View File

@ -15,29 +15,19 @@ const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const newTravelForm = reactive({
ref: null,
agencyModeFk: null,
shipped: null,
landed: null,
warehouseOutFk: null,
warehouseInFk: null,
});
const agenciesOptions = ref([]); const agenciesOptions = ref([]);
const warehousesOptions = ref([]); const warehousesOptions = ref([]);
const viewAction = ref(); const viewAction = ref();
const newTravelForm = ref({});
onBeforeMount(() => { onBeforeMount(() => {
// Esto nos permite decirle a FormModel si queremos observar los cambios o no
// Ya que si queremos clonar queremos que nos permita guardar inmediatamente sin realizar cambios en el form
viewAction.value = route.query.travelData ? 'clone' : 'create'; viewAction.value = route.query.travelData ? 'clone' : 'create';
if (route.query.travelData) { if (route.query.travelData) {
const travelData = JSON.parse(route.query.travelData); const travelData = JSON.parse(route.query.travelData);
for (let key in newTravelForm) {
newTravelForm[key] = travelData[key]; newTravelForm.value = { ...newTravelForm.value, ...travelData };
} delete newTravelForm.value.id;
} }
}); });
@ -60,8 +50,8 @@ const redirectToTravelBasicData = (_, { id }) => {
<QPage> <QPage>
<VnSubToolbar /> <VnSubToolbar />
<FormModel <FormModel
url-update="Travels" url-create="Travels"
model="travel" model="travelCreate"
:form-initial-data="newTravelForm" :form-initial-data="newTravelForm"
:observe-form-changes="viewAction === 'create'" :observe-form-changes="viewAction === 'create'"
@on-data-saved="redirectToTravelBasicData" @on-data-saved="redirectToTravelBasicData"

View File

@ -40,6 +40,7 @@ onMounted(() => {
<template> <template>
<FormModel <FormModel
model="createDepartmentChild"
:form-initial-data="departmentChildData" :form-initial-data="departmentChildData"
:observe-form-changes="false" :observe-form-changes="false"
:default-actions="false" :default-actions="false"

View File

@ -6,12 +6,11 @@ import { useQuasar } from 'quasar';
import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue';
import CreateDepartmentChild from './CreateDepartmentChild.vue'; import CreateDepartmentChild from './CreateDepartmentChild.vue';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify();
const state = useState(); const state = useState();
const router = useRouter(); const router = useRouter();
@ -62,25 +61,28 @@ const removeNode = (node) => {
const { id, parentFk } = node; const { id, parentFk } = node;
quasar quasar
.dialog({ .dialog({
title: t('Are you sure you want to delete it?'), component: VnConfirm,
message: t('Delete department'), componentProps: {
ok: { title: t('Are you sure you want to delete it?'),
push: true, message: t('Delete department'),
color: 'primary', promise: () => remove(id),
}, },
cancel: true,
}) })
.onOk(async () => { .onOk(async () => await fetchNodeLeaves(parentFk));
try {
await axios.post(`/Departments/${id}/removeChild`, id);
notify(t('department.departmentRemoved'), 'positive');
await fetchNodeLeaves(parentFk);
} catch (err) {
console.error('Error removing department');
}
});
}; };
async function remove(id) {
try {
await axios.post(`/Departments/${id}/removeChild`, { id });
quasar.notify({
message: t('department.departmentRemoved'),
type: 'positive',
});
} catch (err) {
console.error('Error removing department');
}
}
const showCreateNodeForm = (nodeId) => { const showCreateNodeForm = (nodeId) => {
showCreateNodeFormVal.value = true; showCreateNodeFormVal.value = true;
creationNodeSelectedId.value = nodeId; creationNodeSelectedId.value = nodeId;

View File

@ -14,7 +14,6 @@ export default {
main: [ main: [
'CustomerList', 'CustomerList',
'CustomerPayments', 'CustomerPayments',
'CustomerExtendedList',
'CustomerNotifications', 'CustomerNotifications',
'CustomerDefaulter', 'CustomerDefaulter',
], ],
@ -70,18 +69,6 @@ export default {
component: () => component: () =>
import('src/pages/Customer/Payments/CustomerPayments.vue'), import('src/pages/Customer/Payments/CustomerPayments.vue'),
}, },
{
path: 'extendedList',
name: 'CustomerExtendedList',
meta: {
title: 'extendedList',
icon: 'vn:client',
},
component: () =>
import(
'src/pages/Customer/ExtendedList/CustomerExtendedList.vue'
),
},
{ {
path: 'notifications', path: 'notifications',
name: 'CustomerNotifications', name: 'CustomerNotifications',

View File

@ -2,8 +2,7 @@ const locationOptions = '[role="listbox"] > div.q-virtual-scroll__content > .q-i
describe('VnLocation', () => { describe('VnLocation', () => {
const dialogInputs = '.q-dialog label input'; const dialogInputs = '.q-dialog label input';
describe('Worker Create', () => { describe('Worker Create', () => {
const inputLocation = const inputLocation = '.q-form input[aria-label="Location"]';
'.q-form .q-card > :nth-child(3) > .q-field > .q-field__inner > .q-field__control > .q-field__control-container';
beforeEach(() => { beforeEach(() => {
cy.viewport(1280, 720); cy.viewport(1280, 720);
cy.login('developer'); cy.login('developer');
@ -25,9 +24,6 @@ describe('VnLocation', () => {
cy.get(inputLocation).clear(); cy.get(inputLocation).clear();
cy.get(inputLocation).type('ecuador'); cy.get(inputLocation).type('ecuador');
cy.get(locationOptions).should('have.length.at.least', 1); cy.get(locationOptions).should('have.length.at.least', 1);
cy.get(
'.q-form .q-card > :nth-child(3) > .q-field > .q-field__inner > .q-field__control > :nth-child(3) > .q-icon'
).click();
}); });
}); });
describe('Fiscal-data', () => { describe('Fiscal-data', () => {
@ -38,9 +34,7 @@ describe('VnLocation', () => {
cy.waitForElement('.q-form'); cy.waitForElement('.q-form');
}); });
it('Create postCode', function () { it('Create postCode', function () {
cy.get( cy.get('.q-form > .q-card > .vn-row:nth-child(6) .--add-icon').click();
':nth-child(6) > .q-field > .q-field__inner > .q-field__control > :nth-child(2) > .q-icon'
).click();
cy.get('.q-card > h1').should('have.text', 'New postcode'); cy.get('.q-card > h1').should('have.text', 'New postcode');
cy.get(dialogInputs).eq(0).clear('12'); cy.get(dialogInputs).eq(0).clear('12');
cy.get(dialogInputs).eq(0).type('1234453'); cy.get(dialogInputs).eq(0).type('1234453');

View File

@ -1,31 +1,81 @@
import { describe, expect, it, beforeAll } from 'vitest'; import { describe, expect, it, beforeAll, afterEach, vi } from 'vitest';
import { axios } from 'app/test/vitest/helper'; import { axios, flushPromises } from 'app/test/vitest/helper';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useRouter } from 'vue-router';
describe('useArrayData', () => { describe('useArrayData', () => {
let arrayData;
beforeAll(() => { beforeAll(() => {
axios.get.mockResolvedValue({ data: [] }); vi.spyOn(useRouter(), 'replace');
arrayData = useArrayData('InvoiceIn', { url: 'invoice-in/list' }); vi.spyOn(useRouter(), 'push');
Object.defineProperty(window.location, 'href', { });
writable: true,
value: 'localhost:9000/invoice-in/list', afterEach(() => {
vi.clearAllMocks();
});
it('should fetch and repalce url with new params', async () => {
vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [] });
const arrayData = useArrayData('ArrayData', { url: 'mockUrl' });
const filter = '{"order":"","limit":10,"skip":0}';
const params = { supplierFk: 2 };
arrayData.store.userParams = params;
arrayData.fetch({});
await flushPromises();
const routerReplace = useRouter().replace.mock.calls[0][0];
expect(axios.get.mock.calls[0][1].params).toEqual({
filter,
supplierFk: 2,
}); });
expect(routerReplace.path).toEqual('mockSection/list');
// Mock the window.history.pushState method within useArrayData expect(JSON.parse(routerReplace.query.params)).toEqual(
window.history.pushState = (data, title, url) => (window.location.href = url); expect.objectContaining(params)
);
// Mock the URL constructor within useArrayData
global.URL = class URL {
constructor(url) {
this.hash = url.split('localhost:9000/')[1];
}
};
}); });
it('should add the params to the url', async () => { it('Should get data and send new URL without keeping parameters, if there is only one record', async () => {
arrayData.store.userParams = { supplierFk: 2 }; vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [{ id: 1 }] });
arrayData.updateStateParams();
expect(window.location.href).contain('params=%7B%22supplierFk%22%3A2%7D'); const arrayData = useArrayData('ArrayData', { url: 'mockUrl', navigate: {} });
const filter = '{"order":"","limit":10,"skip":0}';
const params = { supplierFk: 2 };
arrayData.store.userParams = params;
arrayData.fetch({});
await flushPromises();
const routerPush = useRouter().push.mock.calls[0][0];
expect(axios.get.mock.calls[0][1].params).toEqual({
filter,
supplierFk: 2,
});
expect(routerPush.path).toEqual('mockName/1');
expect(routerPush.query).toBeUndefined();
}); });
// it('Should get data and send new URL keeping parameters, if you have more than one record', async () => {
// vi.spyOn(axios, 'get').mockReturnValueOnce({ data: [{ id: 1 }, { id: 2 }] });
// const arrayData = useArrayData('ArrayData', { url: 'mockUrl', navigate: {} });
// const filter = '{"order":"","limit":10,"skip":0}';
// const params = { supplierFk: 2 };
// arrayData.store.userParams = params;
// arrayData.fetch({});
// await flushPromises();
// const routerPush = useRouter().push.mock.calls[0][0];
// console.log('routerPush: ', routerPush);
// expect(axios.get.mock.calls[0][1].params).toEqual({
// filter,
// supplierFk: 2,
// });
// expect(routerPush.path).toEqual('mockName/1');
// expect(routerPush.query).toBeUndefined();
// });
}); });

View File

@ -15,16 +15,19 @@ installQuasarPlugin({
}); });
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false }); const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false });
const mockPush = vi.fn(); const mockPush = vi.fn();
const mockReplace = vi.fn();
vi.mock('vue-router', () => ({ vi.mock('vue-router', () => ({
useRouter: () => ({ useRouter: () => ({
push: mockPush, push: mockPush,
replace: mockReplace,
currentRoute: { currentRoute: {
value: { value: {
params: { params: {
id: 1, id: 1,
}, },
meta: { moduleName: 'mockName' }, meta: { moduleName: 'mockName' },
matched: [{ path: 'mockName/list' }],
}, },
}, },
}), }),
@ -33,6 +36,7 @@ vi.mock('vue-router', () => ({
query: {}, query: {},
params: {}, params: {},
meta: { moduleName: 'mockName' }, meta: { moduleName: 'mockName' },
path: 'mockSection/list',
}), }),
})); }));