#7283 #7831 itemMigration #553

Merged
carlossa merged 77 commits from 7283-itemMigration into dev 2024-10-25 07:09:13 +00:00
142 changed files with 4226 additions and 2147 deletions
Showing only changes of commit 06a1b3b409 - Show all commits

View File

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

View File

@ -2,9 +2,15 @@ import { boot } from 'quasar/wrappers';
import qFormMixin from './qformMixin';
import mainShortcutMixin from './mainShortcutMixin';
import keyShortcut from './keyShortcut';
import useNotify from 'src/composables/useNotify.js';
const { notify } = useNotify();
export default boot(({ app }) => {
app.mixin(qFormMixin);
app.mixin(mainShortcutMixin);
app.directive('shortcut', keyShortcut);
app.config.errorHandler = function (err) {
console.error(err);
notify('globals.error', 'negative', 'error');
};
});

View File

@ -21,7 +21,7 @@ const customer = computed(() => state.get('customer'));
const bankEntityFormData = reactive({
name: null,
bic: null,
countryFk: customer.value.countryFk,
countryFk: customer.value?.countryFk,
id: null,
});

View File

@ -1,5 +1,5 @@
<script setup>
import { reactive, ref } from 'vue';
import { reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
@ -22,9 +22,11 @@ const postcodeFormData = reactive({
townFk: null,
});
const townsFetchDataRef = ref(null);
const provincesFetchDataRef = ref(null);
const countriesOptions = ref([]);
const provincesOptions = ref([]);
const townsOptions = ref([]);
const town = ref({});
function onDataSaved(formData) {
@ -67,20 +69,62 @@ async function setProvince(id, data) {
data.countryFk = newProvince.countryFk;
}
watch(
() => [postcodeFormData.countryFk],
async (newCountryFk) => {
if (newCountryFk) {
await provincesFetchDataRef.value.fetch({
where: {
countryFk: newCountryFk[0],
},
});
await townsFetchDataRef.value.fetch({
where: {
provinceFk: {
inq: provincesOptions.value.map(({ id }) => id),
},
},
});
}
}
);
watch(
() => postcodeFormData.provinceFk,
async (newProvinceFk) => {
if (newProvinceFk) {
await townsFetchDataRef.value.fetch({
where: { provinceFk: newProvinceFk[0] },
});
}
}
);
async function handleProvinces(data) {
provincesOptions.value = data;
}
async function handleTowns(data) {
townsOptions.value = data;
}
async function handleCountries(data) {
countriesOptions.value = data;
}
</script>
<template>
<FetchData
ref="provincesFetchDataRef"
@on-fetch="(data) => (provincesOptions = data)"
@on-fetch="handleProvinces"
auto-load
url="Provinces/location"
/>
<FetchData
@on-fetch="(data) => (countriesOptions = data)"
ref="townsFetchDataRef"
@on-fetch="handleTowns"
auto-load
url="Countries"
url="Towns/location"
/>
<FetchData @on-fetch="handleCountries" auto-load url="Countries" />
<FormModelPopup
url-create="postcodes"
model="postcode"
@ -99,9 +143,9 @@ async function setProvince(id, data) {
/>
<VnSelectDialog
:label="t('City')"
url="Towns/location"
@update:model-value="(value) => setTown(value, data)"
v-model="data.townFk"
:options="townsOptions"
option-label="name"
option-value="id"
:rules="validate('postcode.city')"
@ -132,6 +176,7 @@ async function setProvince(id, data) {
</VnRow>
<VnRow>
<VnSelectProvince
:country-fk="postcodeFormData.countryFk"
@update:model-value="(value) => setProvince(value, data)"
v-model="data.provinceFk"
/>

View File

@ -50,7 +50,7 @@ const onDataSaved = (dataSaved) => {
model="thermograph"
:title="t('New thermograph')"
:form-initial-data="thermographFormData"
@on-data-saved="onDataSaved($event)"
@on-data-saved="(_, response) => onDataSaved(response)"
>
<template #form-inputs="{ data, validate }">
<VnRow>

View File

@ -234,6 +234,8 @@ async function remove(data) {
newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
fetch(newData);
});
} else {
reset();
}
emit('update:selected', []);
}

View File

@ -297,11 +297,12 @@ const removeTag = (index, params, search) => {
/>
</QItem>
<QItem class="q-mt-lg">
<QIcon
name="add_circle"
<QBtn
icon="add_circle"
shortcut="+"
flat
class="fill-icon-on-hover q-px-xs"
color="primary"
size="sm"
@click="tagValues.push({})"
/>
</QItem>

View File

@ -44,7 +44,6 @@ const itemComputed = computed(() => {
</QItemSection>
</QItem>
</template>
<style lang="scss" scoped>
.q-item {
min-height: 5vh;

View File

@ -13,12 +13,14 @@ import FetchData from 'components/FetchData.vue';
import { useClipboard } from 'src/composables/useClipboard';
import { useRole } from 'src/composables/useRole';
import VnAvatar from './ui/VnAvatar.vue';
import useNotify from 'src/composables/useNotify';
const state = useState();
const session = useSession();
const router = useRouter();
const { t, locale } = useI18n();
const { copyText } = useClipboard();
const { notify } = useNotify();
const userLocale = computed({
get() {
@ -53,6 +55,7 @@ const user = state.getUser();
const warehousesData = ref();
const companiesData = ref();
const accountBankData = ref();
const isEmployee = computed(() => useRole().isEmployee());
onMounted(async () => {
updatePreferences();
@ -70,18 +73,28 @@ function updatePreferences() {
async function saveDarkMode(value) {
const query = `/UserConfigs/${user.value.id}`;
await axios.patch(query, {
darkMode: value,
});
user.value.darkMode = value;
try {
await axios.patch(query, {
darkMode: value,
});
user.value.darkMode = value;
onDataSaved();
} catch (error) {
onDataError();
}
}
async function saveLanguage(value) {
const query = `/VnUsers/${user.value.id}`;
await axios.patch(query, {
lang: value,
});
user.value.lang = value;
try {
await axios.patch(query, {
lang: value,
});
user.value.lang = value;
onDataSaved();
} catch (error) {
onDataError();
}
}
function logout() {
@ -97,11 +110,23 @@ function localUserData() {
state.setUser(user.value);
}
function saveUserData(param, value) {
axios.post('UserConfigs/setUserConfig', { [param]: value });
localUserData();
async function saveUserData(param, value) {
try {
await axios.post('UserConfigs/setUserConfig', { [param]: value });
localUserData();
onDataSaved();
} catch (error) {
onDataError();
}
}
const isEmployee = computed(() => useRole().isEmployee());
const onDataSaved = () => {
notify('globals.dataSaved', 'positive');
};
const onDataError = () => {
notify('errors.updateUserConfig', 'negative');
};
</script>
<template>

View File

@ -10,7 +10,12 @@ import CreateNewProvinceForm from './CreateNewProvinceForm.vue';
const emit = defineEmits(['onProvinceCreated']);
const provinceFk = defineModel({ type: Number });
watch(provinceFk, async () => await provincesFetchDataRef.value.fetch());
const $props = defineProps({
countryFk: {
type: Number,
default: null,
},
});
const { validate } = useValidator();
const { t } = useI18n();
@ -18,17 +23,30 @@ const provincesOptions = ref();
const provincesFetchDataRef = ref();
async function onProvinceCreated(_, data) {
await provincesFetchDataRef.value.fetch();
await provincesFetchDataRef.value.fetch({ where: { countryFk: $props.countryFk } });
provinceFk.value = data.id;
emit('onProvinceCreated', data);
}
watch(
() => $props.countryFk,
async (newProvinceFk) => {
if (newProvinceFk) {
await provincesFetchDataRef.value.fetch({
where: { countryFk: newProvinceFk },
});
}
}
);
async function handleProvinces(data) {
provincesOptions.value = data;
}
</script>
<template>
<FetchData
ref="provincesFetchDataRef"
:filter="{ include: { relation: 'country' } }"
@on-fetch="(data) => (provincesOptions = data)"
@on-fetch="handleProvinces"
auto-load
url="Provinces"
/>

View File

@ -10,8 +10,6 @@ import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue';
defineExpose({ addFilter });
const $props = defineProps({
column: {
type: Object,
@ -30,6 +28,9 @@ const $props = defineProps({
default: 'params',
},
});
defineExpose({ addFilter, props: $props });
const model = defineModel(undefined, { required: true });
const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
const columnFilter = computed(() => $props.column?.columnFilter);
@ -115,11 +116,11 @@ const components = {
rawSelect: selectComponent,
};
async function addFilter(value) {
async function addFilter(value, name) {
value ??= undefined;
if (value && typeof value === 'object') value = model.value;
value = value === '' ? undefined : value;
let field = columnFilter.value?.name ?? $props.column.name;
let field = columnFilter.value?.name ?? $props.column.name ?? name;
if (columnFilter.value?.inWhere) {
if (columnFilter.value.alias) field = columnFilter.value.alias + '.' + field;

View File

@ -10,7 +10,7 @@ import FormModelPopup from 'components/FormModelPopup.vue';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue';
import VnTableFilter from 'components/VnTable/VnFilter.vue';
import VnFilter from 'components/VnTable/VnFilter.vue';
import VnTableChip from 'components/VnTable/VnChip.vue';
import VnVisibleColumn from 'src/components/VnTable/VnVisibleColumn.vue';
import VnLv from 'components/ui/VnLv.vue';
@ -53,6 +53,10 @@ const $props = defineProps({
type: Boolean,
default: true,
},
bottom: {
type: Object,
default: null,
},
cardClass: {
type: String,
default: 'flex-one',
@ -69,10 +73,6 @@ const $props = defineProps({
type: Boolean,
default: false,
},
disableInfiniteScroll: {
type: Boolean,
default: false,
},
hasSubToolbar: {
type: Boolean,
default: null,
@ -101,10 +101,18 @@ const $props = defineProps({
type: String,
default: '90vh',
},
chipLocale: {
type: String,
default: null,
},
footer: {
type: Boolean,
default: false,
},
disabledAttr: {
type: Boolean,
default: false,
},
});
const { t } = useI18n();
const stateStore = useStateStore();
@ -125,6 +133,7 @@ const showForm = ref(false);
const splittedColumns = ref({ columns: [] });
const columnsVisibilitySkipped = ref();
const createForm = ref();
const tableFilterRef = ref([]);
const tableModes = [
{
@ -150,7 +159,7 @@ onMounted(() => {
quasar.platform.is.mobile && !$props.disableOption?.card
? CARD_MODE
: $props.defaultMode;
stateStore.rightDrawer = true;
stateStore.rightDrawer = quasar.screen.gt.xs;
columnsVisibilitySkipped.value = [
...splittedColumns.value.columns
.filter((c) => c.visible == false)
@ -227,7 +236,7 @@ function splitColumns(columns) {
if (col.cardVisible) splittedColumns.value.cardVisible.push(col);
if ($props.isEditable && col.disable == null) col.disable = false;
if ($props.useModel && col.columnFilter != false)
col.columnFilter = { ...col.columnFilter, inWhere: true };
col.columnFilter = { inWhere: true, ...col.columnFilter };
splittedColumns.value.columns.push(col);
}
// Status column
@ -305,6 +314,7 @@ defineExpose({
redirect: redirectFn,
selected,
CrudModelRef,
params,
});
function handleOnDataSaved(_) {
@ -328,6 +338,13 @@ function handleOnDataSaved(_) {
:search-url="searchUrl"
:redirect="!!redirect"
@set-user-params="setUserParams"
:disable-submit-event="true"
@remove="
(key) =>
tableFilterRef
.find((f) => f.props?.column.name == key)
?.addFilter()
"
>
<template #body>
<div
@ -337,7 +354,8 @@ function handleOnDataSaved(_) {
)"
:key="col.id"
>
<VnTableFilter
<VnFilter
ref="tableFilterRef"
:column="col"
:data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
@ -361,10 +379,15 @@ function handleOnDataSaved(_) {
:columns="splittedColumns.columns"
/>
</template>
<template #tags="{ tag, formatFn }" v-if="chipLocale">
<div class="q-gutter-x-xs">
<strong>{{ t(`${chipLocale}.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
</VnFilterPanel>
</QScrollArea>
</QDrawer>
<!-- class in div to fix warn-->
<CrudModel
v-bind="$attrs"
:class="$attrs['class'] ?? 'q-px-md'"
@ -372,9 +395,7 @@ function handleOnDataSaved(_) {
ref="CrudModelRef"
@on-fetch="(...args) => emit('onFetch', ...args)"
:search-url="searchUrl"
:disable-infinite-scroll="
$attrs['disableInfiniteScroll'] ? isTableMode : !disableInfiniteScroll
"
:disable-infinite-scroll="isTableMode"
@save-changes="reload"
:has-sub-toolbar="$props.hasSubToolbar ?? isEditable"
:auto-load="hasParams || $attrs['auto-load']"
@ -394,7 +415,7 @@ function handleOnDataSaved(_) {
card-container-class="grid-three"
flat
:style="isTableMode && `max-height: ${tableHeight}`"
:virtual-scroll="!isTableMode"
:virtual-scroll="isTableMode"
@virtual-scroll="
(event) =>
event.index > rows.length - 2 &&
@ -424,7 +445,7 @@ function handleOnDataSaved(_) {
<QBtn
v-if="$props.rightSearch"
icon="filter_alt"
class="bg-vn-section-color q-ml-md"
class="bg-vn-section-color q-ml-sm"
dense
@click="stateStore.toggleRightDrawer()"
/>
@ -446,7 +467,7 @@ function handleOnDataSaved(_) {
:search-url="searchUrl"
/>
</div>
<VnTableFilter
<VnFilter
v-if="$props.columnSearch"
:column="col"
:show-title="true"
@ -511,8 +532,12 @@ function handleOnDataSaved(_) {
:key="index"
:title="btn.title"
:icon="btn.icon"
class="q-px-sm text-primary-light"
class="q-pa-xs"
flat
dense
:class="
btn.isPrimary ? 'text-primary-light' : 'color-vn-text '
"
:style="`visibility: ${
(btn.show && btn.show(row)) ?? true ? 'visible' : 'hidden'
}`"
@ -520,6 +545,29 @@ function handleOnDataSaved(_) {
/>
</QTd>
</template>
<template #bottom v-if="bottom">
<slot name="bottom-table">
<QBtn
@click="
() =>
createAsDialog
? (showForm = !showForm)
: handleOnDataSaved(create)
"
class="cursor-pointer fill-icon"
color="primary"
icon="add_circle"
size="md"
round
flat
shortcut="+"
:disabled="!disabledAttr"
/>
<QTooltip>
{{ createForm.title }}
</QTooltip>
</slot>
</template>
<template #item="{ row, colsMap }">
<component
:is="$props.redirect ? 'router-link' : 'span'"
@ -636,25 +684,15 @@ function handleOnDataSaved(_) {
</QCard>
</component>
</template>
<template #bottom-row="{ cols }" v-if="footer">
<QTr v-if="rows.length" class="bg-header" style="height: 30px">
<template #bottom-row="{ cols }" v-if="$props.footer">
<QTr v-if="rows.length" style="height: 30px">
<QTh
v-for="col of cols.filter((cols) => cols.visible ?? true)"
:key="col?.id"
class="text-center"
:class="getColAlign(col)"
>
<div
v-if="col?.summation"
:class="`text-${col?.align ?? 'left'}`"
class="text-bold q-pa-sm"
>
{{
rows.reduce(
(sum, currentRow) => sum + currentRow[col.name],
0
)
}}
</div>
<slot :name="`column-footer-${col.name}`" />
</QTh>
</QTr>
</template>
@ -733,16 +771,6 @@ es:
color: var(--vn-text-color);
}
.q-table--dark .q-table__bottom,
.q-table--dark thead,
.q-table--dark tr {
border-color: var(--vn-section-color);
}
.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));
@ -771,10 +799,16 @@ es:
}
}
.q-table th {
padding: 0;
}
.q-table {
th {
padding: 0;
}
&__top {
padding: 12px 0px;
top: 0;
}
}
.vnTable {
thead tr th {
position: sticky;
@ -810,6 +844,15 @@ es:
background-color: var(--vn-section-color);
z-index: 1;
}
table tbody th {
position: relative;
}
tbody:nth-last-child(1) {
@extend .bg-header;
position: sticky;
z-index: 2;
bottom: 0;
}
}
.vn-label-value {
@ -855,4 +898,12 @@ es:
cursor: text;
user-select: all;
}
.q-table__container {
background-color: transparent;
}
.q-table__middle.q-virtual-scroll.q-virtual-scroll--vertical.scroll {
background-color: var(--vn-section-color);
}
</style>

View File

@ -135,7 +135,7 @@ onMounted(async () => {
});
</script>
<template>
<QBtn icon="vn:visible_columns" class="bg-vn-section-color q-mr-md q-px-sm" dense>
<QBtn icon="vn:visible_columns" class="bg-vn-section-color q-mr-sm q-px-sm" dense>
<QPopupProxy ref="popupProxyRef">
<QCard class="column q-pa-md">
<QIcon name="info" size="sm" class="info-icon">

View File

@ -400,7 +400,14 @@ defineExpose({
/>
</QDialog>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="showFormDialog()" class="fill-icon">
<QBtn
fab
color="primary"
icon="add"
shortcut="+"
@click="showFormDialog()"
class="fill-icon"
>
<QTooltip>
{{ t('Upload file') }}
</QTooltip>

View File

@ -103,6 +103,7 @@ const mixinRules = [
@click="
() => {
value = null;
vnInputRef.focus();
emit('remove');
}
"
@ -129,4 +130,30 @@ const mixinRules = [
.q-field__append {
padding-inline: 0;
}
.q-field__append.q-field__marginal.row.no-wrap.items-center.row {
height: 20px;
}
.q-field--outlined .q-field__append.q-field__marginal.row.no-wrap.items-center.row {
height: auto;
}
.q-field__control {
height: unset;
}
.q-field__control.relative-position.row.no-wrap
> .q-field__control-container
> input.q-field__native
~ div.q-field__label {
height: 41px;
}
.q-field--labeled {
.q-field__native,
.q-field__prefix,
.q-field__suffix,
.q-field__input {
padding-bottom: 0;
min-height: 15px;
}
}
</style>

View File

@ -20,6 +20,7 @@ const { validations } = useValidator();
const { t } = useI18n();
const requiredFieldRule = (val) => validations().required($attrs.required, val);
const vnInputDateRef = ref(null);
const dateFormat = 'DD/MM/YYYY';
const isPopupOpen = ref();
@ -91,6 +92,7 @@ const styleAttrs = computed(() => {
<template>
<div @mouseover="hover = true" @mouseleave="hover = false">
<QInput
ref="vnInputDateRef"
v-model="formattedDate"
class="vn-input-date"
:mask="mask"
@ -113,6 +115,7 @@ const styleAttrs = computed(() => {
!$attrs.disable
"
@click="
vnInputDateRef.focus();
model = null;
isPopupOpen = false;
"

View File

@ -16,6 +16,7 @@ const props = defineProps({
default: false,
},
});
const vnInputTimeRef = ref(null);
const initialDate = ref(model.value ?? Date.vnNew());
const { t } = useI18n();
const requiredFieldRule = (val) => validations().required($attrs.required, val);
@ -69,6 +70,7 @@ function dateToTime(newDate) {
<template>
<div @mouseover="hover = true" @mouseleave="hover = false">
<QInput
ref="vnInputTimeRef"
class="vn-input-time"
mask="##:##"
placeholder="--:--"
@ -92,6 +94,7 @@ function dateToTime(newDate) {
!$attrs.disable
"
@click="
vnInputTimeRef.focus();
model = null;
isPopupOpen = false;
"

View File

@ -12,14 +12,43 @@ const props = defineProps({
default: null,
},
});
const formatLocation = (obj, properties) => {
const parts = properties.map((prop) => {
if (typeof prop === 'string') {
return obj[prop];
} else if (typeof prop === 'function') {
return prop(obj);
}
return null;
});
const filteredParts = parts.filter(
(part) => part !== null && part !== undefined && part !== ''
);
return filteredParts.join(', ');
};
const locationProperties = [
'postcode',
(obj) =>
obj.city
? `${obj.city}${obj.province?.name ? `(${obj.province.name})` : ''}`
: null,
(obj) => obj.country?.name,
];
const modelValue = ref(
props.location
? `${props.location?.postcode} - ${props.location?.city}(${props.location?.province?.name}), ${props.location?.country?.name}`
: null
props.location ? formatLocation(props.location, locationProperties) : null
);
function showLabel(data) {
return `${data.code} - ${data.town}(${data.province}), ${data.country}`;
const dataProperties = [
'code',
(obj) => (obj.town ? `${obj.town}(${obj.province})` : null),
'country',
];
return formatLocation(data, dataProperties);
}
const handleModelValue = (data) => {
emit('update:model-value', data);
};

View File

@ -2,6 +2,7 @@
import { useStateStore } from 'stores/useStateStore';
import LeftMenu from 'components/LeftMenu.vue';
import { onMounted } from 'vue';
import { useQuasar } from 'quasar';
const stateStore = useStateStore();
const $props = defineProps({
@ -10,7 +11,9 @@ const $props = defineProps({
default: true,
},
});
onMounted(() => (stateStore.leftDrawer = $props.leftDrawer));
onMounted(
() => (stateStore.leftDrawer = useQuasar().screen.gt.xs ? $props.leftDrawer : false)
);
</script>
<template>

View File

@ -38,6 +38,10 @@ const $props = defineProps({
type: [Array],
default: () => [],
},
exprBuilder: {
type: Function,
default: null,
},
isClearable: {
type: Boolean,
default: true,
@ -179,6 +183,7 @@ async function fetchFilter(val) {
}, {});
} else defaultWhere = { [key]: getVal(val) };
const where = { ...(val ? defaultWhere : {}), ...$props.where };
$props.exprBuilder && Object.assign(where, $props.exprBuilder(key, val));
const fetchOptions = { where, include, limit };
if (fields) fetchOptions.fields = fields;
if (sortBy) fetchOptions.order = sortBy;
@ -278,4 +283,15 @@ const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);
.q-field--outlined {
max-width: 100%;
}
.q-field__inner {
.q-field__control {
min-height: auto !important;
display: flex;
align-items: flex-end;
.q-field__native.row {
min-height: auto !important;
}
}
}
</style>

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, computed } from 'vue';
import { computed } from 'vue';
import { useRole } from 'src/composables/useRole';
import { useAcl } from 'src/composables/useAcl';

View File

@ -47,6 +47,7 @@ let store;
let entity;
const isLoading = ref(false);
const isSameDataKey = computed(() => $props.dataKey === route.meta.moduleName);
const menuRef = ref();
defineExpose({ getData });
onBeforeMount(async () => {
@ -170,7 +171,7 @@ const toModule = computed(() =>
<QTooltip>
{{ t('components.cardDescriptor.moreOptions') }}
</QTooltip>
<QMenu ref="menuRef">
<QMenu :ref="menuRef">
<QList>
<slot name="menu" :entity="entity" :menu-ref="menuRef" />
</QList>

View File

@ -3,7 +3,6 @@ import { onMounted, ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData';
import { useRoute } from 'vue-router';
import { date } from 'quasar';
import toDate from 'filters/toDate';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
@ -59,7 +58,6 @@ const $props = defineProps({
});
defineExpose({ search, sanitizer });
const emit = defineEmits([
'update:modelValue',
'refresh',
@ -187,7 +185,6 @@ async function remove(key) {
}
function formatValue(value) {
if (value instanceof Date) return date.formatDate(value, 'DD/MM/YYYY');
if (typeof value === 'boolean') return value ? t('Yes') : t('No');
if (isNaN(value) && !isNaN(Date.parse(value))) return toDate(value);
@ -214,7 +211,7 @@ function sanitizer(params) {
icon="search"
@click="search()"
></QBtn>
<QForm @submit="search" id="filterPanelForm">
<QForm @submit="search" id="filterPanelForm" @keyup.enter="search()">
<QList dense>
<QItem class="q-mt-xs">
<QItemSection top>

View File

@ -119,10 +119,9 @@ watch(
);
watch(
() => [props.url, props.filter, props.userParams],
([url, filter, userParams]) => mounted.value && fetch({ url, filter, userParams })
() => [props.url, props.filter],
([url, filter]) => mounted.value && fetch({ url, filter })
);
const addFilter = async (filter, params) => {
await arrayData.addFilter({ filter, params });
};
@ -131,9 +130,8 @@ async function fetch(params) {
useArrayData(props.dataKey, params);
arrayData.reset(['filter.skip', 'skip']);
await arrayData.fetch({ append: false });
if (!store.hasMoreData) {
isLoading.value = false;
}
if (!store.hasMoreData) isLoading.value = false;
emit('onFetch', store.data);
return store.data;
}

View File

@ -9,6 +9,7 @@ defineProps({ wrap: { type: Boolean, default: false } });
<style lang="scss" scoped>
.vn-row {
display: flex;
align-items: flex-end;
> :deep(*) {
flex: 1;
}

View File

@ -26,7 +26,8 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
const params = JSON.parse(query[searchUrl]);
const filter = params?.filter && JSON.parse(params?.filter ?? '{}');
delete params.filter;
store.userParams = { ...params, ...store.userParams };
store.userParams = { ...store.userParams, ...params };
store.userFilter = { ...filter, ...store.userFilter };
if (filter?.order) store.order = filter.order;
}

View File

@ -1,15 +1,18 @@
import { useSession } from './useSession';
import axios from 'axios';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
export function usePrintService() {
const quasar = useQuasar();
const { t } = useI18n();
const { getTokenMultimedia } = useSession();
function sendEmail(path, params) {
return axios.post(path, params).then(() =>
quasar.notify({
message: 'Notification sent',
message: t('globals.notificationSent'),
type: 'positive',
icon: 'check',
})

View File

@ -268,6 +268,7 @@ input::-webkit-inner-spin-button {
max-width: 400px;
}
}
.edit-photo-btn {
position: absolute;
right: 12px;
@ -280,3 +281,21 @@ input::-webkit-inner-spin-button {
color: var(--vn-label-color);
text-transform: uppercase;
}
.q-date {
&__today {
border: 2px solid $info;
color: $info;
}
}
.q-field__inner {
.q-field__control {
min-height: auto !important;
display: flex;
align-items: flex-end;
padding-bottom: 2px;
.q-field__native.row {
min-height: auto !important;
}
}
}

View File

@ -36,7 +36,6 @@ $color-font-secondary: #777;
.bg-success {
background-color: $positive;
}
.bg-notice {
background-color: $info;
}

View File

@ -4,7 +4,7 @@ export default function toHour(date) {
if (!isValidDate(date)) {
return '--:--';
}
return (new Date(date || '')).toLocaleTimeString([], {
return new Date(date || '').toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});

View File

@ -103,6 +103,7 @@ globals:
ticket: Ticket
campaign: Campaign
weight: Weight
error: Ups! Something went wrong
pageTitles:
logIn: Login
addressEdit: Update address
@ -110,7 +111,7 @@ globals:
basicData: Basic data
log: Logs
parkingList: Parkings list
agencyList: Agencies list
agencyList: Agencies
agency: Agency
workCenters: Work centers
modes: Modes
@ -134,7 +135,7 @@ globals:
fiscalData: Fiscal data
billingData: Billing data
consignees: Consignees
'address-create': New address
address-create: New address
notes: Notes
credits: Credits
greuges: Greuges
@ -206,7 +207,7 @@ globals:
roadmap: Roadmap
stops: Stops
routes: Routes
cmrsList: CMRs list
cmrsList: CMRs
RouteList: List
routeCreate: New route
RouteRoadmap: Roadmaps
@ -272,6 +273,7 @@ globals:
clientsActionsMonitor: Clients and actions
serial: Serial
medical: Mutual
RouteExtendedList: Router
supplier: Supplier
created: Created
worker: Worker
@ -287,6 +289,8 @@ globals:
createInvoiceIn: Create invoice in
myAccount: My account
noOne: No one
maxTemperature: Maximum Temperature
minTemperature: Minimum Temperatura
params:
clientFk: Client id
salesPersonFk: Sales person
@ -301,6 +305,7 @@ errors:
statusBadGateway: It seems that the server has fall down
statusGatewayTimeout: Could not contact the server
userConfig: Error fetching user config
updateUserConfig: Error updating user config
tokenConfig: Error fetching token config
writeRequest: The requested operation could not be completed
login:
@ -494,6 +499,8 @@ ticket:
warehouse: Warehouse
customerCard: Customer card
alias: Alias
ticketList: Ticket List
newOrder: New Order
boxing:
expedition: Expedition
item: Item
@ -515,6 +522,7 @@ ticket:
landed: Landed
consigneePhone: Consignee phone
consigneeMobile: Consignee mobile
consigneeAddress: Consignee address
clientPhone: Client phone
clientMobile: Client mobile
consignee: Consignee
@ -544,6 +552,11 @@ ticket:
weight: Weight
goTo: Go to
summaryAmount: Summary
purchaseRequest: Purchase request
service: Service
description: Description
attender: Attender
ok: Ok
create:
client: Client
address: Address
@ -567,7 +580,6 @@ invoiceOut:
client: Client
company: Company
customerCard: Customer card
ticketList: Ticket List
summary:
issued: Issued
created: Created
@ -871,6 +883,7 @@ route:
tickets: Tickets
log: Log
autonomous: Autonomous
RouteExtendedList: Router
cmr:
list:
results: results
@ -900,6 +913,7 @@ supplier:
account: Account
payMethod: Pay Method
payDay: Pay Day
country: Country
summary:
responsible: Responsible
notes: Notes
@ -998,6 +1012,7 @@ travel:
warehouseOut: Warehouse out
totalEntries: Total entries
totalEntriesTooltip: Total entries
daysOnward: Landed days onwards
summary:
confirmed: Confirmed
entryId: Entry Id
@ -1095,7 +1110,7 @@ item:
isPhotoRequestedTooltip: This item does need a photo
description: Description
fixedPrice:
itemId: Item ID
itemFk: Item ID
groupingPrice: Grouping price
packingPrice: Packing price
hasMinPrice: Has min price

View File

@ -63,7 +63,7 @@ globals:
shipped: F. envío
totalEntries: Ent. totales
amount: Importe
packages: Bultos
packages: Embalajes
download: Descargar
downloadPdf: Descargar PDF
selectRows: 'Seleccionar las { numberRows } filas(s)'
@ -105,6 +105,7 @@ globals:
ticket: Ticket
campaign: Campaña
weight: Peso
error: ¡Ups! Algo salió mal
pageTitles:
logIn: Inicio de sesión
addressEdit: Modificar consignatario
@ -112,7 +113,7 @@ globals:
basicData: Datos básicos
log: Historial
parkingList: Listado de parkings
agencyList: Listado de agencias
agencyList: Agencias
agency: Agencia
workCenters: Centros de trabajo
modes: Modos
@ -161,6 +162,7 @@ globals:
dms: Gestión documental
entryCreate: Nueva entrada
latestBuys: Últimas compras
reserves: Reservas
tickets: Tickets
ticketCreate: Nuevo ticket
boxing: Encajado
@ -209,12 +211,13 @@ globals:
roadmap: Troncales
stops: Paradas
routes: Rutas
cmrsList: Listado de CMRs
cmrsList: CMRs
RouteList: Listado
routeCreate: Nueva ruta
RouteRoadmap: Troncales
RouteRoadmapCreate: Crear troncal
autonomous: Autónomos
RouteExtendedList: Enrutador
suppliers: Proveedores
supplier: Proveedor
supplierCreate: Nuevo proveedor
@ -265,7 +268,7 @@ globals:
tracking: Estados
components: Componentes
pictures: Fotos
packages: Bultos
packages: Embalajes
ldap: LDAP
samba: Samba
twoFactor: Doble factor
@ -290,6 +293,8 @@ globals:
createInvoiceIn: Crear factura recibida
myAccount: Mi cuenta
noOne: Nadie
maxTemperature: Temperatura máxima
minTemperature: Temperatura mínima
params:
clientFk: Id cliente
salesPersonFk: Comercial
@ -304,6 +309,7 @@ errors:
statusBadGateway: Parece ser que el servidor ha caído
statusGatewayTimeout: No se ha podido contactar con el servidor
userConfig: Error al obtener configuración de usuario
updateUserConfig: Error al actualizar la configuración de usuario
tokenConfig: Error al obtener configuración de token
writeRequest: No se pudo completar la operación solicitada
login:
@ -484,7 +490,7 @@ ticket:
tracking: Estados
components: Componentes
pictures: Fotos
packages: Bultos
packages: Embalajes
list:
nickname: Alias
state: Estado
@ -502,6 +508,8 @@ ticket:
warehouse: Almacén
customerCard: Ficha del cliente
alias: Alias
ticketList: Listado de tickets
newOrder: Nuevo pedido
boxing:
expedition: Expedición
item: Artículo
@ -523,6 +531,7 @@ ticket:
landed: Entregado
consigneePhone: Tel. consignatario
consigneeMobile: Móv. consignatario
consigneeAddress: Dir. consignatario
clientPhone: Tel. cliente
clientMobile: Móv. cliente
consignee: Consignatario
@ -552,6 +561,10 @@ ticket:
weight: Peso
goTo: Ir a
summaryAmount: Resumen
purchaseRequest: Petición de compra
service: Servicio
description: Descripción
attender: Consignatario
create:
client: Cliente
address: Dirección
@ -884,6 +897,7 @@ supplier:
account: Cuenta
payMethod: Método de pago
payDay: Dia de pago
country: País
summary:
responsible: Responsable
notes: Notas
@ -982,6 +996,7 @@ travel:
warehouseOut: Alm.entrada
totalEntries:
totalEntriesTooltip: Entradas totales
daysOnward: Días de llegada en adelante
summary:
confirmed: Confirmado
entryId: Id entrada
@ -1079,7 +1094,7 @@ item:
isPhotoRequestedTooltip: Este artículo necesita una foto
description: Descripción
fixedPrice:
itemId: ID Artículo
itemFk: ID Artículo
groupingPrice: Precio grouping
packingPrice: Precio packing
hasMinPrice: Tiene precio mínimo

View File

@ -142,7 +142,13 @@ const redirectToRoleSummary = (id) =>
<SubRoleCreateForm @on-submit-create-subrole="createSubRole" />
</QDialog>
<QPageSticky position="bottom-right" :offset="[18, 18]">
<QBtn fab icon="add" color="primary" @click="openCreateSubRoleForm()">
<QBtn
fab
icon="add"
shortcut="+"
color="primary"
@click="openCreateSubRoleForm()"
>
<QTooltip>{{ t('warehouses.add') }}</QTooltip>
</QBtn>
</QPageSticky>

View File

@ -317,7 +317,7 @@ async function saveWhenHasChanges() {
</div>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="showImportDialog()" />
<QBtn fab color="primary" shortcut="+" icon="add" @click="showImportDialog()" />
</QPageSticky>
</template>

View File

@ -246,7 +246,13 @@ function onDrag() {
</QDialog>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<label for="fileInput">
<QBtn fab @click="inputFile.nativeEl.click()" icon="add" color="primary">
<QBtn
fab
@click="inputFile.nativeEl.click()"
shortcut="+"
icon="add"
color="primary"
>
<QInput
ref="inputFile"
type="file"

View File

@ -16,7 +16,7 @@ import { useVnConfirm } from 'composables/useVnConfirm';
import VnTable from 'components/VnTable/VnTable.vue';
import VnInput from 'components/common/VnInput.vue';
import VnSubToolbar from 'components/ui/VnSubToolbar.vue';
import VnFilter from 'components/VnTable/VnFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import CustomerNewPayment from 'src/pages/Customer/components/CustomerNewPayment.vue';
import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue';
@ -33,9 +33,9 @@ const stateStore = useStateStore();
const user = state.getUser();
const clientRisk = ref([]);
const companies = ref([]);
const tableRef = ref();
const companyId = ref(user.value.companyFk);
const companyLastId = ref(user.value.companyFk);
const balances = ref([]);
const vnFilterRef = ref({});
const filter = computed(() => {
@ -45,33 +45,6 @@ const filter = computed(() => {
};
});
const companyFilterColumn = {
align: 'left',
name: 'companyId',
label: t('Company'),
component: 'select',
attrs: {
url: 'Companies',
optionLabel: 'code',
sortBy: 'code',
limit: 0,
},
columnFilter: {
event: {
remove: () => (companyId.value = null),
'update:modelValue': (newCompanyFk) => {
if (!newCompanyFk) return;
vnFilterRef.value.addFilter(newCompanyFk);
companyLastId.value = newCompanyFk;
},
blur: () =>
!companyId.value &&
(companyId.value = companyLastId.value ?? user.value.companyFk),
},
},
visible: false,
};
const columns = computed(() => [
{
align: 'right',
@ -166,6 +139,11 @@ onBeforeMount(() => {
});
async function getCurrentBalance(data) {
currentBalance.value[companyId.value] = {
amount: 0,
code: companies.value.find((c) => c.id === companyId.value)?.code,
};
for (const balance of data) {
currentBalance.value[balance.companyFk] = {
code: balance.company.code,
@ -192,14 +170,21 @@ const showBalancePdf = ({ id }) => {
<template>
<FetchData
url="Companies"
auto-load
@on-fetch="(data) => (companies = data)"
></FetchData>
<FetchData
v-if="companies.length > 0"
url="clientRisks"
:filter="{
include: { relation: 'company', scope: { fields: ['code'] } },
where: { clientFk: route.params.id, companyFk: companyId },
where: { clientFk: route.params.id },
}"
auto-load
@on-fetch="getCurrentBalance"
></FetchData>
<VnSubToolbar class="q-mb-md">
<template #st-data>
<div class="column justify-center q-px-md q-py-sm">
@ -212,13 +197,15 @@ const showBalancePdf = ({ id }) => {
</template>
<template #st-actions>
<div>
<VnFilter
<VnSelect
:label="t('Company')"
ref="vnFilterRef"
v-model="companyId"
data-key="CustomerBalance"
:column="companyFilterColumn"
search-url="balance"
/>
:options="companies"
option-label="code"
option-value="id"
></VnSelect>
</div>
</template>
</VnSubToolbar>

View File

@ -16,6 +16,19 @@ const { t } = useI18n();
const businessTypes = ref([]);
const contactChannels = ref([]);
const title = ref();
const handleSalesModelValue = (val) => ({
or: [
{ name: val },
{ nickname: { like: '%' + val + '%' } },
{ code: { like: `${val}%` } },
],
});
const exprBuilder = (param, value) => {
return {
and: [{ active: { neq: false } }, handleSalesModelValue(value)],
};
};
</script>
<template>
<FetchData
@ -34,7 +47,7 @@ const title = ref();
<VnRow>
<VnInput
:label="t('globals.name')"
:rules="validate('client.socialName')"
:rules="validate('client.name')"
autofocus
clearable
v-model="data.name"
@ -98,8 +111,10 @@ const title = ref();
}"
:fields="['id', 'nickname']"
sort-by="nickname ASC"
option-label="nickname"
option-value="id"
:rules="validate('client.salesPersonFk')"
:use-like="false"
:expr-builder="exprBuilder"
emit-value
auto-load
>

View File

@ -0,0 +1,177 @@
<script setup>
import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import { QItem } from 'quasar';
import VnSelect from 'src/components/common/VnSelect.vue';
import { QItemSection } from 'quasar';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import { toDate } from 'src/filters';
const { t } = useI18n();
defineProps({ dataKey: { type: String, required: true } });
</script>
<template>
<VnFilterPanel :data-key="dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params }">
<QItem>
<QItemSection>
<VnInput
:label="t('params.item')"
v-model="params.itemId"
is-outlined
lazy-rules
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
v-model="params.buyerId"
url="TicketRequests/getItemTypeWorker"
:fields="['id', 'nickname']"
sort-by="nickname ASC"
:label="t('params.buyer')"
option-value="id"
option-label="nickname"
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
v-model="params.typeId"
url="ItemTypes"
:include="['category']"
:fields="['id', 'name', 'categoryFk']"
sort-by="name ASC"
:label="t('params.typeId')"
option-label="name"
option-value="id"
dense
outlined
rounded
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption>{{
scope.opt?.category?.name
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
v-model="params.categoryId"
url="ItemCategories"
:fields="['id', 'name']"
sort-by="name ASC"
:label="t('params.categoryId')"
option-label="name"
option-value="id"
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
v-model="params.campaignId"
url="Campaigns/latest"
sort-by="dated DESC"
:label="t('params.campaignId')"
option-label="code"
option-value="id"
dense
outlined
rounded
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
t(`params.${scope.opt?.code}`)
}}</QItemLabel>
<QItemLabel caption>{{
toDate(scope.opt.dated)
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
:label="t('params.from')"
v-model="params.from"
@update:model-value="searchFn()"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
:label="t('params.to')"
v-model="params.to"
@update:model-value="searchFn()"
is-outlined
/>
</QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
item: Item id
buyer: Buyer
type: Type
category: Category
itemId: Item id
buyerId: Buyer
typeId: Type
categoryId: Category
from: From
to: To
campaignId: Campaña
valentinesDay: Valentine's Day
mothersDay: Mother's Day
allSaints: All Saints' Day
es:
params:
item: Id artículo
buyer: Comprador
type: Tipo
category: Categoría
itemId: Id Artículo
buyerId: Comprador
typeId: Tipo
categoryId: Reino
from: Desde
to: Hasta
campaignId: Campaña
valentinesDay: Día de San Valentín
mothersDay: Día de la Madre
allSaints: Día de Todos los Santos
</i18n>

View File

@ -56,17 +56,18 @@ const customerContactsRef = ref(null);
</div>
</VnRow>
<VnRow>
<QIcon
<QBtn
@click="customerContactsRef.insert()"
class="cursor-pointer"
color="primary"
name="add"
size="sm"
flat
icon="add"
shortcut="+"
>
<QTooltip>
{{ t('Add contact') }}
</QTooltip>
</QIcon>
</QBtn>
</VnRow>
</QCard>
</template>

View File

@ -190,6 +190,18 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit
>
<QTooltip>{{ t('Go to user') }}</QTooltip>
</QBtn>
<QBtn
v-if="entity.supplier"
:to="{
name: 'SupplierSummary',
params: { id: entity.supplier.id },
}"
size="md"
icon="vn:supplier"
color="primary"
>
<QTooltip>{{ t('Go to supplier') }}</QTooltip>
</QBtn>
</QCardActions>
</template>
</CardDescriptor>
@ -204,6 +216,7 @@ es:
Customer ticket list: Listado de tickets del cliente
Customer invoice out list: Listado de facturas del cliente
Go to user: Ir al usuario
Go to supplier: Ir al proveedor
Customer unpaid: Cliente impago
Unpaid: Impagado
unpaidDated: 'Fecha {dated}'

View File

@ -236,6 +236,7 @@ const toCustomerFileManagementCreate = () => {
@click.stop="toCustomerFileManagementCreate()"
color="primary"
fab
shortcut="+"
icon="add"
/>
<QTooltip>

View File

@ -134,15 +134,17 @@ function handleLocation(data, location) {
</QTooltip>
</QIcon>
</div>
<QCheckbox :label="t('Verified data')" v-model="data.isTaxDataChecked" />
<QCheckbox :label="t('Daily invoice')" v-model="data.hasDailyInvoice" />
</VnRow>
<VnRow>
<QCheckbox
:label="t('Electronic invoice')"
v-model="data.hasElectronicInvoice"
/><QCheckbox
:label="t('Verified data')"
v-model="data.isTaxDataChecked"
/>
<QCheckbox :label="t('Daily invoice')" v-model="data.hasDailyInvoice" />
</VnRow>
</template>
</FormModel>

View File

@ -99,7 +99,13 @@ const tableRef = ref();
</VnTable>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerSamplesCreate()" color="primary" fab icon="add" />
<QBtn
@click.stop="toCustomerSamplesCreate()"
color="primary"
fab
icon="add"
shortcut="+"
/>
<QTooltip>
{{ t('Send sample') }}
</QTooltip>

View File

@ -1,35 +1,20 @@
<script setup>
import { ref } 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';
const { t } = useI18n();
const props = defineProps({
defineProps({
dataKey: {
type: String,
required: true,
},
});
const provinces = ref();
const workers = ref();
const zones = ref();
</script>
<template>
<FetchData url="Provinces" @on-fetch="(data) => (provinces = data)" auto-load />
<FetchData url="Zones" @on-fetch="(data) => (zones = data)" auto-load />
<FetchData
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<VnFilterPanel :data-key="props.dataKey" :search-button="true" search-url="table">
<VnFilterPanel :data-key="dataKey" :search-button="true" search-url="table">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
@ -65,15 +50,14 @@ const zones = ref();
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
<QItemSection v-if="!workers">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers">
<QItemSection>
<VnSelect
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
auto-load
:label="t('Salesperson')"
v-model="params.salesPersonFk"
@update:model-value="searchFn()"
:options="workers"
option-value="id"
option-label="name"
emit-value
@ -88,15 +72,12 @@ const zones = ref();
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
<QItemSection v-if="!provinces">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="provinces">
<VnSelect
<QItemSection
><VnSelect
url="Provinces"
:label="t('Province')"
v-model="params.provinceFk"
@update:model-value="searchFn()"
:options="provinces"
option-value="id"
option-label="name"
emit-value
@ -105,6 +86,7 @@ const zones = ref();
dense
outlined
rounded
auto-load
:input-debounce="0"
/>
</QItemSection>
@ -135,25 +117,21 @@ const zones = ref();
</QItemSection>
</QItem>
<QItem>
<QItemSection v-if="!zones">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="zones">
<VnSelect
:label="t('Zone')"
v-model="params.zoneFk"
@update:model-value="searchFn()"
:options="zones"
option-value="id"
option-label="name"
emit-value
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
<VnSelect
url="Zones"
:label="t('Zone')"
v-model="params.zoneFk"
@update:model-value="searchFn()"
option-value="id"
option-label="name"
emit-value
map-options
hide-selected
dense
outlined
rounded
auto-load
/>
</QItem>
<QItem>
<QItemSection>

View File

@ -8,10 +8,10 @@ import VnLocation from 'src/components/common/VnLocation.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import CustomerSummary from './Card/CustomerSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import RightMenu from 'src/components/common/RightMenu.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import { toDate } from 'src/filters';
import CustomerFilter from './CustomerFilter.vue';
const { t } = useI18n();
const router = useRouter();
@ -397,6 +397,11 @@ function handleLocation(data, location) {
:label="t('Search customer')"
data-key="Customer"
/>
<RightMenu>
<template #right-panel>
<CustomerFilter data-key="Customer" />
</template>
</RightMenu>
<VnTable
ref="tableRef"
data-key="Customer"
@ -413,6 +418,7 @@ function handleLocation(data, location) {
order="id DESC"
:columns="columns"
redirect="customer"
:right-search="false"
auto-load
>
<template #more-create-dialog="{ data }">
@ -423,9 +429,10 @@ function handleLocation(data, location) {
:params="{
departmentCodes: ['VT', 'shopping'],
}"
option-label="nickname"
option-value="id"
:fields="['id', 'nickname']"
sort-by="nickname ASC"
:use-like="false"
emit-value
auto-load
>

View File

@ -134,6 +134,7 @@ function handleLocation(data, location) {
option-label="fiscalName"
option-value="id"
v-model="data.customsAgentFk"
:tooltip="t('Create a new expense')"
>
<template #form>
<CustomerNewCustomsAgent @on-data-saved="refreshData()" />

View File

@ -11,7 +11,7 @@ import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import CustomsNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCustomsAgent.vue';
import CustomerNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCustomsAgent.vue';
const { t } = useI18n();
const route = useRoute();
@ -226,9 +226,10 @@ function handleLocation(data, location) {
option-label="fiscalName"
option-value="id"
v-model="data.customsAgentFk"
:tooltip="t('New customs agent')"
>
<template #form>
<CustomsNewCustomsAgent />
<CustomerNewCustomsAgent />
</template>
</VnSelectDialog>
</div>
@ -272,16 +273,17 @@ function handleLocation(data, location) {
</div>
</VnRow>
<QIcon
<QBtn
@click.stop="addNote()"
class="cursor-pointer add-icon q-mt-md"
name="add"
size="sm"
flat
icon="add"
shortcut="+"
>
<QTooltip>
{{ t('Add note') }}
</QTooltip>
</QIcon>
</QBtn>
</template>
</FormModel>
</template>
@ -308,6 +310,7 @@ es:
Mobile: Movíl
Incoterms: Incoterms
Customs agent: Agente de aduanas
New customs agent: Nuevo agente de aduanas
Notes: Notas
Observation type: Tipo de observación
Description: Descripción

View File

@ -46,7 +46,6 @@ const onSubmit = async () => {
};
try {
await axios.patch(`Clients/${$props.id}/setPassword`, payload);
await $props.promise();
} catch (error) {
notify('errors.create', 'negative');
} finally {

View File

@ -35,6 +35,7 @@ const filter = {
],
where: { clientFk: route.params.id },
order: ['shipped DESC', 'id'],
limit: 30,
};
const columns = computed(() => [
@ -149,7 +150,6 @@ const setShippedColor = (date) => {
auto-load
order="shipped DESC, id"
:disable-option="{ card: true, table: true }"
limit="5"
class="full-width"
:disable-infinite-scroll="true"
>

View File

@ -58,7 +58,7 @@ customer:
vies: VIES
payMethod: Método de pago
bankAccount: Cuenta bancaria
dueDay: Día de pago
dueDay: Vencimiento
hasLcr: Recibido LCR
hasCoreVnl: Recibido core VNL
hasB2BVnl: Recibido B2B VNL

View File

@ -138,7 +138,13 @@ const columns = computed(() => [
</template>
</CrudModel>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="entryObservationsRef.insert()" />
<QBtn
fab
color="primary"
icon="add"
shortcut="+"
@click="entryObservationsRef.insert()"
/>
</QPageSticky>
</template>
<i18n>

View File

@ -10,6 +10,7 @@ import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.v
import { toDate, toCurrency } from 'src/filters';
import { getUrl } from 'src/composables/getUrl';
import axios from 'axios';
import FetchedTags from 'src/components/ui/FetchedTags.vue';
const route = useRoute();
const { t } = useI18n();
@ -163,7 +164,7 @@ const fetchEntryBuys = async () => {
>
<template #header-left>
<router-link
v-if="route.name !== 'EntrySummary'"
v-if="route?.name !== 'EntrySummary'"
:to="{ name: 'EntrySummary', params: { id: entityId } }"
class="header link"
:href="entryUrl"
@ -184,7 +185,10 @@ const fetchEntryBuys = async () => {
<QIcon name="open_in_new" />
</router-link>
<VnLv :label="t('entry.summary.commission')" :value="entry.commission" />
<VnLv :label="t('entry.summary.currency')" :value="entry.currency.name" />
<VnLv
:label="t('entry.summary.currency')"
:value="entry.currency?.name"
/>
<VnLv :label="t('entry.summary.company')" :value="entry.company.code" />
<VnLv :label="t('entry.summary.reference')" :value="entry.reference" />
<VnLv
@ -210,12 +214,12 @@ const fetchEntryBuys = async () => {
</VnLv>
<VnLv
:label="t('entry.summary.travelAgency')"
:value="entry.travel.agency.name"
:value="entry.travel.agency?.name"
/>
<VnLv :label="t('shipped')" :value="toDate(entry.travel.shipped)" />
<VnLv
:label="t('entry.summary.travelWarehouseOut')"
:value="entry.travel.warehouseOut.name"
:value="entry.travel.warehouseOut?.name"
/>
<QCheckbox
:label="t('entry.summary.travelDelivered')"
@ -225,7 +229,7 @@ const fetchEntryBuys = async () => {
<VnLv :label="t('landed')" :value="toDate(entry.travel.landed)" />
<VnLv
:label="t('entry.summary.travelWarehouseIn')"
:value="entry.travel.warehouseIn.name"
:value="entry.travel.warehouseIn?.name"
/>
<QCheckbox
:label="t('entry.summary.travelReceived')"
@ -281,17 +285,17 @@ const fetchEntryBuys = async () => {
>
<template #body="{ cols, row, rowIndex }">
<QTr no-hover>
<QTd v-for="col in cols" :key="col.name">
<QTd v-for="col in cols" :key="col?.name">
<component
:is="tableColumnComponents[col.name].component(props)"
v-bind="tableColumnComponents[col.name].props(props)"
@click="tableColumnComponents[col.name].event(props)"
:is="tableColumnComponents[col?.name].component()"
v-bind="tableColumnComponents[col?.name].props()"
@click="tableColumnComponents[col?.name].event()"
class="col-content"
>
<template
v-if="
col.name !== 'observation' &&
col.name !== 'isConfirmed'
col?.name !== 'observation' &&
col?.name !== 'isConfirmed'
"
>{{ col.value }}</template
>

View File

@ -1,16 +1,17 @@
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import RightMenu from 'src/components/common/RightMenu.vue';
import VnTable from 'components/VnTable/VnTable.vue';
import EntryLatestBuysFilter from './EntryLatestBuysFilter.vue';
import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'src/filters';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import EntryLatestBuysFilter from './EntryLatestBuysFilter.vue';
import VnTable from 'components/VnTable/VnTable.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const stateStore = useStateStore();
const { t } = useI18n();
import { toDate } from 'src/filters';
import VnImg from 'src/components/ui/VnImg.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
const columns = [
{
align: 'center',

View File

@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue';
import VnSelect from 'components/common/VnSelect.vue';
import ItemsFilterPanel from 'src/components/ItemsFilterPanel.vue';
@ -18,6 +19,7 @@ defineProps({
const itemTypeWorkersOptions = ref([]);
const suppliersOptions = ref([]);
const tagValues = ref([]);
</script>
<template>

View File

@ -47,17 +47,16 @@ const columns = [
},
},
{
align: 'left',
align: 'center',
label: t('Reserve'),
name: 'reserve',
columnFilter: false,
create: true,
component: 'number',
summation: true,
cardVisible: true,
},
{
align: 'left',
align: 'center',
label: t('Bought'),
name: 'bought',
summation: true,
@ -77,7 +76,7 @@ const columns = [
name: 'tableActions',
actions: [
{
title: t('More'),
title: t('View more details'),
icon: 'search',
isPrimary: true,
action: (row) => {
@ -101,14 +100,9 @@ const travel = ref(null);
const userParams = ref({
dated: Date.vnNew(),
});
const filter = ref({
where: {
shipped: (userParams.value.dated
? new Date(userParams.value.dated)
: Date.vnNew()
).setHours(0, 0, 0, 0),
m3: { neq: null },
},
fields: ['id', 'm3', 'warehouseInFk'],
include: [
{
relation: 'warehouseIn',
@ -117,6 +111,13 @@ const filter = ref({
},
},
],
where: {
shipped: (userParams.value.dated
? new Date(userParams.value.dated)
: Date.vnNew()
).setHours(0, 0, 0, 0),
m3: { neq: null },
},
});
const setUserParams = async ({ dated }) => {
@ -128,6 +129,22 @@ const setUserParams = async ({ dated }) => {
function openDialog() {
travelDialogRef.value = true;
}
function setFooter(data) {
const footer = {
bought: 0,
reserve: 0,
};
data.forEach((row) => {
footer.bought += row?.bought;
footer.reserve += row?.reserve;
});
tableRef.value.footer = footer;
}
function round(value) {
return Math.round(value * 100) / 100;
}
</script>
<template>
<VnSubToolbar>
@ -139,7 +156,9 @@ function openDialog() {
:filter="filter"
@on-fetch="
(data) => {
travel = data.filter((data) => data.warehouseIn.code === 'VNH');
travel = data.find(
(data) => data.warehouseIn.code.toLowerCase() === 'vnh'
);
}
"
/>
@ -149,10 +168,10 @@ function openDialog() {
{{ t('Booked trucks') }}:
</span>
<span>
{{ travel[0]?.m3 }}
{{ travel?.m3 }}
</span>
<QBtn
v-if="travel[0]?.m3"
v-if="travel?.m3"
style="max-width: 20%"
flat
icon="edit"
@ -166,10 +185,10 @@ function openDialog() {
</VnSubToolbar>
<QDialog v-model="travelDialogRef" :maximized="true" :class="['vn-row', 'wrap']">
<FormModelPopup
:url-update="`Travels/${travel[0].id}`"
:url-update="`Travels/${travel?.id}`"
model="travel"
:title="t('Travel m3')"
:form-initial-data="{ id: travel[0].id, m3: travel[0].m3 }"
:form-initial-data="{ id: travel?.id, m3: travel?.m3 }"
@on-data-saved="fetchDataRef.fetch()"
>
<template #form-inputs="{ data }">
@ -192,51 +211,81 @@ function openDialog() {
/>
</template>
</RightMenu>
<QPage class="column items-center q-pa-md">
<VnTable
ref="tableRef"
data-key="StockBoughts"
url="StockBoughts/getStockBought"
save-url="StockBoughts/crud"
order="reserve DESC"
:right-search="false"
:is-editable="true"
:create="{
urlCreate: 'StockBoughts',
title: t('Reserve some space'),
onDataSaved: () => tableRef.reload(),
formInitialData: {
workerFk: user.id,
dated: Date.vnNow(),
},
}"
:columns="columns"
:user-params="userParams"
:footer="true"
auto-load
>
<template #column-workerFk="{ row }">
<span class="link" @click.stop>
{{ row?.worker?.user?.name }}
<WorkerDescriptorProxy :id="row?.workerFk" />
</span>
</template>
</VnTable>
</QPage>
<div class="table-container">
<div class="column items-center">
<VnTable
ref="tableRef"
data-key="StockBoughts"
url="StockBoughts/getStockBought"
save-url="StockBoughts/crud"
order="reserve DESC"
:right-search="false"
:is-editable="true"
@on-fetch="(data) => setFooter(data)"
:create="{
urlCreate: 'StockBoughts',
title: t('Reserve some space'),
onDataSaved: () => tableRef.reload(),
formInitialData: {
workerFk: user.id,
dated: Date.vnNow(),
},
}"
:columns="columns"
:user-params="userParams"
:footer="true"
table-height="80vh"
auto-load
>
<template #column-workerFk="{ row }">
<span class="link" @click.stop>
{{ row?.worker?.user?.name }}
<WorkerDescriptorProxy :id="row?.workerFk" />
</span>
</template>
<template #column-bought="{ row }">
<span :class="{ 'text-negative': row.reserve < row.bought }">
{{ row?.bought }}
</span>
</template>
<template #column-footer-reserve>
<span>
{{ round(tableRef.footer.reserve) }}
</span>
</template>
<template #column-footer-bought>
<span
:class="{
'text-negative':
tableRef.footer.reserve < tableRef.footer.bought,
}"
>
{{ round(tableRef.footer.bought) }}
</span>
</template>
</VnTable>
</div>
</div>
</template>
<style lang="css" scoped>
<style lang="scss" scoped>
.travel {
margin-bottom: 0px;
}
.table-container {
display: flex;
justify-content: center;
}
.column {
display: flex;
flex-direction: column;
align-items: center;
width: 35%;
}
.text-negative {
color: $negative !important;
}
</style>
<i18n>
en:
Buyer: Buyer
Reserve: Reserve
Bought: Bought
More: More
Date: Date
This buyer has already made a reservation for this date: This buyer has already made a reservation for this date
es:
Edit travel: Editar envío
Travel: Envíos
@ -244,8 +293,8 @@ function openDialog() {
Buyer: Comprador
Reserve: Reservado
Bought: Comprado
More: Más
Date: Fecha
View more details: Ver más detalles
Reserve some space: Reservar espacio
This buyer has already made a reservation for this date: Este comprador ya ha hecho una reserva para esta fecha
</i18n>

View File

@ -22,7 +22,7 @@ const customUrl = `StockBoughts/getStockBoughtDetail?workerFk=${$props.workerFk}
const columns = [
{
align: 'left',
label: 'Entry',
label: t('Entry'),
name: 'entryFk',
isTitle: true,
isId: true,
@ -41,29 +41,33 @@ const columns = [
create: true,
columnClass: 'expand',
columnFilter: false,
cardVisible: true,
},
{
align: 'left',
name: 'volume',
label: t('Volume'),
columnFilter: false,
cardVisible: true,
},
{
align: 'left',
label: t('Packaging'),
name: 'packagingFk',
columnFilter: false,
cardVisible: true,
},
{
align: 'left',
label: 'Packing',
name: 'packing',
columnFilter: false,
cardVisible: true,
},
];
</script>
<template>
<QDialog :maximized="true">
<QDialog>
<div class="container">
<VnTable
ref="tableRef"
@ -78,13 +82,13 @@ const columns = [
auto-load
>
<template #column-entryFk="{ row }">
<span class="link" @click.stop>
<span class="link">
{{ row?.entryFk }}
<EntryDescriptorProxy :id="row.entryFk" />
</span>
</template>
<template #column-itemName="{ row }">
<span class="link" @click.stop>
<span class="link">
{{ row?.itemName }}
<ItemDescriptorProxy :id="row.itemFk" />
</span>
@ -93,14 +97,18 @@ const columns = [
</div>
</QDialog>
</template>
<style lang="css">
.q-dialog__inner {
<style lang="css" scoped>
.container {
max-width: 50vw;
overflow: auto;
display: flex;
justify-content: center;
align-items: center;
margin: auto;
background-color: var(--vn-section-color);
padding: 4px;
}
.container > div > div > .q-table__top.relative-position.row.items-center {
background-color: red !important;
}
</style>
<i18n>
@ -110,4 +118,9 @@ const columns = [
Bought: Comprado
More: Más
Date: Fecha
Entry: Entrada
Item: Artículo
Name: Nombre
Volume: Volumen
Packaging: Embalage
</i18n>

View File

@ -281,6 +281,7 @@ async function onSubmit() {
v-else
icon="add_circle"
round
shortcut="+"
padding="xs"
@click="setCreateDms()"
>

View File

@ -230,7 +230,7 @@ async function insert() {
</template>
</CrudModel>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn color="primary" icon="add" size="lg" round @click="insert" />
<QBtn color="primary" icon="add" shortcut="+" size="lg" round @click="insert" />
</QPageSticky>
</template>
<style lang="scss" scoped>

View File

@ -224,6 +224,7 @@ const formatOpt = (row, { model, options }, prop) => {
<QBtn
color="primary"
icon="add"
shortcut="+"
size="lg"
round
@click="invoiceInFormRef.insert()"

View File

@ -405,6 +405,7 @@ const formatOpt = (row, { model, options }, prop) => {
color="primary"
icon="add"
size="lg"
shortcut="+"
round
@click="invoiceInFormRef.insert()"
>

View File

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

View File

@ -27,13 +27,16 @@ const { openReport } = usePrintService();
const columns = computed(() => [
{
align: 'left',
align: 'center',
name: 'id',
label: t('invoiceOutList.tableVisibleColumns.id'),
chip: {
condition: () => true,
},
isId: true,
columnFilter: {
name: 'search',
},
},
{
align: 'left',

View File

@ -65,17 +65,18 @@ const focusLastInput = () => {
</QTooltip>
</QIcon>
</div>
<QIcon
<QBtn
@click="insertRow()"
class="cursor-pointer fill-icon-on-hover"
color="primary"
name="add_circle"
size="sm"
icon="add_circle"
shortcut="+"
flat
>
<QTooltip>
{{ t('Add barcode') }}
</QTooltip>
</QIcon>
</QBtn>
</QCard>
</template>
</CrudModel>

View File

@ -168,19 +168,20 @@ const insertTag = (rows) => {
</div>
</VnRow>
<VnRow class="justify-center items-center">
<QIcon
<QBtn
@click="insertTag(rows)"
class="cursor-pointer"
:disable="!validRow"
color="primary"
name="add"
size="sm"
flat
icon="add"
shortcut="+"
style="flex: 0"
>
<QTooltip>
{{ t('itemTags.addTag') }}
</QTooltip>
</QIcon>
</QBtn>
</VnRow>
</QCard>
</template>

View File

@ -36,23 +36,23 @@ const user = state.getUser();
const fixedPrices = ref([]);
const warehousesOptions = ref([]);
const rowsSelected = ref([]);
const itemFixedPriceFilterRef = ref();
const params = reactive({});
const defaultColumnAttrs = {
align: 'left',
sortable: true,
};
onMounted(async () => {
stateStore.rightDrawer = true;
params.warehouseFk = user.value.warehouseFk;
});
onUnmounted(() => (stateStore.rightDrawer = false));
const defaultColumnAttrs = {
align: 'left',
sortable: true,
};
const columns = computed(() => [
{
label: t('item.fixedPrice.itemId'),
name: 'itemId',
label: t('item.fixedPrice.itemFk'),
name: 'itemFk',
...defaultColumnAttrs,
isId: true,
cardVisible: true,
@ -426,7 +426,7 @@ function handleOnDataSave({ CrudModelRef }) {
:default-save="false"
data-key="ItemFixedPrices"
url="FixedPrices/filter"
:order="['itemFk ASC']"
:order="['description DESC']"
save-url="FixedPrices/crud"
:user-params="{ warehouseFk: user.warehouseFk }"
ref="tableRef"
@ -460,7 +460,7 @@ function handleOnDataSave({ CrudModelRef }) {
<QCheckbox flat v-model="scope.selected" />
</template>
<template #column-itemId="props">
<template #column-itemFk="props">
<VnSelect
style="max-width: 100px"
url="Items/withName"
@ -514,7 +514,7 @@ function handleOnDataSave({ CrudModelRef }) {
</template>
<template #column-minPrice="props">
<QTd class="col">
<div class="row">
<div class="row" style="align-items: center">
<QCheckbox
:model-value="props.row.hasMinPrice"
@update:model-value="updateMinPrice($event, props)"
@ -600,11 +600,14 @@ function handleOnDataSave({ CrudModelRef }) {
.q-table th,
.q-table td {
padding-inline: 5px !important;
// text-align: -webkit-right;
}
.q-table tr td {
font-size: 10pt;
border-top: none;
border-collapse: collapse;
}
.q-table tbody td {
max-width: none;
.q-td.col {
& .vnInputDate {
min-width: 90px;

View File

@ -212,6 +212,7 @@ const decrement = (paramsObj, key) => {
flat
dense
size="12px"
shortcut="+"
@click="add(params, 'scopeDays')"
/>
<QBtn

View File

@ -139,7 +139,11 @@ const columns = computed(() => [
</VnTable>
</template>
<style lang="scss" scoped>
.full-width .vn-row > * {
flex: 0.4;
.full-width .vn-row {
margin-bottom: 0;
flex-direction: row;
> * {
max-width: 125px;
}
}
</style>

View File

@ -132,7 +132,6 @@ const openTab = (id) =>
:table="{
'row-key': 'id',
selection: 'multiple',
'hide-bottom': true,
}"
default-mode="table"
:row-click="({ id }) => openTab(id)"

View File

@ -11,7 +11,7 @@ import FetchData from 'src/components/FetchData.vue';
import { dateRange } from 'src/filters';
defineProps({ dataKey: { type: String, required: true } });
const { t } = useI18n();
const { t, te } = useI18n();
const warehouses = ref();
const groupedStates = ref();
@ -26,6 +26,12 @@ const handleScopeDays = (params, days, callback) => {
}
if (callback) callback();
};
const getLocale = (label) => {
const param = label.split('.').at(-1);
const globalLocale = `globals.params.${param}`;
return te(globalLocale) ? t(globalLocale) : t(`params.${param}`);
};
</script>
<template>
<FetchData url="Warehouses" auto-load @on-fetch="(data) => (warehouses = data)" />
@ -43,10 +49,11 @@ const handleScopeDays = (params, days, callback) => {
:hidden-tags="['from', 'to', 'search']"
:custom-tags="['scopeDays']"
:unremovable-params="['from', 'to', 'scopeDays']"
search-url="saleMonitorTickets"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong v-text="`${t(`params.${tag.label}`)}:`" />
<strong v-text="`${getLocale(tag.label)}:`" />
<span v-text="formatFn(tag.value)" />
</div>
</template>
@ -110,7 +117,7 @@ const handleScopeDays = (params, days, callback) => {
url="Workers/search"
:params="{ departmentCodes: ['VT'] }"
is-outlined
option-value="code"
option-value="id"
option-label="name"
:no-one="true"
>

View File

@ -54,7 +54,6 @@ const columns = computed(() => [
label: t('salesTicketsTable.problems'),
name: 'totalProblems',
align: 'left',
columnFilter: false,
attrs: {
dense: true,
@ -65,7 +64,6 @@ const columns = computed(() => [
name: 'id',
field: 'id',
align: 'left',
columnFilter: {
component: 'number',
name: 'id',
@ -108,9 +106,7 @@ const columns = computed(() => [
{
label: t('salesTicketsTable.date'),
name: 'shippedDate',
style: { 'max-width': '100px' },
align: 'left',
columnFilter: {
component: 'date',
name: 'shippedDate',
@ -164,7 +160,6 @@ const columns = computed(() => [
label: t('salesTicketsTable.state'),
name: 'state',
align: 'left',
style: { 'max-width': '100px' },
columnFilter: {
component: 'select',
name: 'stateFk',
@ -193,7 +188,6 @@ const columns = computed(() => [
label: t('salesTicketsTable.zone'),
name: 'zoneFk',
align: 'left',
columnFilter: {
component: 'select',
name: 'zoneFk',
@ -210,8 +204,6 @@ const columns = computed(() => [
name: 'totalWithVat',
field: 'totalWithVat',
align: 'left',
style: { 'max-width': '75px' },
columnFilter: {
component: 'number',
name: 'totalWithVat',
@ -370,7 +362,7 @@ const openTab = (id) =>
</QCheckbox>
</template>
<template #column-totalProblems="{ row }">
<QTd class="no-padding" style="max-width: 60px">
<span>
<QIcon
v-if="row.isTaxDataChecked === 0"
name="vn:no036"
@ -424,46 +416,40 @@ const openTab = (id) =>
>
<QTooltip>{{ $t('salesTicketsTable.tooLittle') }}</QTooltip>
</QIcon>
</QTd>
</span>
</template>
<template #column-id="{ row }">
<QTd class="no-padding">
<span class="link" @click.stop.prevent>
{{ row.id }}
<TicketDescriptorProxy :id="row.id" />
</span>
</QTd>
<span class="link" @click.stop.prevent>
{{ row.id }}
<TicketDescriptorProxy :id="row.id" />
</span>
</template>
<template #column-clientFk="{ row }">
<QTd class="no-padding" @click.stop :title="row.nickname">
<span class="link">{{ row.nickname }}</span>
<div @click.stop :title="row.nickname">
<span class="link" v-text="row.nickname" />
<CustomerDescriptorProxy :id="row.clientFk" />
</QTd>
</div>
</template>
<template #column-salesPersonFk="{ row }">
<QTd class="no-padding" @click.stop :title="row.userName">
<div @click.stop :title="row.userName">
<span class="link" v-text="dashIfEmpty(row.userName)" />
<WorkerDescriptorProxy :id="row.salesPersonFk" />
</QTd>
</div>
</template>
<template #column-shippedDate="{ row }">
<QTd class="no-padding">
<QBadge
v-bind="getBadgeAttrs(row.shippedDate)"
class="q-pa-sm"
style="font-size: 14px"
>
{{ formatShippedDate(row.shippedDate) }}
</QBadge>
</QTd>
<QBadge
v-bind="getBadgeAttrs(row.shippedDate)"
class="q-pa-sm"
style="font-size: 14px"
>
{{ formatShippedDate(row.shippedDate) }}
</QBadge>
</template>
<template #column-provinceFk="{ row }">
<QTd class="no-padding">
<span :title="row.province" v-text="row.province" />
</QTd>
<span :title="row.province" v-text="row.province" />
</template>
<template #column-state="{ row }">
<QTd class="no-padding" @click.stop.prevent>
<div @click.stop.prevent>
<div v-if="row.refFk">
<span class="link">{{ row.refFk }}</span>
<InvoiceOutDescriptorProxy :id="row.invoiceOutId" />
@ -477,32 +463,33 @@ const openTab = (id) =>
>
{{ row.state }}
</QBadge>
</QTd>
</div>
</template>
<template #column-isFragile="{ row }">
<QTd class="no-padding">
<QIcon v-if="row.isFragile" name="local_bar" color="primary" size="sm">
<QTooltip>{{ $t('salesTicketsTable.isFragile') }}</QTooltip>
</QIcon>
</QTd>
<QIcon v-if="row.isFragile" name="local_bar" color="primary" size="sm">
<QTooltip>{{ $t('salesTicketsTable.isFragile') }}</QTooltip>
</QIcon>
</template>
<template #column-zoneFk="{ row }">
<QTd class="no-padding" @click.stop.prevent :title="row.zoneName">
<div @click.stop.prevent :title="row.zoneName">
<span class="link">{{ row.zoneName }}</span>
<ZoneDescriptorProxy :id="row.zoneFk" />
</QTd>
</div>
</template>
<template #column-totalWithVat="{ row }">
<QTd class="no-padding">
<QBadge
:color="totalPriceColor(row) || 'transparent'"
:text-color="totalPriceColor(row) ? 'black' : 'white'"
class="q-pa-sm"
style="font-size: 14px"
>
{{ toCurrency(row.totalWithVat) }}
</QBadge>
</QTd>
<QBadge
:color="totalPriceColor(row) || 'transparent'"
:text-color="totalPriceColor(row) ? 'black' : 'white'"
class="q-pa-sm"
style="font-size: 14px"
>
{{ toCurrency(row.totalWithVat) }}
</QBadge>
</template>
</VnTable>
</template>
<style lang="scss" scoped>
td .q-icon {
margin: 0 2px;
}
</style>

View File

@ -374,8 +374,10 @@ function addOrder(value, field, params) {
/>
</QItem>
<QItem class="q-mt-lg">
<QIcon
name="add_circle"
<QBtn
icon="add_circle"
shortcut="+"
flat
class="filter-icon"
@click="tagValues.push({})"
/>

View File

@ -249,7 +249,7 @@ watch(
@on-fetch="(data) => (orderSummary.vat = data)"
auto-load
/>
<QDrawer side="right" :width="270" v-model="stateStore.rightDrawer">
<QDrawer side="right" :width="265" v-model="stateStore.rightDrawer">
<QCard
class="order-lines-summary q-pa-lg"
v-if="orderSummary.vat && orderSummary.total"

View File

@ -14,14 +14,15 @@ import OrderFilter from './Card/OrderFilter.vue';
import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue';
import WorkerDescriptorProxy from '../Worker/Card/WorkerDescriptorProxy.vue';
import { toDateTimeFormat } from 'src/filters/date';
import { onMounted } from 'vue';
import { useRoute } from 'vue-router';
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const tableRef = ref();
const agencyList = ref([]);
const addressesList = ref([]);
const clientId = ref();
const route = useRoute();
const columns = computed(() => [
{
align: 'left',
@ -169,6 +170,13 @@ const getDateColor = (date) => {
if (comparation == 0) return 'bg-warning';
if (comparation < 0) return 'bg-success';
};
onMounted(() => {
if (!route.query.createForm) return;
const clientId = route.query.createForm;
const id = JSON.parse(clientId);
fetchClientAddress(id.clientFk, id);
});
</script>
<template>
<OrderSearchbar />
@ -184,13 +192,14 @@ const getDateColor = (date) => {
:order="['landed DESC', 'clientFk ASC', 'id DESC']"
:create="{
urlCreate: 'Orders/new',
title: 'Create Order',
title: t('module.cerateOrder'),
onDataSaved: (url) => {
tableRef.redirect(url);
},
formInitialData: {
active: true,
addressId: null,
clientFk: null,
},
}"
:user-params="{ showEmpty: false }"
@ -221,7 +230,7 @@ const getDateColor = (date) => {
<VnSelect
url="Clients"
:include="{ relation: 'addresses' }"
v-model="clientId"
v-model="data.clientFk"
:label="t('module.customer')"
@update:model-value="(id) => fetchClientAddress(id, data)"
/>

View File

@ -10,6 +10,7 @@ module:
total: Total
salesPerson: Sales Person
address: Address
cerateOrder: Create order
lines:
item: Item
warehouse: Warehouse

View File

@ -10,6 +10,7 @@ module:
total: Total
salesPerson: Comercial
address: Dirección
cerateOrder: Crear cesta
lines:
item: Artículo
warehouse: Almacén

View File

@ -27,12 +27,15 @@ const columns = computed(() => [
condition: () => true,
},
isId: true,
columnFilter: false,
},
{
align: 'left',
label: t('globals.name'),
name: 'name',
isTitle: true,
columnFilter: false,
columnClass: 'expand',
},
{
align: 'left',
@ -70,18 +73,33 @@ const columns = computed(() => [
data-key="AgencyList"
:expr-builder="exprBuilder"
/>
<VnTable
ref="tableRef"
data-key="AgencyList"
url="Agencies"
order="name"
:columns="columns"
:right-search="false"
:use-model="true"
redirect="agency"
default-mode="card"
/>
<div class="list-container">
<div class="list">
<VnTable
data-key="AgencyList"
url="Agencies"
order="name"
:columns="columns"
:right-search="false"
:use-model="true"
redirect="agency"
default-mode="card"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.list {
display: flex;
flex-direction: column;
align-items: center;
width: 55%;
}
.list-container {
display: flex;
justify-content: center;
}
</style>
<i18n>
es:
isOwn: Tiene propietario

View File

@ -88,7 +88,7 @@ async function deleteWorCenter(id) {
</VnPaginate>
</div>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="dialog.show()" color="primary" fab icon="add">
<QBtn @click.stop="dialog.show()" color="primary" fab shortcut="+" icon="add">
<QDialog ref="dialog">
<FormModelPopup
:title="t('Add work center')"

View File

@ -167,8 +167,8 @@ const setTicketsRoute = async () => {
<QTd :props="props">
<span class="link">
{{ props.value }}
<TicketDescriptorProxy :id="props?.row?.id" />
</span>
<TicketDescriptorProxy :id="props?.row?.id" />
</QTd>
</template>
<template #body-cell-client="props">

View File

@ -103,8 +103,8 @@ es:
Roadmap: Troncal
ETD date: Fecha ETD
ETD hour: Hora ETD
Tractor plate: Matrícula tractor
Trailer plate: Matrícula trailer
Tractor plate: Matrícula tractora
Trailer plate: Matrícula remolque
Carrier: Transportista
Price: Precio
Driver name: Nombre del conductor

View File

@ -164,8 +164,8 @@ en:
to: To
es:
params:
tractorPlate: Matrícula del tractor
trailerPlate: Matrícula del trailer
tractorPlate: Matrícula tractora
trailerPlate: Matrícula remolque
supplierFk: Transportista
price: Precio
driverName: Nombre del conductor
@ -174,8 +174,8 @@ es:
to: Hasta
From: Desde
To: Hasta
Tractor Plate: Matrícula del tractor
Trailer Plate: Matrícula del trailer
Tractor Plate: Matrícula tractora
Trailer Plate: Matrícula remolque
Carrier: Transportista
Price: Precio
Driver name: Nombre del conductor

View File

@ -65,9 +65,10 @@ const updateDefaultStop = (data) => {
</div>
</QCardSection>
<QCardSection>
<QIcon
name="add"
size="sm"
<QBtn
flat
icon="add"
shortcut="+"
class="cursor-pointer"
color="primary"
@click="roadmapStopsCrudRef.insert()"
@ -75,7 +76,7 @@ const updateDefaultStop = (data) => {
<QTooltip>
{{ t('Add stop') }}
</QTooltip>
</QIcon>
</QBtn>
</QCardSection>
</QCard>
</template>

View File

@ -149,8 +149,8 @@ const filter = {
<i18n>
es:
Carrier: Transportista
Tractor Plate: Matrícula tractor
Trailer Plate: Matrícula trailer
Tractor Plate: Matrícula tractora
Trailer Plate: Matrícula remolque
Phone: Teléfono
Worker: Trabajador
Observations: Observaciones

View File

@ -0,0 +1,360 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useQuasar } from 'quasar';
import { toDate } from 'src/filters';
import { useRouter } from 'vue-router';
import { usePrintService } from 'src/composables/usePrintService';
import axios from 'axios';
import RouteSearchbar from 'pages/Route/Card/RouteSearchbar.vue';
import RouteListTicketsDialog from 'pages/Route/Card/RouteListTicketsDialog.vue';
import RouteSummary from 'pages/Route/Card/RouteSummary.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import RouteFilter from 'pages/Route/Card/RouteFilter.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnTable from 'components/VnTable/VnTable.vue';
const { openReport } = usePrintService();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const quasar = useQuasar();
const selectedRows = ref([]);
const tableRef = ref([]);
const confirmationDialog = ref(false);
const startingDate = ref(null);
const router = useRouter();
const routeFilter = {
include: [
{
relation: 'workers',
scope: {
fields: ['id', 'firstName'],
},
},
],
};
const columns = computed(() => [
{
align: 'left',
name: 'id',
label: 'Id',
chip: {
condition: () => true,
},
isId: true,
columnFilter: false,
},
{
align: 'left',
name: 'workerFk',
label: t('route.Worker'),
create: true,
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
useLike: false,
optionFilter: 'firstName',
find: {
value: 'workerFk',
label: 'workerUserName',
},
},
columnFilter: {
inWhere: true,
},
useLike: false,
cardVisible: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef),
},
{
align: 'left',
name: 'agencyModeFk',
label: t('route.Agency'),
isTitle: true,
cardVisible: true,
create: true,
component: 'select',
attrs: {
url: 'agencyModes',
fields: ['id', 'name'],
find: {
value: 'agencyModeFk',
label: 'agencyName',
},
},
columnClass: 'expand',
},
{
align: 'left',
name: 'vehicleFk',
label: t('route.Vehicle'),
cardVisible: true,
create: true,
component: 'select',
attrs: {
url: 'vehicles',
fields: ['id', 'numberPlate'],
optionLabel: 'numberPlate',
optionFilterValue: 'numberPlate',
find: {
value: 'vehicleFk',
label: 'vehiclePlateNumber',
},
},
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
name: 'created',
label: t('route.Date'),
columnFilter: false,
cardVisible: true,
create: true,
component: 'date',
format: ({ date }) => toDate(date),
},
{
align: 'left',
name: 'from',
label: t('route.From'),
visible: false,
cardVisible: true,
create: true,
component: 'date',
format: ({ date }) => toDate(date),
},
{
align: 'left',
name: 'to',
label: t('route.To'),
visible: false,
cardVisible: true,
create: true,
component: 'date',
format: ({ date }) => toDate(date),
},
{
align: 'center',
name: 'm3',
label: 'm3',
cardVisible: true,
columnClass: 'shrink',
},
{
align: 'left',
name: 'started',
label: t('route.hourStarted'),
component: 'time',
columnFilter: false,
},
{
align: 'left',
name: 'finished',
label: t('route.hourFinished'),
component: 'time',
columnFilter: false,
},
{
align: 'center',
name: 'kmStart',
label: t('route.KmStart'),
columnClass: 'shrink',
create: true,
visible: false,
},
{
align: 'center',
name: 'kmEnd',
label: t('route.KmEnd'),
columnClass: 'shrink',
create: true,
visible: false,
},
{
align: 'left',
name: 'description',
label: t('route.Description'),
isTitle: true,
create: true,
component: 'input',
field: 'description',
},
{
align: 'left',
name: 'isOk',
label: t('route.Served'),
component: 'checkbox',
columnFilter: false,
columnClass: 'shrink',
},
{
align: 'right',
name: 'tableActions',
actions: [
{
title: t('route.Add tickets'),
icon: 'vn:ticketAdd',
action: (row) => openTicketsDialog(row?.id),
isPrimary: true,
},
{
title: t('route.components.smartCard.viewSummary'),
icon: 'preview',
action: (row) => viewSummary(row?.id, RouteSummary),
isPrimary: true,
},
{
title: t('route.Route summary'),
icon: 'arrow_forward',
action: (row) => navigate(row?.id),
isPrimary: true,
},
],
},
]);
function navigate(id) {
router.push({ path: `/route/${id}` });
}
const cloneRoutes = () => {
if (!selectedRows.value.length || !startingDate.value) return;
axios.post('Routes/clone', {
created: startingDate.value,
ids: selectedRows.value.map((row) => row?.id),
});
startingDate.value = null;
tableRef.value.reload();
};
const showRouteReport = () => {
const ids = selectedRows.value.map((row) => row?.id);
const idString = ids.join(',');
let url = `Routes/${idString}/driver-route-pdf`;
let params = {};
if (selectedRows.value.length >= 1) {
params = {
id: idString,
};
url = `Routes/downloadZip`;
}
openReport(url, params, '_blank');
};
function markAsServed() {
selectedRows.value.forEach(async (row) => {
await axios.patch(`Routes/${row?.id}`, { isOk: true });
});
tableRef.value.reload();
startingDate.value = null;
}
const openTicketsDialog = (id) => {
quasar
.dialog({
component: RouteListTicketsDialog,
componentProps: {
id,
},
})
.onOk(() => tableRef.value.reload());
};
</script>
<template>
<RouteSearchbar />
<QDialog v-model="confirmationDialog">
<QCard style="min-width: 350px">
<QCardSection>
<p class="text-h6 q-ma-none">{{ t('route.Select the starting date') }}</p>
</QCardSection>
<QCardSection class="q-pt-none">
<VnInputDate
:label="t('route.Stating date')"
v-model="startingDate"
autofocus
/>
</QCardSection>
<QCardActions align="right">
<QBtn
flat
:label="t('route.Cancel')"
v-close-popup
class="text-primary"
/>
<QBtn color="primary" v-close-popup @click="cloneRoutes">
{{ t('globals.clone') }}
</QBtn>
</QCardActions>
</QCard>
</QDialog>
<VnSubToolbar />
<RightMenu>
<template #right-panel>
<RouteFilter data-key="RouteList" />
</template>
</RightMenu>
<VnTable
class="route-list"
ref="tableRef"
data-key="RouteList"
url="Routes/filter"
:columns="columns"
:right-search="false"
:is-editable="true"
:filter="routeFilter"
redirect="route"
:row-click="false"
:create="{
urlCreate: 'Routes',
title: t('route.createRoute'),
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {},
}"
save-url="Routes/crud"
:disable-option="{ card: true }"
table-height="85vh"
v-model:selected="selectedRows"
:table="{
'row-key': 'id',
selection: 'multiple',
}"
>
<template #moreBeforeActions>
<QBtn
icon="vn:clone"
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="confirmationDialog = true"
>
<QTooltip>{{ t('route.Clone Selected Routes') }}</QTooltip>
</QBtn>
<QBtn
icon="cloud_download"
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="showRouteReport"
>
<QTooltip>{{ t('route.Download selected routes as PDF') }}</QTooltip>
</QBtn>
<QBtn
icon="check"
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="markAsServed()"
>
<QTooltip>{{ t('route.Mark as served') }}</QTooltip>
</QBtn>
</template>
</VnTable>
</template>

View File

@ -1,34 +1,19 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useSession } from 'composables/useSession';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useQuasar } from 'quasar';
import { toDate } from 'src/filters';
import { useRouter } from 'vue-router';
import { toHour } from 'src/filters';
import axios from 'axios';
import RouteSearchbar from 'pages/Route/Card/RouteSearchbar.vue';
import RouteListTicketsDialog from 'pages/Route/Card/RouteListTicketsDialog.vue';
import RouteSummary from 'pages/Route/Card/RouteSummary.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import RouteFilter from 'pages/Route/Card/RouteFilter.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnTable from 'components/VnTable/VnTable.vue';
import { usePrintService } from 'src/composables/usePrintService';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
const { openReport } = usePrintService();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const quasar = useQuasar();
const session = useSession();
const selectedRows = ref([]);
const tableRef = ref([]);
const confirmationDialog = ref(false);
const startingDate = ref(null);
const router = useRouter();
const routeFilter = {
include: [
{
@ -42,156 +27,70 @@ const routeFilter = {
const columns = computed(() => [
{
align: 'left',
isId: true,
name: 'id',
label: 'Id',
chip: {
condition: () => true,
},
isId: true,
columnFilter: false,
},
{
align: 'left',
name: 'workerFk',
label: t('Worker'),
label: t('route.Worker'),
create: true,
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
useLike: false,
optionFilter: 'firstName',
find: {
value: 'workerFk',
label: 'workerUserName',
},
},
columnFilter: {
inWhere: true,
},
useLike: false,
cardVisible: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef),
},
{
align: 'left',
name: 'agencyModeFk',
label: t('Agency'),
isTitle: true,
cardVisible: true,
create: true,
component: 'select',
attrs: {
url: 'agencyModes',
fields: ['id', 'name'],
find: {
value: 'agencyModeFk',
label: 'agencyName',
},
},
columnClass: 'expand',
},
{
align: 'left',
name: 'vehicleFk',
label: t('Vehicle'),
cardVisible: true,
create: true,
component: 'select',
attrs: {
url: 'vehicles',
fields: ['id', 'numberPlate'],
optionLabel: 'numberPlate',
optionFilterValue: 'numberPlate',
find: {
value: 'vehicleFk',
label: 'vehiclePlateNumber',
},
},
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
name: 'created',
label: t('Date'),
columnFilter: false,
cardVisible: true,
create: true,
component: 'date',
format: ({ date }) => toDate(date),
},
{
align: 'left',
name: 'from',
label: t('From'),
visible: false,
name: 'agencyName',
label: t('route.Agency'),
cardVisible: true,
create: true,
component: 'date',
format: ({ date }) => toDate(date),
columnClass: 'expand',
columnFilter: false,
},
{
align: 'left',
name: 'to',
label: t('To'),
visible: false,
name: 'vehiclePlateNumber',
label: t('route.Vehicle'),
cardVisible: true,
create: true,
component: 'date',
format: ({ date }) => toDate(date),
},
{
align: 'center',
name: 'm3',
label: t('Volume'),
cardVisible: true,
columnClass: 'shrink',
columnFilter: false,
},
{
align: 'left',
name: 'started',
label: t('hourStarted'),
component: 'time',
label: t('route.hourStarted'),
cardVisible: true,
columnFilter: false,
format: (row) => toHour(row.started),
},
{
align: 'left',
name: 'finished',
label: t('hourFinished'),
component: 'time',
label: t('route.hourFinished'),
cardVisible: true,
columnFilter: false,
},
{
align: 'center',
name: 'kmStart',
label: t('KmStart'),
columnClass: 'shrink',
create: true,
visible: false,
},
{
align: 'center',
name: 'kmEnd',
label: t('KmEnd'),
columnClass: 'shrink',
create: true,
visible: false,
format: (row) => toHour(row.started),
},
{
align: 'left',
name: 'description',
label: t('Description'),
label: t('route.Description'),
cardVisible: true,
isTitle: true,
create: true,
component: 'input',
field: 'description',
columnFilter: false,
},
{
align: 'left',
name: 'isOk',
label: t('Served'),
label: t('route.Served'),
component: 'checkbox',
columnFilter: false,
columnClass: 'shrink',
@ -200,212 +99,43 @@ const columns = computed(() => [
align: 'right',
name: 'tableActions',
actions: [
{
title: t('Add tickets'),
icon: 'vn:ticketAdd',
action: (row) => openTicketsDialog(row?.id),
},
{
title: t('components.smartCard.viewSummary'),
icon: 'preview',
action: (row) => viewSummary(row?.id, RouteSummary),
},
{
title: t('Route summary'),
icon: 'arrow_forward',
isPrimary: true,
action: (row) => navigate(row?.id),
},
],
},
]);
function navigate(id) {
router.push({ path: `/route/${id}` });
}
const cloneRoutes = () => {
if (!selectedRows.value.length || !startingDate.value) return;
axios.post('Routes/clone', {
created: startingDate.value,
ids: selectedRows.value.map((row) => row?.id),
});
startingDate.value = null;
tableRef.value.reload();
};
const showRouteReport = () => {
const ids = selectedRows.value.map((row) => row?.id);
const idString = ids.join(',');
let url = `Routes/${idString}/driver-route-pdf`;
let params = {};
if (selectedRows.value.length >= 1) {
params = {
id: idString,
};
url = `Routes/downloadZip`;
}
openReport(url, params, '_blank');
};
function markAsServed() {
selectedRows.value.forEach(async (row) => {
await axios.patch(`Routes/${row?.id}`, { isOk: true });
});
tableRef.value.reload();
startingDate.value = null;
}
const openTicketsDialog = (id) => {
quasar
.dialog({
component: RouteListTicketsDialog,
componentProps: {
id,
},
})
.onOk(() => tableRef.value.reload());
};
</script>
<template>
<RouteSearchbar />
<QDialog v-model="confirmationDialog">
<QCard style="min-width: 350px">
<QCardSection>
<p class="text-h6 q-ma-none">{{ t('Select the starting date') }}</p>
</QCardSection>
<QCardSection class="q-pt-none">
<VnInputDate
:label="t('Stating date')"
v-model="startingDate"
autofocus
/>
</QCardSection>
<QCardActions align="right">
<QBtn flat :label="t('Cancel')" v-close-popup class="text-primary" />
<QBtn color="primary" v-close-popup @click="cloneRoutes">
{{ t('globals.clone') }}
</QBtn>
</QCardActions>
</QCard>
</QDialog>
<VnSubToolbar />
<RightMenu>
<template #right-panel>
<RouteFilter data-key="RouteList" />
</template>
</RightMenu>
<VnTable
class="route-list"
ref="tableRef"
data-key="RouteList"
url="Routes/filter"
:columns="columns"
:right-search="false"
:is-editable="true"
:filter="routeFilter"
redirect="route"
:row-click="false"
:create="{
urlCreate: 'Routes',
title: t('Create route'),
title: t('route.createRoute'),
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {},
}"
save-url="Routes/crud"
:disable-option="{ card: true }"
table-height="85vh"
v-model:selected="selectedRows"
:table="{
'row-key': 'id',
selection: 'multiple',
}"
>
<template #moreBeforeActions>
<QBtn
icon="vn:clone"
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="confirmationDialog = true"
>
<QTooltip>{{ t('Clone Selected Routes') }}</QTooltip>
</QBtn>
<QBtn
icon="cloud_download"
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="showRouteReport"
>
<QTooltip>{{ t('Download selected routes as PDF') }}</QTooltip>
</QBtn>
<QBtn
icon="check"
color="primary"
class="q-mr-sm"
:disable="!selectedRows?.length"
@click="markAsServed()"
>
<QTooltip>{{ t('Mark as served') }}</QTooltip>
</QBtn>
<template #column-workerFk="{ row }">
<span class="link" @click.stop>
{{ row?.workerUserName }}
<WorkerDescriptorProxy :id="row?.workerFk" v-if="row?.workerFk" />
</span>
</template>
</VnTable>
</template>
<style lang="scss" scoped>
.table-input-cell {
max-width: 143px;
}
.route-list {
width: 100%;
max-height: 100%;
}
.table-actions {
gap: 12px;
}
th:last-child,
td:last-child {
background-color: var(--vn-section-color);
position: sticky;
right: 0;
}
</style>
<i18n>
en:
newRoute: New Route
hourStarted: Started hour
hourFinished: Finished hour
es:
From: Desde
To: Hasta
Worker: Trabajador
Agency: Agencia
Vehicle: Vehículo
Volume: Volumen
Date: Fecha
Description: Descripción
Hour started: Hora inicio
Hour finished: Hora fin
KmStart: Km inicio
KmEnd: Km fin
Served: Servida
newRoute: Nueva Ruta
Clone Selected Routes: Clonar rutas seleccionadas
Select the starting date: Seleccione la fecha de inicio
Stating date: Fecha de inicio
Cancel: Cancelar
Mark as served: Marcar como servidas
Download selected routes as PDF: Descargar rutas seleccionadas como PDF
Add ticket: Añadir tickets
Preview: Vista previa
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

@ -87,7 +87,7 @@ const columns = computed(() => [
actions: [
{
title: t('Ver cmr'),
icon: 'visibility',
icon: 'preview',
isPrimary: true,
action: (row) => viewSummary(row?.id, RoadmapSummary),
},
@ -237,4 +237,5 @@ es:
Price: Precio
Observations: Observaciones
Preview: Vista previa
Select the estimated date of departure (ETD): Selecciona la fecha estimada de salida
</i18n>

View File

@ -342,10 +342,7 @@ const openSmsDialog = async () => {
</template>
<template #body-cell-city="{ value, row }">
<QTd auto-width>
<span
class="text-primary cursor-pointer"
@click="goToBuscaman(row)"
>
<span class="link" @click="goToBuscaman(row)">
{{ value }}
<QTooltip>{{ t('Open buscaman') }}</QTooltip>
</span>
@ -353,7 +350,7 @@ const openSmsDialog = async () => {
</template>
<template #body-cell-client="{ value, row }">
<QTd auto-width>
<span class="text-primary cursor-pointer">
<span class="link">
{{ value }}
<CustomerDescriptorProxy :id="row?.clientFk" />
</span>
@ -361,7 +358,7 @@ const openSmsDialog = async () => {
</template>
<template #body-cell-ticket="{ value, row }">
<QTd auto-width class="text-center">
<span class="text-primary cursor-pointer">
<span class="link">
{{ value }}
<TicketDescriptorProxy :id="row?.id" />
</span>
@ -396,7 +393,7 @@ const openSmsDialog = async () => {
</VnPaginate>
</div>
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="openTicketsDialog">
<QBtn fab icon="add" shortcut="+" color="primary" @click="openTicketsDialog">
<QTooltip>
{{ t('Add ticket') }}
</QTooltip>

View File

@ -0,0 +1,25 @@
route:
Worker: Worker
Agency: Agency
Vehicle: Vehicle
Description: Description
hourStarted: H.Start
hourFinished: H.End
createRoute: Create route
From: From
To: To
Date: Date
KmStart: Km start
KmEnd: Km end
Served: Served
Clone Selected Routes: Clone selected routes
Select the starting date: Select the starting date
Stating date: Starting date
Cancel: Cancel
Mark as served: Mark as served
Download selected routes as PDF: Download selected routes as PDF
Add ticket: Add ticket
Preview: Preview
Summary: Summary
Route is closed: Route is closed
Route is not served: Route is not served

View File

@ -0,0 +1,25 @@
route:
Worker: Trabajador
Agency: Agencia
Vehicle: Vehículo
Description: Descripción
hourStarted: H.Inicio
hourFinished: H.Fin
createRoute: Crear ruta
From: Desde
To: Hasta
Date: Fecha
KmStart: Km inicio
KmEnd: Km fin
Served: Servida
Clone Selected Routes: Clonar rutas seleccionadas
Select the starting date: Seleccione la fecha de inicio
Stating date: Fecha de inicio
Cancel: Cancelar
Mark as served: Marcar como servidas
Download selected routes as PDF: Descargar rutas seleccionadas como PDF
Add ticket: Añadir tickets
Preview: Vista previa
Summary: Resumen
Route is closed: La ruta está cerrada
Route is not served: La ruta no está servida

View File

@ -102,9 +102,7 @@ const setWireTransfer = async () => {
<VnInput :label="t('supplier.accounts.iban')" v-model="row.iban">
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>{{
t('components.iban_tooltip')
}}</QTooltip>
<QTooltip>{{ t('components.iban_tooltip') }}</QTooltip>
</QIcon>
</template>
</VnInput>
@ -115,6 +113,7 @@ const setWireTransfer = async () => {
option-label="bic"
option-value="id"
hide-selected
:roles-allowed-to-create="['financial']"
>
<template #form>
<CreateBankEntityForm
@ -165,9 +164,10 @@ const setWireTransfer = async () => {
</div>
</VnRow>
<VnRow>
<QIcon
name="add"
size="sm"
<QBtn
flat
icon="add"
shortcut="+"
class="cursor-pointer"
color="primary"
@click="supplierAccountRef.insert()"
@ -175,7 +175,7 @@ const setWireTransfer = async () => {
<QTooltip>
{{ t('Add account') }}
</QTooltip>
</QIcon>
</QBtn>
</VnRow>
</QCard>
</template>

View File

@ -84,9 +84,10 @@ const insertRow = () => {
</VnRow>
</QCardSection>
<VnRow>
<QIcon
name="add"
size="sm"
<QBtn
flat
icon="add"
shortcut="+"
class="cursor-pointer"
color="primary"
@click="insertRow()"
@ -94,7 +95,7 @@ const insertRow = () => {
<QTooltip>
{{ t('Add contact') }}
</QTooltip>
</QIcon>
</QBtn>
</VnRow>
</QCard>
</template>

View File

@ -109,7 +109,7 @@ const getEntryQueryParams = (supplier) => {
:subtitle="data.subtitle"
:filter="filter"
@on-fetch="setData"
data-key="supplier"
data-key="supplierDescriptor"
:summary="$props.summary"
>
<template #body="{ entity }">

View File

@ -75,6 +75,19 @@ const columns = computed(() => [
},
visible: false,
},
{
align: 'left',
label: t('supplier.list.tableVisibleColumns.country'),
name: 'country',
columnFilter: {
component: 'select',
name: 'countryFk',
attrs: {
url: 'countries',
fields: ['id', 'name'],
},
},
},
]);
</script>

View File

@ -4,7 +4,6 @@ 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';
@ -115,7 +114,7 @@ const totalNewPrice = computed(() => {
const totalDifference = computed(() => {
return rows.value.reduce((acc, item) => acc + item.component?.difference || 0, 0);
});
const showMovablecolumn = computed(() => (haveDifferences.value > 0 ? ['movable'] : []));
const showMovableColumn = computed(() => (haveDifferences.value > 0 ? ['movable'] : []));
const haveDifferences = computed(() => _ticketData.value.sale?.haveDifferences);
const ticketHaveNegatives = () => {
let _haveNegatives = false;
@ -145,85 +144,83 @@ onUnmounted(() => (stateStore.rightDrawer = false));
@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"
<QDrawer side="right" :width="265" v-model="stateStore.rightDrawer">
<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-left" horizontal>
<span>
{{ t('basicData.price') }}:
{{ toCurrency(totalPrice) }}
</span>
</QCardSection>
<QCardSection class="column items-left" horizontal>
<span>
{{ t('basicData.newPrice') }}: {{ toCurrency(totalNewPrice) }}
</span>
</QCardSection>
<QCardSection class="column items-left" 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
>
<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>
<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>
</QDrawer>
<QTable
:visible-columns="showMovablecolumn"
:visible-columns="showMovableColumn"
:rows="rows"
:columns="columns"
row-key="id"
@ -233,15 +230,15 @@ onUnmounted(() => (stateStore.rightDrawer = false));
flat
>
<template #body-cell-item="{ row }">
<QTd>
<QBtn flat color="primary">
<QTd @click.stop class="link">
<QBtn flat>
{{ row.itemFk }}
<ItemDescriptorProxy :id="row.itemFk" />
</QBtn>
</QTd>
</template>
<template #body-cell-description="{ row }">
<QTd>
<QTd style="display: contents">
<div class="column">
<span>{{ row.item.name }}</span>
<span class="color-vn-label">{{ row.item.subName }}</span>

View File

@ -12,6 +12,7 @@ import VnInputTime from 'components/common/VnInputTime.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import { useValidator } from 'src/composables/useValidator';
import { toTimeFormat } from 'filters/date.js';
const $props = defineProps({
@ -23,7 +24,7 @@ const $props = defineProps({
});
const emit = defineEmits(['updateForm']);
const { validate } = useValidator();
const { notify } = useNotify();
const router = useRouter();
const { t } = useI18n();
@ -51,18 +52,18 @@ const agencyByWarehouseFilter = computed(() => ({
},
}));
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,
}
: {},
}));
function zoneWhere() {
if (formData?.value?.agencyModeFk) {
return formData.value?.agencyModeFk
? {
shipped: formData.value?.shipped,
addressFk: formData.value?.addressFk,
agencyModeFk: formData.value?.agencyModeFk,
warehouseFk: formData.value?.warehouseFk,
}
: {};
}
}
const getLanded = async (params) => {
try {
@ -293,13 +294,6 @@ onMounted(() => onFormModelInit());
@on-fetch="(data) => (agenciesOptions = data)"
auto-load
/>
<FetchData
ref="zonesFetchRef"
url="Zones/includingExpired"
:filter="zonesFilter"
@on-fetch="(data) => (zonesOptions = data)"
auto-load
/>
<QForm>
<VnRow>
<VnSelect
@ -313,6 +307,7 @@ onMounted(() => onFormModelInit());
hide-selected
map-options
:required="true"
:rules="validate('basicData.client')"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
@ -333,6 +328,7 @@ onMounted(() => onFormModelInit());
hide-selected
map-options
:required="true"
:rules="validate('basicData.warehouse')"
/>
</VnRow>
<VnRow>
@ -345,6 +341,7 @@ onMounted(() => onFormModelInit());
hide-selected
map-options
:required="true"
:rules="validate('basicData.address')"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
@ -392,6 +389,7 @@ onMounted(() => onFormModelInit());
:label="t('basicData.alias')"
v-model="formData.nickname"
:required="true"
:rules="validate('basicData.alias')"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md no-wrap">
@ -404,6 +402,7 @@ onMounted(() => onFormModelInit());
hide-selected
map-options
:required="true"
:rules="validate('basicData.company')"
/>
<VnSelect
:label="t('basicData.agency')"
@ -414,17 +413,22 @@ onMounted(() => onFormModelInit());
hide-selected
map-options
@focus="agencyFetchRef.fetch()"
:rules="validate('basicData.agency')"
/>
<VnSelect
:label="t('basicData.zone')"
v-model="zoneId"
option-value="id"
option-label="name"
:options="zonesOptions"
url="Zones/includingExpired"
:fields="['id', 'name']"
sort-by="id"
:where="zoneWhere"
hide-selected
map-options
:required="true"
@focus="zonesFetchRef.fetch()"
:rules="validate('basicData.zone')"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
@ -444,16 +448,19 @@ onMounted(() => onFormModelInit());
:label="t('basicData.shipped')"
v-model="formData.shipped"
:required="true"
:rules="validate('basicData.shipped')"
/>
<VnInputTime
:label="t('basicData.shippedHour')"
v-model="formData.shipped"
:required="true"
:rules="validate('basicData.shippedHour')"
/>
<VnInputDate
:label="t('basicData.landed')"
v-model="formData.landed"
:required="true"
:rules="validate('basicData.landed')"
/>
</VnRow>
</QForm>

View File

@ -3,7 +3,7 @@ import { ref, onBeforeMount } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import BasicDataTable from './BasicDataTable.vue';
import TicketBasicData from './TicketBasicData.vue';
import TicketBasicDataForm from './TicketBasicDataForm.vue';
import { useVnConfirm } from 'composables/useVnConfirm';
@ -158,7 +158,10 @@ onBeforeMount(async () => await getTicketData());
color="primary"
animated
keep-alive
style="max-width: 800px; margin: auto"
style="margin: auto"
:style="{
'max-width': step > 1 ? 'none' : '800px',
}"
>
<QStep :name="1" :title="t('globals.pageTitles.basicData')" :done="step > 1">
<TicketBasicDataForm
@ -168,7 +171,7 @@ onBeforeMount(async () => await getTicketData());
/>
</QStep>
<QStep :name="2" :title="t('basicData.priceDifference')">
<BasicDataTable
<TicketBasicData
:form-data="formData"
v-model:haveNegatives="haveNegatives"
@update-form="($event) => (formData = $event)"

View File

@ -31,6 +31,7 @@ const router = useRouter();
const { notify } = useNotify();
const newTicketFormData = reactive({});
const date = new Date();
const createTicket = async () => {
try {
@ -64,7 +65,11 @@ const createTicket = async () => {
>
<template #form-inputs="{ data }">
<VnRow>
<VnInputDate :label="t('expedition.landed')" v-model="data.landed" />
<VnInputDate
:label="t('expedition.landed')"
v-model="data.landed"
:model-value="date"
/>
</VnRow>
<VnRow>
<VnInput

View File

@ -5,15 +5,16 @@ import { useRoute } from 'vue-router';
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 ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue';
import VnImg from 'src/components/ui/VnImg.vue';
import { useStateStore } from 'stores/useStateStore';
import { dashIfEmpty } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
import { toCurrency } from 'filters/index';
import axios from 'axios';
import VnTable from 'src/components/VnTable/VnTable.vue';
const route = useRoute();
const stateStore = useStateStore();
@ -75,22 +76,39 @@ const columns = computed(() => [
name: 'item',
align: 'left',
},
{
align: 'left',
label: t('lines.image'),
name: 'image',
columnField: {
component: VnImg,
attrs: (id) => {
return {
id,
width: '50px',
};
},
},
columnFilter: false,
},
{
label: t('ticketComponents.description'),
name: 'description',
align: 'left',
columnClass: 'expand',
},
{
label: t('ticketComponents.quantity'),
name: 'quantity',
field: 'quantity',
align: 'left',
format: (val) => dashIfEmpty(val),
format: (row) => dashIfEmpty(row.quantity),
},
{
label: t('ticketComponents.serie'),
name: 'serie',
align: 'left',
format: (row) => dashIfEmpty(row.serie),
},
{
label: t('ticketComponents.components'),
@ -174,181 +192,166 @@ onUnmounted(() => (stateStore.rightDrawer = false));
@on-fetch="(data) => (components = data)"
auto-load
/>
<RightMenu>
<template #right-panel>
<QCard
class="q-pa-sm color-vn-text"
bordered
flat
style="border-color: black"
<QDrawer side="right" :width="265" v-model="stateStore.rightDrawer">
<QCard class="q-pa-sm color-vn-text" bordered flat style="border-color: black">
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('ticketComponents.total') }}
</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label"
>{{ t('ticketComponents.baseToCommission') }}:
</span>
<span>{{ toCurrency(getBase) }}</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label"
>{{ t('ticketComponents.totalWithoutVat') }}:
</span>
<span>{{ toCurrency(getTotal) }}</span>
</QCardSection>
</QCard>
<QCard class="q-pa-sm color-vn-text" bordered flat style="border-color: black">
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('ticketComponents.components') }}
</span>
</QCardSection>
<QCardSection
v-for="(component, index) in componentsList"
:key="index"
horizontal
>
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('ticketComponents.total') }}
</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label"
>{{ t('ticketComponents.baseToCommission') }}:
</span>
<span>{{ toCurrency(getBase) }}</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label"
>{{ t('ticketComponents.totalWithoutVat') }}:
</span>
<span>{{ toCurrency(getTotal) }}</span>
</QCardSection>
</QCard>
<QCard
class="q-pa-sm color-vn-text"
bordered
flat
style="border-color: black"
>
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('ticketComponents.components') }}
</span>
</QCardSection>
<QCardSection
v-for="(component, index) in componentsList"
:key="index"
horizontal
>
<span v-if="component.name" class="q-mr-xs color-vn-label">
{{ component.name }}:
</span>
<span v-if="component.value">{{
toCurrency(component.value, 'EUR', 3)
}}</span>
</QCardSection>
</QCard>
<QCard
class="q-pa-sm color-vn-text"
bordered
flat
style="border-color: black"
>
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('ticketComponents.zoneBreakdown') }}
</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('ticketComponents.price') }}:
</span>
<span>{{ toCurrency(ticketData?.zonePrice, 'EUR', 2) }}</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('ticketComponents.bonus') }}:
</span>
<span>{{ toCurrency(ticketData?.zoneBonus, 'EUR', 2) }}</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('ticketComponents.zone') }}:
</span>
<span class="link">
{{ dashIfEmpty(ticketData?.zone?.name) }}
<ZoneDescriptorProxy :id="ticketData?.zone?.id" />
</span>
</QCardSection>
<QCardSection v-if="ticketData?.zone?.isVolumetric" horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('ticketComponents.volume') }}:
</span>
<span>{{ ticketVolume }}</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('ticketComponents.packages') }}:
</span>
<span>{{ dashIfEmpty(ticketData?.packages) }}</span>
</QCardSection>
</QCard>
<QCard
class="q-pa-sm color-vn-text"
bordered
flat
style="border-color: black"
>
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('ticketComponents.theoricalCost') }}
</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('ticketComponents.totalPrice') }}:
</span>
<span>{{ toCurrency(theoricalCost, 'EUR', 2) }}</span>
</QCardSection>
</QCard>
</template>
</RightMenu>
<QTable
:rows="components"
<span v-if="component.name" class="q-mr-xs color-vn-label">
{{ component.name }}:
</span>
<span v-if="component.value">{{
toCurrency(component.value, 'EUR', 3)
}}</span>
</QCardSection>
</QCard>
<QCard class="q-pa-sm color-vn-text" bordered flat style="border-color: black">
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('ticketComponents.zoneBreakdown') }}
</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('ticketComponents.price') }}:
</span>
<span>{{ toCurrency(ticketData?.zonePrice, 'EUR', 2) }}</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('ticketComponents.bonus') }}:
</span>
<span>{{ toCurrency(ticketData?.zoneBonus, 'EUR', 2) }}</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('ticketComponents.zone') }}:
</span>
<span class="link">
{{ dashIfEmpty(ticketData?.zone?.name) }}
<ZoneDescriptorProxy :id="ticketData?.zone?.id" />
</span>
</QCardSection>
<QCardSection v-if="ticketData?.zone?.isVolumetric" horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('ticketComponents.volume') }}:
</span>
<span>{{ ticketVolume }}</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('ticketComponents.packages') }}:
</span>
<span>{{ dashIfEmpty(ticketData?.packages) }}</span>
</QCardSection>
</QCard>
<QCard class="q-pa-sm color-vn-text" bordered flat style="border-color: black">
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('ticketComponents.theoricalCost') }}
</span>
</QCardSection>
<QCardSection horizontal>
<span class="q-mr-xs color-vn-label">
{{ t('ticketComponents.totalPrice') }}:
</span>
<span>{{ toCurrency(theoricalCost, 'EUR', 2) }}</span>
</QCardSection>
</QCard>
</QDrawer>
<VnTable
ref="tableRef"
data-key="TicketComponents"
url="Sales"
:user-filter="salesFilter"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
:right-search="false"
auto-load
:disable-option="{ card: true }"
:column-search="false"
>
<template #body-cell-item="{ row }">
<QTd>
<QBtn flat color="primary">
<span class="link">{{ row.itemFk }}</span>
<ItemDescriptorProxy :id="row.itemFk" />
</QBtn>
</QTd>
<template #column-item="{ row }">
<span @click.stop flat class="link">
{{ row.itemFk }}
<ItemDescriptorProxy :id="row.itemFk" />
</span>
</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" />
</div>
</QTd>
<template #column-image="{ row }">
<div class="image-wrapper">
<VnImg :id="parseInt(row?.item?.id)" class="rounded" />
</div>
</template>
<template #body-cell-serie="{ row }">
<QTd>
<div class="column">
<span v-for="(saleComponent, index) in row.components" :key="index">
{{ saleComponent.component?.componentType?.name }}
</span>
</div>
</QTd>
<template #column-description="{ row }">
<div class="column">
<span>{{ row.item.name }}</span>
<span class="color-vn-label">{{ row.item.subName }}</span>
<FetchedTags :item="row.item" />
</div>
</template>
<template #body-cell-components="{ row }">
<QTd>
<div class="column">
<span v-for="(saleComponent, index) in row.components" :key="index">
{{ saleComponent.component?.name }}
</span>
</div>
</QTd>
<template #column-serie="{ row }">
<div class="column">
<span v-for="(saleComponent, index) in row.components" :key="index">
{{ saleComponent.component?.componentType?.name }}
</span>
</div>
</template>
<template #body-cell-import="{ row }">
<QTd>
<div class="column text-right">
<span v-for="(saleComponent, index) in row.components" :key="index">
<template #column-components="{ row }">
<div class="column">
<span v-for="(saleComponent, index) in row.components" :key="index">
{{ saleComponent.component?.name }}
</span>
</div>
</template>
<template #column-import="{ row }">
<div class="column text-left">
<span v-for="(saleComponent, index) in row.components" :key="index">
{{ toCurrency(saleComponent.value, 'EUR', 3) }}
</span>
</div>
</template>
<template #column-total="{ row }">
<div class="column">
<span v-for="(saleComponent, index) in row.components" :key="index">
{{ toCurrency(saleComponent.value * row.quantity, 'EUR', 3) }}
<!-- <QTooltip>
{{ saleComponent.component?.name }}:
{{ toCurrency(saleComponent.value, 'EUR', 3) }}
</span>
</div>
</QTd>
</QTooltip> -->
</span>
</div>
</template>
<template #body-cell-total="{ row }">
<QTd>
<div class="column text-right">
<span v-for="(saleComponent, index) in row.components" :key="index">
{{ toCurrency(saleComponent.value * row.quantity, 'EUR', 3) }}
</span>
</div>
</QTd>
</template>
</QTable>
</VnTable>
</template>
<style lang="scss" scoped>
.image-wrapper {
width: 40px;
height: 40px;
}
</style>

View File

@ -2,13 +2,13 @@
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import TicketDescriptorMenu from './TicketDescriptorMenu.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription';
import VnUserLink from 'src/components/ui/VnUserLink.vue';
import { toDateTimeFormat } from 'src/filters/date';
const $props = defineProps({
id: {
@ -30,13 +30,24 @@ const filter = {
{
relation: 'address',
scope: {
fields: ['id', 'name', 'mobile', 'phone'],
fields: ['id', 'name', 'mobile', 'phone', 'incotermsFk'],
},
},
{
relation: 'client',
scope: {
fields: ['id', 'name', 'salesPersonFk', 'phone', 'mobile', 'email'],
fields: [
'id',
'name',
'salesPersonFk',
'phone',
'mobile',
'email',
'isActive',
'isFreezed',
'isTaxDataChecked',
'hasElectronicInvoice',
],
include: [
{
relation: 'user',
@ -87,6 +98,10 @@ const filter = {
};
const data = ref(useCardDescription());
function ticketFilter(ticket) {
return JSON.stringify({ clientFk: ticket.clientFk });
}
</script>
<template>
@ -128,7 +143,10 @@ const data = ref(useCardDescription());
/>
</template>
</VnLv>
<VnLv :label="t('ticket.card.shipped')" :value="toDate(entity.shipped)" />
<VnLv
:label="t('ticket.card.shipped')"
:value="toDateTimeFormat(entity.shipped)"
/>
<VnLv
v-if="entity.agencyMode"
:label="t('ticket.card.agency')"
@ -138,7 +156,39 @@ const data = ref(useCardDescription());
<VnLv :label="t('ticket.card.alias')" :value="entity.nickname" />
</template>
<template #icons="{ entity }">
<QCardActions>
<QCardActions class="q-gutter-x-xs">
<QIcon
v-if="entity.client.isActive == false"
name="vn:disabled"
size="xs"
color="primary"
>
<QTooltip>{{ t('Client inactive') }}</QTooltip>
</QIcon>
<QIcon
v-if="entity.client.isFreezed == true"
name="vn:frozen"
size="xs"
color="primary"
>
<QTooltip>{{ t('Client Frozen') }}</QTooltip>
</QIcon>
<QIcon
v-if="entity.problem.includes('hasRisk')"
name="vn:risk"
size="xs"
color="primary"
>
<QTooltip>{{ t('Client has debt') }}</QTooltip>
</QIcon>
<QIcon
v-if="entity.client.isTaxDataChecked == false"
name="vn:no036"
size="xs"
color="primary"
>
<QTooltip>{{ t('Client not checked') }}</QTooltip>
</QIcon>
<QIcon
v-if="entity.isDeleted == true"
name="vn:deletedTicket"
@ -159,6 +209,27 @@ const data = ref(useCardDescription());
>
<QTooltip>{{ t('ticket.card.customerCard') }}</QTooltip>
</QBtn>
<QBtn
size="md"
icon="vn:ticket"
color="primary"
:to="{ name: 'TicketList', query: { table: ticketFilter(entity) } }"
>
<QTooltip>{{ t('ticket.card.ticketList') }}</QTooltip>
</QBtn>
<QBtn
size="md"
icon="vn:basketadd"
color="primary"
:to="{
name: 'OrderList',
query: {
createForm: JSON.stringify({ clientFk: entity.clientFk }),
},
}"
>
<QTooltip>{{ t('ticket.card.newOrder') }}</QTooltip>
</QBtn>
</QCardActions>
</template>
</CardDescriptor>
@ -168,4 +239,8 @@ const data = ref(useCardDescription());
es:
This ticket is deleted: Este ticket está eliminado
Go to module index: Ir al índice del modulo
Client inactive: Cliente inactivo
Client not checked: Cliente no verificado
Client has debt: Cliente con deuda
Client Frozen: Cliente congelado
</i18n>

View File

@ -1,6 +1,6 @@
<script setup>
import axios from 'axios';
import { ref } from 'vue';
import { ref, toRefs } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
@ -9,6 +9,11 @@ import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnSmsDialog from 'components/common/VnSmsDialog.vue';
import toDate from 'filters/toDate';
import FormPopup from 'components/FormPopup.vue';
import VnSelect from 'components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import VnInputTime from 'src/components/common/VnInputTime.vue';
import { useAcl } from 'src/composables/useAcl';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import { useArrayData } from 'src/composables/useArrayData';
@ -24,8 +29,16 @@ const { dialog, notify } = useQuasar();
const { t } = useI18n();
const { openReport, sendEmail } = usePrintService();
const ticketSummary = useArrayData('TicketSummary');
const ticket = ref(props.ticket);
const { ticket } = toRefs(props);
const ticketId = currentRoute.value.params.id;
const client = ref();
const showTransferDialog = ref(false);
const showTurnDialog = ref(false);
const showChangeTimeDialog = ref(false);
const dialogRef = ref();
const isEditable = ref();
const hasInvoicing = useAcl('invoicing');
const hasPdf = ref();
const weight = ref();
const actions = {
clone: async () => {
@ -76,6 +89,7 @@ const actions = {
notify({
message: t('You can undo this action within the first hour'),
icon: 'info',
type: 'warning',
});
push({ name: 'TicketList' });
@ -87,10 +101,14 @@ const actions = {
function openDeliveryNote(type = 'deliveryNote', documentType = 'pdf') {
const path = `Tickets/${ticket.value.id}/delivery-note-${documentType}`;
openReport(path, {
recipientId: ticket.value.clientFk,
type: type,
});
openReport(
path,
{
recipientId: ticket.value.clientFk,
type: type,
},
'_blank'
);
}
function sendDeliveryNoteConfirmation(type = 'deliveryNote', documentType = 'pdf') {
@ -173,34 +191,280 @@ function openConfirmDialog(callback) {
dialog({
component: VnConfirm,
componentProps: {
title: t('This ticket will be removed from current route! Continue anyway?'),
message: t('You are going to delete this ticket'),
promise: actions[callback],
},
});
}
function generatePdfDialog() {
dialog({
component: VnConfirm,
componentProps: {
promise: generatePdfInvoice,
},
});
}
async function generatePdfInvoice() {
const { data } = await axios.get('invoiceOuts', {
params: {
filter: JSON.stringify({
where: { ref: ticket.value.refFk },
}),
},
});
const invoiceId = data[0].id;
const { response } = await axios.post(`InvoiceOuts/${invoiceId}/createPdf`);
if (!response) {
notify({
message: 'The invoice PDF document has been regenerated',
type: 'positive',
});
}
}
function makeInvoiceDialog() {
dialog({
component: VnConfirm,
componentProps: {
title: t('Are you sure you want to invoice this ticket?'),
message: t('You are going to invoice this ticket'),
promise: makeInvoice,
},
});
}
async function makeInvoice() {
const params = {
ticketsIds: [parseInt(ticketId)],
};
await axios.post(`Tickets/invoiceTicketsAndPdf`, params);
notify({
message: t('Ticket invoiced'),
type: 'positive',
});
window.location.reload();
}
async function transferClient(client) {
const params = {
clientFk: client,
};
const { data } = await axios.patch(`Tickets/${ticketId}/transferClient`, params);
if (data) window.location.reload();
}
async function addTurn(day) {
const params = {
ticketFk: parseInt(ticketId),
weekDay: day,
agencyModeFk: ticket.value.agencyModeFk,
};
await axios.patch(`TicketWeeklies`, params);
notify({
message: t('Current ticket deleted and added to shift'),
type: 'positive',
});
window.location.reload();
}
async function createRefund(withWarehouse) {
const params = {
ticketsIds: [parseInt(ticketId)],
withWarehouse: withWarehouse,
};
const { data } = await axios.post(`Tickets/refund`, params);
if (data) {
const refundTicket = data;
push({ name: 'TicketSale', params: { id: refundTicket[0].id } });
}
}
async function changeShippedHour(time) {
const params = {
shipped: time,
};
const { data } = await axios.post(`Tickets/${ticketId}/updateEditableTicket`, params);
if (data) window.location.reload();
}
function openRecalculateDialog() {
dialog({
component: VnConfirm,
componentProps: {
title: t('Recalculate components'),
message: t('Are you sure you want to recalculate components?'),
promise: recalculateComponents,
},
});
}
async function recalculateComponents() {
await axios.post(`Tickets/${ticketId}/recalculateComponents`);
notify({
message: t('Data saved'),
type: 'positive',
});
window.location.reload();
}
const handleFetchData = (data) => {
isEditable.value = data;
handleInvoiceOutData();
};
async function handleInvoiceOutData() {
const { data } = await axios.get(`InvoiceOuts`, {
params: {
filter: JSON.stringify({ where: { ref: ticket.value.refFk } }),
},
});
hasPdf.value = data[0]?.hasPdf;
}
</script>
<template>
<FetchData
:url="`Tickets/${ticketId}/isEditable`"
auto-load
@on-fetch="handleFetchData"
/>
<QItem @click="showTransferDialog = !showTransferDialog" v-ripple clickable>
<QItemSection avatar>
<QIcon name="content_paste" />
</QItemSection>
<QItemSection>{{ t('Transfer client') }}</QItemSection>
</QItem>
<QDialog ref="dialogRef" v-model="showTransferDialog">
<FormPopup
@on-submit="transferClient(client)"
:title="t('Transfer client')"
:custom-submit-button-label="t('Transfer client')"
:default-cancel-button="false"
>
<template #form-inputs>
<VnSelect
url="Clients"
:fields="['id', 'name']"
option-label="name"
option-value="id"
v-model="client"
:label="t('Client')"
auto-load
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ `#${scope.opt.id} - ` }}
{{ scope.opt.name }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</template>
</FormPopup>
</QDialog>
<QItem @click="showTurnDialog = !showTurnDialog" v-ripple clickable>
<QItemSection avatar>
<QIcon name="vn:calendar" />
</QItemSection>
<QItemSection>{{ t('addTurn') }}</QItemSection>
</QItem>
<QDialog ref="dialogRef" v-model="showTurnDialog">
<FormPopup
@on-submit="addTurn"
:title="t('What is the day of receipt of the ticket?')"
:default-submit-button="false"
:default-cancel-button="false"
style="text-align: center"
>
<template #form-inputs>
<QBtnGroup spread>
<QBtn
:label="t('weekdays.mon')"
color="primary"
@click="addTurn(0)"
v-ripple
class="weekdaysBtn"
/>
<QBtn
:label="t('weekdays.tue')"
color="primary"
@click="addTurn(1)"
v-ripple
class="weekdaysBtn"
/>
<QBtn
:label="t('weekdays.wed')"
color="primary"
@click="addTurn(2)"
v-ripple
class="weekdaysBtn"
/>
<QBtn
:label="t('weekdays.thu')"
color="primary"
@click="addTurn(3)"
v-ripple
class="weekdaysBtn"
/>
<QBtn
:label="t('weekdays.fri')"
color="primary"
@click="addTurn(4)"
v-ripple
class="weekdaysBtn"
/>
<QBtn
:label="t('weekdays.sat')"
color="primary"
@click="addTurn(5)"
v-ripple
class="weekdaysBtn"
/>
<QBtn
:label="t('weekdays.sun')"
color="primary"
@click="addTurn(6)"
v-ripple
class="weekdaysBtn"
/>
</QBtnGroup>
</template>
</FormPopup>
</QDialog>
<QItem v-ripple clickable>
<QItemSection avatar>
<QIcon name="picture_as_pdf" />
</QItemSection>
<QItemSection>{{ t('Open Delivery Note...') }}</QItemSection>
<QItemSection>{{ t('Show Delivery Note...') }}</QItemSection>
<QItemSection side>
<QIcon name="keyboard_arrow_right" />
</QItemSection>
<QMenu anchor="top end" self="top start" auto-close bordered>
<QList>
<QItem @click="openDeliveryNote('deliveryNote')" v-ripple clickable>
<QItemSection>{{ t('With prices') }}</QItemSection>
<QItemSection>{{ t('as PDF') }}</QItemSection>
</QItem>
<QItem @click="openDeliveryNote('withoutPrices')" v-ripple clickable>
<QItemSection>{{ t('Without prices') }}</QItemSection>
<QItemSection>{{ t('as PDF without prices') }}</QItemSection>
</QItem>
<QItem
@click="openDeliveryNote('deliveryNote', 'csv')"
v-ripple
clickable
>
<QItemSection>{{ t('As CSV') }}</QItemSection>
<QItemSection>{{ t('as CSV') }}</QItemSection>
</QItem>
</QList>
</QMenu>
@ -220,21 +484,21 @@ function openConfirmDialog(callback) {
v-ripple
clickable
>
<QItemSection>{{ t('With prices') }}</QItemSection>
<QItemSection>{{ t('Send PDF') }}</QItemSection>
</QItem>
<QItem
@click="sendDeliveryNoteConfirmation('withoutPrices')"
v-ripple
clickable
>
<QItemSection>{{ t('Without prices') }}</QItemSection>
<QItemSection>{{ t('Send PDF to tablet') }}</QItemSection>
</QItem>
<QItem
@click="sendDeliveryNoteConfirmation('deliveryNote', 'csv')"
v-ripple
clickable
>
<QItemSection>{{ t('As CSV') }}</QItemSection>
<QItemSection>{{ t('Send CSV') }}</QItemSection>
</QItem>
</QList>
</QMenu>
@ -243,8 +507,26 @@ function openConfirmDialog(callback) {
<QItemSection avatar>
<QIcon name="receipt" />
</QItemSection>
<QItemSection>{{ t('Open Proforma Invoice') }}</QItemSection>
<QItemSection>{{ t('Show Proforma') }}</QItemSection>
</QItem>
<QItem
v-if="isEditable"
@click="showChangeTimeDialog = !showChangeTimeDialog"
v-ripple
clickable
>
<QItemSection avatar>
<QIcon name="schedule" />
</QItemSection>
<QItemSection>{{ t('Change shipped hour') }}</QItemSection>
</QItem>
<QDialog ref="dialogRef" v-model="showChangeTimeDialog">
<FormPopup @on-submit="changeShippedHour(time)" :title="t('Change shipped hour')">
<template #form-inputs>
<VnInputTime v-model="time" :label="t('Shipped hour')" clearable />
</template>
</FormPopup>
</QDialog>
<QItem v-ripple clickable>
<QItemSection avatar>
<QIcon name="sms" />
@ -259,24 +541,72 @@ function openConfirmDialog(callback) {
<QItemSection>{{ t('Pending payment') }}</QItemSection>
</QItem>
<QItem @click="showSmsDialog('minAmount')" v-ripple clickable>
<QItemSection>{{ t('Minimum amount') }}</QItemSection>
<QItemSection>{{ t('Minimum import') }}</QItemSection>
</QItem>
<QItem
@click="showSmsDialogWithChanges('orderChanges')"
v-ripple
clickable
>
<QItemSection>{{ t('Order changes') }}</QItemSection>
<QItemSection>{{ t('Notify changes') }}</QItemSection>
</QItem>
</QList>
</QMenu>
</QItem>
<QItem @click="makeInvoiceDialog()" v-ripple clickable v-if="isEditable">
<QItemSection avatar>
<QIcon name="picture_as_pdf" />
</QItemSection>
<QItemSection>{{ t('Make invoice') }}</QItemSection>
</QItem>
<QItem
@click="generatePdfDialog()"
v-ripple
clickable
v-if="ticket.refFk !== null && (hasInvoicing || hasPdf)"
>
<QItemSection avatar>
<QIcon name="picture_as_pdf" />
</QItemSection>
<QItemSection>{{
hasPdf ? t('Regenerate PDF invoice') : t('Generate PDF invoice')
}}</QItemSection>
</QItem>
<QItem @click="openConfirmDialog('clone')" v-ripple clickable>
<QItemSection avatar>
<QIcon name="content_copy" />
</QItemSection>
<QItemSection>{{ t('To clone ticket') }}</QItemSection>
</QItem>
<QItem v-if="isEditable" @click="openRecalculateDialog()" v-ripple clickable>
<QItemSection avatar>
<QIcon name="refresh" />
</QItemSection>
<QItemSection>{{ t('Recalculate components') }}</QItemSection>
</QItem>
<QItem v-ripple clickable>
<QItemSection avatar>
<QIcon name="monetization_on" />
</QItemSection>
<QItemSection>{{ t('Refund all...') }}</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>
<QItem @click="$refs.weightDialog.show()" v-ripple clickable>
<QItemSection avatar>
<QIcon name="weight" />
@ -307,29 +637,61 @@ function openConfirmDialog(callback) {
</template>
</VnConfirm>
</template>
<style lang="scss" scoped>
.weekdaysBtn {
margin: 1%;
}
</style>
<i18n>
en:
addTurn: Add turn
invoiceIds: "Invoices have been generated with the following ids: {invoiceIds}"
es:
Open Delivery Note...: Abrir albarán...
Show Delivery Note...: Ver albarán...
Send Delivery Note...: Enviar albarán...
With prices: Con precios
Without prices: Sin precios
As CSV: Como CSV
Open Proforma Invoice: Abrir factura proforma
as PDF: como PDF
as PDF without prices: como PDF sin precios
as CSV: Como CSV
Send PDF: Enviar PDF
Send PDF to tablet: Enviar PDF a tablet
Send CSV: Enviar CSV
Show Proforma: Ver proforma
Delete ticket: Eliminar ticket
Send SMS...: Enviar SMS
Send SMS...: Enviar SMS...
Pending payment: Pago pendiente
Minimum amount: Importe mínimo
Order changes: Cambios del pedido
Minimum import: Importe mínimo
Notify changes: Notificar cambios
Ticket deleted: Ticket eliminado
You can undo this action within the first hour: Puedes deshacer esta acción dentro de la primera hora
To clone ticket: Clonar ticket
Ticket cloned: Ticked clonado
It was not able to clone the ticket: No se pudo clonar el ticket
Generate PDF invoice: Generar PDF factura
Regenerate PDF invoice: Regenerar PDF factura
The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado
Transfer client: Transferir cliente
Client: Cliente
addTurn: Añadir a turno
What is the day of receipt of the ticket?: ¿Cuál es el día de preparación del pedido?
Current ticket deleted and added to shift: Ticket actual eliminado y añadido al turno
Refund all...: Abonar todo...
with warehouse: con almacén
without warehouse: sin almacén
Make invoice: Crear factura
Change shipped hour: Cambiar hora de envío
Shipped hour: Hora de envío
Recalculate components: Recalcular componentes
Are you sure you want to recalculate components?: ¿Seguro que quieres recalcular los componentes?
Data saved: Datos guardados
Are you sure you want to invoice this ticket?: ¿Seguro que quieres facturar este ticket?
You are going to invoice this ticket: Vas a facturar este ticket
Ticket invoiced: Ticket facturado
Set weight: Establecer peso
Weight set: Peso establecido
This ticket may be invoiced, do you want to continue?: Es posible que se facture este ticket, desea continuar?
invoiceIds: "Se han generado las facturas con los siguientes ids: {invoiceIds}"
This ticket will be removed from current route! Continue anyway?: ¡Se eliminará el ticket de la ruta actual! ¿Continuar de todas formas?
You are going to delete this ticket: Vas a eliminar este ticket
</i18n>

View File

@ -1,39 +1,35 @@
<script setup>
import { onMounted, ref, computed, onUnmounted, reactive, watch } from 'vue';
import { onMounted, ref, computed, onUnmounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnInput from 'src/components/common/VnInput.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import TicketEditManaProxy from './TicketEditMana.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import ExpeditionNewTicket from './ExpeditionNewTicket.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency, toPercentage } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
import { useVnConfirm } from 'composables/useVnConfirm';
import useNotify from 'src/composables/useNotify.js';
import { toDateTimeFormat } from 'src/filters/date';
import axios from 'axios';
import VnTable from 'src/components/VnTable/VnTable.vue';
const route = useRoute();
const stateStore = useStateStore();
const { t } = useI18n();
const { notify } = useNotify();
const { openConfirmationModal } = useVnConfirm();
const editPriceProxyRef = ref(null);
const newTicketDialogRef = ref(null);
const logsTableDialogRef = ref(null);
const tableRef = ref();
const expeditionsLogsData = ref([]);
const selectedExpeditions = ref([]);
const allColumnNames = ref([]);
const visibleColumns = ref([]);
const newTicketWithRoute = ref(false);
const selectedRows = ref([]);
const hasSelectedRows = computed(() => selectedRows.value.length > 0);
const exprBuilder = (param, value) => {
switch (param) {
@ -54,8 +50,6 @@ const expeditionsArrayData = useArrayData('ticketExpeditions', {
filter: expeditionsFilter.value,
exprBuilder: exprBuilder,
});
const expeditionsStore = expeditionsArrayData.store;
const ticketExpeditions = computed(() => expeditionsStore.data);
const ticketArrayData = useArrayData('ticketData');
const ticketStore = ticketArrayData.store;
@ -73,129 +67,87 @@ watch(
{ immediate: true }
);
const params = reactive({});
const applyColumnFilter = async (col) => {
try {
const paramKey = col.columnFilter?.filterParamKey || col.field;
params[paramKey] = col.columnFilter.filterValue;
await expeditionsArrayData.addFilter({ filter: expeditionsFilter.value, params });
} catch (err) {
console.error('Error applying column filter', err);
}
};
const getInputEvents = (col) => {
return col.columnFilter.type === 'select'
? { 'update:modelValue': () => applyColumnFilter(col) }
: {
'keyup.enter': () => applyColumnFilter(col),
};
};
const columns = computed(() => [
{
align: 'left',
label: t('expedition.id'),
name: 'id',
field: 'id',
align: 'left',
sortable: true,
chip: {
condition: () => true,
},
isId: true,
columnFilter: {
component: VnInput,
type: 'text',
filterParamKey: 'expeditionFk',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
inWhere: true,
},
},
{
label: t('expedition.item'),
name: 'item',
name: 'packagingItemFk',
align: 'left',
cardVisible: true,
columnFilter: {
component: VnInput,
type: 'text',
filterParamKey: 'packageItemName',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
inWhere: true,
},
},
{
label: t('expedition.name'),
name: 'name',
field: 'packageItemName',
name: 'packageItemName',
align: 'left',
isTitle: true,
columnFilter: {
component: VnSelect,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
url: 'Items',
fields: ['id', 'name'],
'sort-by': 'name ASC',
'option-value': 'id',
'option-label': 'name',
dense: true,
},
inWhere: true,
},
},
{
label: t('expedition.packageType'),
name: 'packageType',
field: 'freightItemName',
name: 'freightItemName',
align: 'left',
columnFilter: {
component: VnInput,
type: 'text',
// filterParamKey: 'expeditionFk',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
inWhere: true,
},
},
{
label: t('expedition.counter'),
name: 'counter',
field: 'counter',
align: 'left',
columnFilter: null,
columnFilter: {
inWhere: true,
},
},
{
label: t('expedition.externalId'),
name: 'externalId',
field: 'externalId',
align: 'left',
columnFilter: null,
cardVisible: true,
columnFilter: {
inWhere: true,
},
},
{
label: t('expedition.created'),
name: 'created',
field: 'created',
align: 'left',
columnFilter: null,
format: (value) => toDateTimeFormat(value),
cardVisible: true,
format: (row) => toDateTimeFormat(row.created),
},
{
label: t('expedition.state'),
name: 'state',
field: 'state',
align: 'left',
columnFilter: null,
cardVisible: true,
columnFilter: { inWhere: true },
},
{
label: '',
name: 'history',
align: 'left',
columnFilter: null,
align: 'right',
name: 'tableActions',
actions: [
{
title: t('expedition.historyAction'),
icon: 'history',
isPrimary: true,
action: (row) => showLog(row),
},
],
},
]);
@ -204,23 +156,29 @@ const logTableColumns = computed(() => [
label: t('expedition.state'),
name: 'state',
field: 'state',
align: 'left',
align: 'center',
sortable: true,
},
{
label: t('expedition.name'),
name: 'name',
align: 'name',
field: 'name',
align: 'center',
columnFilter: null,
},
{
label: t('expedition.created'),
name: 'created',
field: 'created',
align: 'left',
columnFilter: null,
align: 'center',
format: (value) => toDateTimeFormat(value),
},
{
label: t('expedition.isScanned'),
name: 'isScanned',
field: 'isScanned',
align: 'center',
},
]);
const showNewTicketDialog = (withRoute = false) => {
@ -255,10 +213,20 @@ const getExpeditionState = async (expedition) => {
order: ['created DESC'],
};
const { data } = await axios.get(`ExpeditionStates/filter`, {
const { data: expeditionStates } = await axios.get(`ExpeditionStates/filter`, {
params: { filter: JSON.stringify(filter) },
});
expeditionsLogsData.value = data;
const { data: scannedStates } = await axios.get(`ExpeditionStates`, {
params: { filter: JSON.stringify(filter), fields: ['id', 'isScanned'] },
});
expeditionsLogsData.value = expeditionStates.map((state) => {
const scannedState = scannedStates.find((s) => s.id === state.id);
return {
...state,
isScanned: scannedState ? scannedState.isScanned : false,
};
});
} catch (error) {
console.error(error);
}
@ -275,21 +243,13 @@ onUnmounted(() => (stateStore.rightDrawer = false));
<template>
<VnSubToolbar>
<template #st-data>
<TableVisibleColumns
:all-columns="allColumnNames"
table-code="expeditionIndex"
labels-traductions-path="expedition"
@on-config-saved="visibleColumns = [...$event, 'history']"
/>
</template>
<template #st-actions>
<QBtnGroup push class="q-gutter-x-sm" flat>
<QBtnDropdown
ref="btnDropdownRef"
color="primary"
:label="t('expedition.move')"
:disable="!selectedExpeditions.length"
:disable="!hasSelectedRows"
>
<template #label>
<QTooltip>{{ t('Select lines to see the options') }}</QTooltip>
@ -322,7 +282,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</QList>
</QBtnDropdown>
<QBtn
:disable="!selectedExpeditions.length"
:disable="!hasSelectedRows"
icon="delete"
color="primary"
@click="
@ -332,115 +292,34 @@ onUnmounted(() => (stateStore.rightDrawer = false));
deleteExpedition
)
"
/>
>
<QTooltip>{{ t('expedition.removeExpedition') }}</QTooltip>
</QBtn>
</QBtnGroup>
</template>
</VnSubToolbar>
<QTable
:rows="ticketExpeditions"
<VnTable
ref="tableRef"
data-key="TicketExpedition"
url="Expeditions/filter"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
selection="multiple"
v-model:selected="selectedExpeditions"
:visible-columns="visibleColumns"
:no-data-label="t('globals.noResults')"
:filter="expeditionsFilter"
v-model:selected="selectedRows"
:table="{
'row-key': 'id',
selection: 'multiple',
}"
auto-load
order="created DESC"
>
<template #top-row="{ cols }">
<QTr>
<QTd />
<QTd v-for="(col, index) in cols" :key="index" style="max-width: 100px">
<component
:is="col.columnFilter.component"
v-if="col.columnFilter"
v-model="col.columnFilter.filterValue"
v-bind="col.columnFilter.attrs"
v-on="col.columnFilter.event(col)"
dense
/>
</QTd>
</QTr>
</template>
<template #body-cell-item="{ row }">
<QTd auto-width @click.stop>
<QBtn flat color="primary">{{ row.packagingItemFk }}</QBtn>
<template #column-packagingItemFk="{ row }">
<span class="link" @click.stop>
{{ row.packagingItemFk }}
<ItemDescriptorProxy :id="row.packagingItemFk" />
</QTd>
</span>
</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-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
@click.stop="showLog(row)"
color="primary"
icon="history"
size="md"
flat
>
<QTooltip class="text-no-wrap">
{{ t('expedition.historyAction') }}
</QTooltip>
</QBtn>
</QTd>
</template>
</QTable>
</VnTable>
<QDialog ref="newTicketDialogRef" transition-show="scale" transition-hide="scale">
<ExpeditionNewTicket
:ticket="ticketData"
@ -454,12 +333,23 @@ onUnmounted(() => (stateStore.rightDrawer = false));
data-key="TicketExpeditionLog"
:rows="expeditionsLogsData"
:columns="logTableColumns"
class="q-pa-sm"
class="q-pa-md full-width"
>
<template #body-cell-name="{ row }">
<QTd auto-width>
<QBtn flat dense color="primary">{{ row.name }}</QBtn>
<WorkerDescriptorProxy :id="row.workerFk" />
<QTd style="text-align: center">
<span class="link" @click.stop>
<QBtn flat dense>{{ row.name }}</QBtn>
<WorkerDescriptorProxy :id="row.workerFk" />
</span>
</QTd>
</template>
<template #body-cell-isScanned="{ row }">
<QTd style="text-align: center">
<QCheckbox disable v-model="row.isScanned">
{{
row.isScanned === 1 ? t('expedition.yes') : t('expedition.no')
}}
</QCheckbox>
</QTd>
</template>
</QTable>

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