HOTFIX: #6943 CustomerList form salesPersons options #790

Closed
jsegarra wants to merge 84 commits from hotfix_newCustomer_SalesPerson into master
165 changed files with 6151 additions and 2114 deletions
Showing only changes of commit 7b91695005 - Show all commits

View File

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

View File

@ -2,9 +2,11 @@ import axios from 'axios';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { Router } from 'src/router'; import { Router } from 'src/router';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useStateQueryStore } from 'src/stores/useStateQueryStore';
const session = useSession(); const session = useSession();
const { notify } = useNotify(); const { notify } = useNotify();
const stateQuery = useStateQueryStore();
const baseUrl = '/api/'; const baseUrl = '/api/';
axios.defaults.baseURL = baseUrl; axios.defaults.baseURL = baseUrl;
@ -15,7 +17,7 @@ const onRequest = (config) => {
if (token.length && !config.headers.Authorization) { if (token.length && !config.headers.Authorization) {
config.headers.Authorization = token; config.headers.Authorization = token;
} }
stateQuery.add(config);
return config; return config;
}; };
@ -24,10 +26,10 @@ const onRequestError = (error) => {
}; };
const onResponse = (response) => { const onResponse = (response) => {
const { method } = response.config; const config = response.config;
stateQuery.remove(config);
const isSaveRequest = method === 'patch'; if (config.method === 'patch') {
if (isSaveRequest) {
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
} }
@ -35,6 +37,8 @@ const onResponse = (response) => {
}; };
const onResponseError = (error) => { const onResponseError = (error) => {
stateQuery.remove(error.config);
let message = ''; let message = '';
const response = error.response; const response = error.response;

View File

@ -0,0 +1,4 @@
import { QInput } from 'quasar';
import setDefault from './setDefault';
setDefault(QInput, 'dense', true);

View File

@ -0,0 +1,4 @@
import { QSelect } from 'quasar';
import setDefault from './setDefault';
setDefault(QSelect, 'dense', true);

View File

@ -1 +1,3 @@
export * from './defaults/qTable'; export * from './defaults/qTable';
export * from './defaults/qInput';
export * from './defaults/qSelect';

View File

@ -31,8 +31,8 @@ const countriesFilter = {
const countriesOptions = ref([]); const countriesOptions = ref([]);
const onDataSaved = (formData, requestResponse) => { const onDataSaved = (...args) => {
emit('onDataSaved', formData, requestResponse); emit('onDataSaved', ...args);
}; };
onMounted(async () => { onMounted(async () => {

View File

@ -1,35 +1,42 @@
<script setup> <script setup>
import { reactive, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnSelectProvince from 'components/VnSelectProvince.vue'; import VnSelectProvince from 'components/VnSelectProvince.vue';
import VnInput from 'components/common/VnInput.vue'; import VnInput from 'components/common/VnInput.vue';
import FormModelPopup from './FormModelPopup.vue'; import FormModelPopup from './FormModelPopup.vue';
const emit = defineEmits(['onDataSaved']); const emit = defineEmits(['onDataSaved']);
const $props = defineProps({
countryFk: {
type: Number,
default: null,
},
provinceSelected: {
type: Number,
default: null,
},
provinces: {
type: Array,
default: () => [],
},
});
const { t } = useI18n(); const { t } = useI18n();
const cityFormData = reactive({ const cityFormData = ref({
name: null, name: null,
provinceFk: null, provinceFk: null,
}); });
onMounted(() => {
const provincesOptions = ref([]); cityFormData.value.provinceFk = $props.provinceSelected;
});
const onDataSaved = (...args) => { const onDataSaved = (...args) => {
emit('onDataSaved', ...args); emit('onDataSaved', ...args);
}; };
</script> </script>
<template> <template>
<FetchData
@on-fetch="(data) => (provincesOptions = data)"
auto-load
url="Provinces"
/>
<FormModelPopup <FormModelPopup
:title="t('New city')" :title="t('New city')"
:subtitle="t('Please, ensure you put the correct data!')" :subtitle="t('Please, ensure you put the correct data!')"
@ -41,11 +48,16 @@ const onDataSaved = (...args) => {
<template #form-inputs="{ data, validate }"> <template #form-inputs="{ data, validate }">
<VnRow> <VnRow>
<VnInput <VnInput
:label="t('Name')" :label="t('Names')"
v-model="data.name" v-model="data.name"
:rules="validate('city.name')" :rules="validate('city.name')"
/> />
<VnSelectProvince v-model="data.provinceFk" /> <VnSelectProvince
:province-selected="$props.provinceSelected"
:country-fk="$props.countryFk"
v-model="data.provinceFk"
:provinces="$props.provinces"
/>
</VnRow> </VnRow>
</template> </template>
</FormModelPopup> </FormModelPopup>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { reactive, ref } from 'vue'; import { reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
@ -22,9 +22,11 @@ const postcodeFormData = reactive({
townFk: null, townFk: null,
}); });
const townsFetchDataRef = ref(null);
const provincesFetchDataRef = ref(null); const provincesFetchDataRef = ref(null);
const countriesOptions = ref([]); const countriesOptions = ref([]);
const provincesOptions = ref([]); const provincesOptions = ref([]);
const townsOptions = ref([]);
const town = ref({}); const town = ref({});
function onDataSaved(formData) { function onDataSaved(formData) {
@ -61,26 +63,91 @@ function setTown(newTown, data) {
} }
async function setProvince(id, data) { async function setProvince(id, data) {
await provincesFetchDataRef.value.fetch();
const newProvince = provincesOptions.value.find((province) => province.id == id); const newProvince = provincesOptions.value.find((province) => province.id == id);
if (!newProvince) return; if (!newProvince) return;
data.countryFk = newProvince.countryFk; data.countryFk = newProvince.countryFk;
} }
async function onProvinceCreated(data) {
await provincesFetchDataRef.value.fetch({
where: { countryFk: postcodeFormData.countryFk },
});
postcodeFormData.provinceFk.value = data.id;
}
watch(
() => [postcodeFormData.countryFk],
async (newCountryFk, oldValueFk) => {
if (Array.isArray(newCountryFk)) {
newCountryFk = newCountryFk[0];
}
if (Array.isArray(oldValueFk)) {
oldValueFk = oldValueFk[0];
}
if (!!oldValueFk && newCountryFk !== oldValueFk) {
postcodeFormData.provinceFk = null;
postcodeFormData.townFk = null;
}
if (oldValueFk !== newCountryFk) {
await provincesFetchDataRef.value.fetch({
where: {
countryFk: newCountryFk,
},
});
await townsFetchDataRef.value.fetch({
where: {
provinceFk: {
inq: provincesOptions.value.map(({ id }) => id),
},
},
});
}
}
);
watch(
() => postcodeFormData.provinceFk,
async (newProvinceFk, oldValueFk) => {
if (Array.isArray(newProvinceFk)) {
newProvinceFk = newProvinceFk[0];
}
if (newProvinceFk !== oldValueFk) {
await townsFetchDataRef.value.fetch({
where: { provinceFk: newProvinceFk },
});
}
}
);
async function handleProvinces(data) {
provincesOptions.value = data;
}
async function handleTowns(data) {
townsOptions.value = data;
}
async function handleCountries(data) {
countriesOptions.value = data;
}
</script> </script>
<template> <template>
<FetchData <FetchData
ref="provincesFetchDataRef" ref="provincesFetchDataRef"
@on-fetch="(data) => (provincesOptions = data)" @on-fetch="handleProvinces"
:sort-by="['name ASC']"
:limit="30"
auto-load auto-load
url="Provinces/location" url="Provinces/location"
/> />
<FetchData <FetchData
@on-fetch="(data) => (countriesOptions = data)" ref="townsFetchDataRef"
:sort-by="['name ASC']"
:limit="30"
@on-fetch="handleTowns"
auto-load auto-load
url="Countries" url="Towns/location"
/> />
<FormModelPopup <FormModelPopup
url-create="postcodes" url-create="postcodes"
model="postcode" model="postcode"
@ -96,18 +163,20 @@ async function setProvince(id, data) {
:label="t('Postcode')" :label="t('Postcode')"
v-model="data.code" v-model="data.code"
:rules="validate('postcode.code')" :rules="validate('postcode.code')"
clearable
/> />
<VnSelectDialog <VnSelectDialog
:label="t('City')" :label="t('City')"
url="Towns/location"
@update:model-value="(value) => setTown(value, data)" @update:model-value="(value) => setTown(value, data)"
:tooltip="t('Create city')"
v-model="data.townFk" v-model="data.townFk"
:options="townsOptions"
option-label="name" option-label="name"
option-value="id" option-value="id"
:rules="validate('postcode.city')" :rules="validate('postcode.city')"
:acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]" :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
:emit-value="false" :emit-value="false"
clearable :clearable="true"
> >
<template #option="{ itemProps, opt }"> <template #option="{ itemProps, opt }">
<QItem v-bind="itemProps"> <QItem v-bind="itemProps">
@ -122,6 +191,9 @@ async function setProvince(id, data) {
</template> </template>
<template #form> <template #form>
<CreateNewCityForm <CreateNewCityForm
:country-fk="data.countryFk"
:province-selected="data.provinceFk"
:provinces="provincesOptions"
@on-data-saved=" @on-data-saved="
(_, requestResponse) => (_, requestResponse) =>
onCityCreated(requestResponse, data) onCityCreated(requestResponse, data)
@ -132,12 +204,19 @@ async function setProvince(id, data) {
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnSelectProvince <VnSelectProvince
:country-fk="data.countryFk"
:province-selected="data.provinceFk"
@update:model-value="(value) => setProvince(value, data)" @update:model-value="(value) => setProvince(value, data)"
v-model="data.provinceFk" v-model="data.provinceFk"
:clearable="true"
:provinces="provincesOptions"
@on-province-created="onProvinceCreated"
/> />
<VnSelect <VnSelect
url="Countries"
:sort-by="['name ASC']"
:label="t('Country')" :label="t('Country')"
:options="countriesOptions" @update:options="handleCountries"
hide-selected hide-selected
option-label="name" option-label="name"
option-value="id" option-value="id"
@ -152,6 +231,7 @@ async function setProvince(id, data) {
<i18n> <i18n>
es: es:
New postcode: Nuevo código postal New postcode: Nuevo código postal
Create city: Crear población
Please, ensure you put the correct data!: ¡Por favor, asegúrese de poner los datos correctos! Please, ensure you put the correct data!: ¡Por favor, asegúrese de poner los datos correctos!
City: Población City: Población
Province: Provincia Province: Provincia

View File

@ -16,7 +16,16 @@ const provinceFormData = reactive({
name: null, name: null,
autonomyFk: null, autonomyFk: null,
}); });
const $props = defineProps({
countryFk: {
type: Number,
default: null,
},
provinces: {
type: Array,
default: () => [],
},
});
const autonomiesOptions = ref([]); const autonomiesOptions = ref([]);
const onDataSaved = (dataSaved, requestResponse) => { const onDataSaved = (dataSaved, requestResponse) => {
@ -31,7 +40,14 @@ const onDataSaved = (dataSaved, requestResponse) => {
<FetchData <FetchData
@on-fetch="(data) => (autonomiesOptions = data)" @on-fetch="(data) => (autonomiesOptions = data)"
auto-load auto-load
:filter="{
where: {
countryFk: $props.countryFk,
},
}"
url="Autonomies/location" url="Autonomies/location"
:sort-by="['name ASC']"
:limit="30"
/> />
<FormModelPopup <FormModelPopup
:title="t('New province')" :title="t('New province')"

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useStateQueryStore } from 'src/stores/useStateQueryStore';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import PinnedModules from './PinnedModules.vue'; import PinnedModules from './PinnedModules.vue';
import UserPanel from 'components/UserPanel.vue'; import UserPanel from 'components/UserPanel.vue';
@ -12,6 +13,7 @@ import VnAvatar from './ui/VnAvatar.vue';
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
const quasar = useQuasar(); const quasar = useQuasar();
const stateQuery = useStateQueryStore();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const appName = 'Lilium'; const appName = 'Lilium';
@ -50,6 +52,14 @@ const pinnedModulesRef = ref();
</QBtn> </QBtn>
</RouterLink> </RouterLink>
<VnBreadcrumbs v-if="$q.screen.gt.sm" /> <VnBreadcrumbs v-if="$q.screen.gt.sm" />
<QSpinner
color="primary"
class="q-ml-md"
:class="{
'no-visible': !stateQuery.isLoading().value,
}"
size="xs"
/>
<QSpace /> <QSpace />
<div id="searchbar" class="searchbar"></div> <div id="searchbar" class="searchbar"></div>
<QSpace /> <QSpace />

View File

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

View File

@ -8,33 +8,55 @@ import FetchData from 'components/FetchData.vue';
import CreateNewProvinceForm from './CreateNewProvinceForm.vue'; import CreateNewProvinceForm from './CreateNewProvinceForm.vue';
const emit = defineEmits(['onProvinceCreated']); const emit = defineEmits(['onProvinceCreated']);
const provinceFk = defineModel({ type: Number }); const $props = defineProps({
watch(provinceFk, async () => await provincesFetchDataRef.value.fetch()); countryFk: {
type: Number,
default: null,
},
provinceSelected: {
type: Number,
default: null,
},
provinces: {
type: Array,
default: () => [],
},
});
const provinceFk = defineModel({ type: Number, default: null });
const { validate } = useValidator(); const { validate } = useValidator();
const { t } = useI18n(); const { t } = useI18n();
const provincesOptions = ref(); const provincesOptions = ref($props.provinces);
provinceFk.value = $props.provinceSelected;
const provincesFetchDataRef = ref(); const provincesFetchDataRef = ref();
async function onProvinceCreated(_, data) { async function onProvinceCreated(_, data) {
await provincesFetchDataRef.value.fetch(); await provincesFetchDataRef.value.fetch({ where: { countryFk: $props.countryFk } });
provinceFk.value = data.id; provinceFk.value = data.id;
emit('onProvinceCreated', data); emit('onProvinceCreated', data);
} }
async function handleProvinces(data) {
provincesOptions.value = data;
}
</script> </script>
<template> <template>
<FetchData <FetchData
ref="provincesFetchDataRef" ref="provincesFetchDataRef"
:filter="{ include: { relation: 'country' } }" :filter="{
@on-fetch="(data) => (provincesOptions = data)" include: { relation: 'country' },
auto-load where: {
countryFk: $props.countryFk,
},
}"
@on-fetch="handleProvinces"
url="Provinces" url="Provinces"
/> />
<VnSelectDialog <VnSelectDialog
:label="t('Province')" :label="t('Province')"
:options="provincesOptions" :options="$props.provinces"
:tooltip="t('Create province')"
hide-selected hide-selected
v-model="provinceFk" v-model="provinceFk"
:rules="validate && validate('postcode.provinceFk')" :rules="validate && validate('postcode.provinceFk')"
@ -49,11 +71,15 @@ async function onProvinceCreated(_, data) {
</QItem> </QItem>
</template> </template>
<template #form> <template #form>
<CreateNewProvinceForm @on-data-saved="onProvinceCreated" /> <CreateNewProvinceForm
:country-fk="$props.countryFk"
@on-data-saved="onProvinceCreated"
/>
</template> </template>
</VnSelectDialog> </VnSelectDialog>
</template> </template>
<i18n> <i18n>
es: es:
Province: Provincia Province: Provincia
Create province: Crear provincia
</i18n> </i18n>

View File

@ -10,7 +10,7 @@ import FormModelPopup from 'components/FormModelPopup.vue';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnTableColumn from 'components/VnTable/VnColumn.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 VnTableChip from 'components/VnTable/VnChip.vue';
import VnVisibleColumn from 'src/components/VnTable/VnVisibleColumn.vue'; import VnVisibleColumn from 'src/components/VnTable/VnVisibleColumn.vue';
import VnLv from 'components/ui/VnLv.vue'; import VnLv from 'components/ui/VnLv.vue';
@ -53,6 +53,10 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
bottom: {
type: Object,
default: null,
},
cardClass: { cardClass: {
type: String, type: String,
default: 'flex-one', default: 'flex-one',
@ -105,6 +109,10 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
disabledAttr: {
type: Boolean,
default: false,
},
}); });
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -315,6 +323,13 @@ function handleOnDataSaved(_) {
if (_.onDataSaved) _.onDataSaved({ CrudModelRef: CrudModelRef.value }); if (_.onDataSaved) _.onDataSaved({ CrudModelRef: CrudModelRef.value });
else $props.create.onDataSaved(_); else $props.create.onDataSaved(_);
} }
function handleScroll() {
const tMiddle = tableRef.value.$el.querySelector('.q-table__middle');
const { scrollHeight, scrollTop, clientHeight } = tMiddle;
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) <= 40;
if (isAtBottom) CrudModelRef.value.vnPaginateRef.paginate();
}
</script> </script>
<template> <template>
<QDrawer <QDrawer
@ -348,7 +363,7 @@ function handleOnDataSaved(_) {
)" )"
:key="col.id" :key="col.id"
> >
<VnTableFilter <VnFilter
ref="tableFilterRef" ref="tableFilterRef"
:column="col" :column="col"
:data-key="$attrs['data-key']" :data-key="$attrs['data-key']"
@ -402,6 +417,7 @@ function handleOnDataSaved(_) {
ref="tableRef" ref="tableRef"
v-bind="table" v-bind="table"
class="vnTable" class="vnTable"
:class="{ 'last-row-sticky': $props.footer }"
:columns="splittedColumns.columns" :columns="splittedColumns.columns"
:rows="rows" :rows="rows"
v-model:selected="selected" v-model:selected="selected"
@ -411,12 +427,7 @@ function handleOnDataSaved(_) {
flat flat
:style="isTableMode && `max-height: ${tableHeight}`" :style="isTableMode && `max-height: ${tableHeight}`"
:virtual-scroll="isTableMode" :virtual-scroll="isTableMode"
@virtual-scroll=" @virtual-scroll="handleScroll"
(event) =>
event.index > rows.length - 2 &&
($props.crudModel?.paginate ?? true) &&
CrudModelRef.vnPaginateRef.paginate()
"
@row-click="(_, row) => rowClickFunction && rowClickFunction(row)" @row-click="(_, row) => rowClickFunction && rowClickFunction(row)"
@update:selected="emit('update:selected', $event)" @update:selected="emit('update:selected', $event)"
> >
@ -466,7 +477,7 @@ function handleOnDataSaved(_) {
:search-url="searchUrl" :search-url="searchUrl"
/> />
</div> </div>
<VnTableFilter <VnFilter
v-if="$props.columnSearch" v-if="$props.columnSearch"
:column="col" :column="col"
:show-title="true" :show-title="true"
@ -496,6 +507,7 @@ function handleOnDataSaved(_) {
auto-width auto-width
class="no-margin q-px-xs" class="no-margin q-px-xs"
:class="[getColAlign(col), col.columnClass]" :class="[getColAlign(col), col.columnClass]"
:style="col.style"
v-if="col.visible ?? true" v-if="col.visible ?? true"
@click.ctrl=" @click.ctrl="
($event) => ($event) =>
@ -524,6 +536,7 @@ function handleOnDataSaved(_) {
:class="getColAlign(col)" :class="getColAlign(col)"
class="sticky no-padding" class="sticky no-padding"
@click="stopEventPropagation($event)" @click="stopEventPropagation($event)"
:style="col.style"
> >
<QBtn <QBtn
v-for="(btn, index) of col.actions" v-for="(btn, index) of col.actions"
@ -544,6 +557,29 @@ function handleOnDataSaved(_) {
/> />
</QTd> </QTd>
</template> </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 }"> <template #item="{ row, colsMap }">
<component <component
:is="$props.redirect ? 'router-link' : 'span'" :is="$props.redirect ? 'router-link' : 'span'"
@ -660,17 +696,15 @@ function handleOnDataSaved(_) {
</QCard> </QCard>
</component> </component>
</template> </template>
<template #bottom-row="{ cols }" v-if="footer"> <template #bottom-row="{ cols }" v-if="$props.footer">
<QTr v-if="rows.length" class="bg-header" style="height: 30px"> <QTr v-if="rows.length" style="height: 30px">
<QTh <QTh
v-for="col of cols.filter((cols) => cols.visible ?? true)" v-for="col of cols.filter((cols) => cols.visible ?? true)"
:key="col?.id" :key="col?.id"
class="text-center" class="text-center"
>
<slot
:name="`column-footer-${col.name}`"
:class="getColAlign(col)" :class="getColAlign(col)"
/> >
<slot :name="`column-footer-${col.name}`" />
</QTh> </QTh>
</QTr> </QTr>
</template> </template>
@ -688,7 +722,7 @@ function handleOnDataSaved(_) {
icon="add" icon="add"
shortcut="+" shortcut="+"
/> />
<QTooltip> <QTooltip self="top right">
{{ createForm?.title }} {{ createForm?.title }}
</QTooltip> </QTooltip>
</QPageSticky> </QPageSticky>
@ -749,16 +783,6 @@ es:
color: var(--vn-text-color); 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 { .grid-three {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, max-content)); grid-template-columns: repeat(auto-fit, minmax(400px, max-content));
@ -833,6 +857,18 @@ es:
background-color: var(--vn-section-color); background-color: var(--vn-section-color);
z-index: 1; z-index: 1;
} }
table tbody th {
position: relative;
}
}
.last-row-sticky {
tbody:nth-last-child(1) {
@extend .bg-header;
position: sticky;
z-index: 2;
bottom: 0;
}
} }
.vn-label-value { .vn-label-value {
@ -878,4 +914,12 @@ es:
cursor: text; cursor: text;
user-select: all; 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> </style>

View File

@ -0,0 +1,136 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnRow from '../ui/VnRow.vue';
import VnInput from './VnInput.vue';
import FetchData from '../FetchData.vue';
import useNotify from 'src/composables/useNotify';
const props = defineProps({
submitFn: { type: Function, default: () => {} },
askOldPass: { type: Boolean, default: false },
});
const emit = defineEmits(['onSubmit']);
const { t } = useI18n();
const { notify } = useNotify();
const form = ref();
const changePassDialog = ref();
const passwords = ref({ newPassword: null, repeatPassword: null });
const requirements = ref([]);
const isLoading = ref(false);
const validate = async () => {
const { newPassword, repeatPassword, oldPassword } = passwords.value;
if (!newPassword) {
notify(t('You must enter a new password'), 'negative');
return;
}
if (newPassword !== repeatPassword) {
notify(t("Passwords don't match"), 'negative');
return;
}
try {
isLoading.value = true;
await props.submitFn(newPassword, oldPassword);
emit('onSubmit');
} catch (e) {
notify('errors.writeRequest', 'negative');
} finally {
changePassDialog.value.hide();
isLoading.value = false;
}
};
defineExpose({ show: () => changePassDialog.value.show() });
</script>
<template>
<FetchData
url="UserPasswords/findOne"
auto-load
@on-fetch="(data) => (requirements = data)"
/>
<QDialog ref="changePassDialog">
<QCard style="width: 350px">
<QCardSection>
<slot name="header">
<VnRow class="items-center" style="flex-direction: row">
<span class="text-h6" v-text="t('globals.changePass')" />
<QIcon
class="cursor-pointer"
name="close"
size="xs"
style="flex: 0"
v-close-popup
/>
</VnRow>
</slot>
</QCardSection>
<QForm ref="form">
<QCardSection>
<VnInput
v-if="props.askOldPass"
:label="t('Old password')"
v-model="passwords.oldPassword"
type="password"
:required="true"
autofocus
/>
<VnInput
:label="t('New password')"
v-model="passwords.newPassword"
type="password"
:required="true"
:info="
t('passwordRequirements', {
length: requirements.length,
nAlpha: requirements.nAlpha,
nUpper: requirements.nUpper,
nDigits: requirements.nDigits,
nPunct: requirements.nPunct,
})
"
autofocus
/>
<VnInput
:label="t('Repeat password')"
v-model="passwords.repeatPassword"
type="password"
/>
</QCardSection>
</QForm>
<QCardActions>
<slot name="actions">
<QBtn
:disabled="isLoading"
:loading="isLoading"
:label="t('globals.cancel')"
class="q-ml-sm"
color="primary"
flat
type="reset"
v-close-popup
/>
<QBtn
:disabled="isLoading"
:loading="isLoading"
:label="t('globals.confirm')"
color="primary"
@click="validate"
/>
</slot>
</QCardActions>
</QCard>
</QDialog>
</template>
<i18n>
es:
New password: Nueva contraseña
Repeat password: Repetir contraseña
You must enter a new password: Debes introducir la nueva contraseña
Passwords don't match: Las contraseñas no coinciden
</i18n>

View File

@ -0,0 +1,29 @@
<script setup>
const model = defineModel({ type: [String, Number], required: true });
</script>
<template>
<QDate v-model="model" :today-btn="true" :options="$attrs.options" />
</template>
<style lang="scss" scoped>
.q-date {
width: 245px;
min-width: unset;
:deep(.q-date__calendar) {
padding-bottom: 0;
}
:deep(.q-date__view) {
min-height: 245px;
padding: 8px;
}
:deep(.q-date__calendar-days-container) {
min-height: 160px;
height: unset;
}
:deep(.q-date__header) {
padding: 2px 2px 5px 12px;
height: 60px;
}
}
</style>

View File

@ -103,6 +103,7 @@ const mixinRules = [
@click=" @click="
() => { () => {
value = null; value = null;
vnInputRef.focus();
emit('remove'); emit('remove');
} }
" "

View File

@ -3,6 +3,7 @@ import { onMounted, watch, computed, ref } from 'vue';
import { date } from 'quasar'; import { date } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useAttrs } from 'vue'; import { useAttrs } from 'vue';
import VnDate from './VnDate.vue';
const model = defineModel({ type: [String, Date] }); const model = defineModel({ type: [String, Date] });
const $props = defineProps({ const $props = defineProps({
@ -20,6 +21,7 @@ const { validations } = useValidator();
const { t } = useI18n(); const { t } = useI18n();
const requiredFieldRule = (val) => validations().required($attrs.required, val); const requiredFieldRule = (val) => validations().required($attrs.required, val);
const vnInputDateRef = ref(null);
const dateFormat = 'DD/MM/YYYY'; const dateFormat = 'DD/MM/YYYY';
const isPopupOpen = ref(); const isPopupOpen = ref();
@ -86,11 +88,17 @@ const styleAttrs = computed(() => {
} }
: {}; : {};
}); });
const manageDate = (date) => {
formattedDate.value = date;
isPopupOpen.value = false;
};
</script> </script>
<template> <template>
<div @mouseover="hover = true" @mouseleave="hover = false"> <div @mouseover="hover = true" @mouseleave="hover = false">
<QInput <QInput
ref="vnInputDateRef"
v-model="formattedDate" v-model="formattedDate"
class="vn-input-date" class="vn-input-date"
:mask="mask" :mask="mask"
@ -113,6 +121,7 @@ const styleAttrs = computed(() => {
!$attrs.disable !$attrs.disable
" "
@click=" @click="
vnInputDateRef.focus();
model = null; model = null;
isPopupOpen = false; isPopupOpen = false;
" "
@ -126,6 +135,7 @@ const styleAttrs = computed(() => {
/> />
</template> </template>
<QMenu <QMenu
v-if="$q.screen.gt.xs"
transition-show="scale" transition-show="scale"
transition-hide="scale" transition-hide="scale"
v-model="isPopupOpen" v-model="isPopupOpen"
@ -134,19 +144,11 @@ const styleAttrs = computed(() => {
:no-focus="true" :no-focus="true"
:no-parent-event="true" :no-parent-event="true"
> >
<QDate <VnDate v-model="popupDate" @update:model-value="manageDate" />
v-model="popupDate"
:landscape="true"
:today-btn="true"
:options="$attrs.options"
@update:model-value="
(date) => {
formattedDate = date;
isPopupOpen = false;
}
"
/>
</QMenu> </QMenu>
<QDialog v-else v-model="isPopupOpen">
<VnDate v-model="popupDate" @update:model-value="manageDate" />
</QDialog>
</QInput> </QInput>
</div> </div>
</template> </template>

View File

@ -3,6 +3,8 @@ import { computed, ref, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { date } from 'quasar'; import { date } from 'quasar';
import { useValidator } from 'src/composables/useValidator'; import { useValidator } from 'src/composables/useValidator';
import VnTime from './VnTime.vue';
const { validations } = useValidator(); const { validations } = useValidator();
const $attrs = useAttrs(); const $attrs = useAttrs();
const model = defineModel({ type: String }); const model = defineModel({ type: String });
@ -16,6 +18,7 @@ const props = defineProps({
default: false, default: false,
}, },
}); });
const vnInputTimeRef = ref(null);
const initialDate = ref(model.value ?? Date.vnNew()); const initialDate = ref(model.value ?? Date.vnNew());
const { t } = useI18n(); const { t } = useI18n();
const requiredFieldRule = (val) => validations().required($attrs.required, val); const requiredFieldRule = (val) => validations().required($attrs.required, val);
@ -69,6 +72,7 @@ function dateToTime(newDate) {
<template> <template>
<div @mouseover="hover = true" @mouseleave="hover = false"> <div @mouseover="hover = true" @mouseleave="hover = false">
<QInput <QInput
ref="vnInputTimeRef"
class="vn-input-time" class="vn-input-time"
mask="##:##" mask="##:##"
placeholder="--:--" placeholder="--:--"
@ -92,6 +96,7 @@ function dateToTime(newDate) {
!$attrs.disable !$attrs.disable
" "
@click=" @click="
vnInputTimeRef.focus();
model = null; model = null;
isPopupOpen = false; isPopupOpen = false;
" "
@ -104,6 +109,7 @@ function dateToTime(newDate) {
/> />
</template> </template>
<QMenu <QMenu
v-if="$q.screen.gt.xs"
transition-show="scale" transition-show="scale"
transition-hide="scale" transition-hide="scale"
v-model="isPopupOpen" v-model="isPopupOpen"
@ -112,8 +118,11 @@ function dateToTime(newDate) {
:no-focus="true" :no-focus="true"
:no-parent-event="true" :no-parent-event="true"
> >
<QTime v-model="formattedTime" mask="HH:mm" landscape now-btn /> <VnTime v-model="formattedTime" />
</QMenu> </QMenu>
<QDialog v-else v-model="isPopupOpen">
<VnTime v-model="formattedTime" />
</QDialog>
</QInput> </QInput>
</div> </div>
</template> </template>

View File

@ -12,6 +12,16 @@ const props = defineProps({
default: null, default: null,
}, },
}); });
const locationProperties = [
'postcode',
(obj) =>
obj.city
? `${obj.city}${obj.province?.name ? `(${obj.province.name})` : ''}`
: null,
(obj) => obj.country?.name,
];
const formatLocation = (obj, properties) => { const formatLocation = (obj, properties) => {
const parts = properties.map((prop) => { const parts = properties.map((prop) => {
if (typeof prop === 'string') { if (typeof prop === 'string') {
@ -29,23 +39,10 @@ const formatLocation = (obj, properties) => {
return filteredParts.join(', '); 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( const modelValue = ref(
props.location ? formatLocation(props.location, locationProperties) : null props.location ? formatLocation(props.location, locationProperties) : null
); );
const handleModelValue = (data) => {
emit('update:model-value', data);
};
function showLabel(data) { function showLabel(data) {
const dataProperties = [ const dataProperties = [
'code', 'code',
@ -54,6 +51,10 @@ function showLabel(data) {
]; ];
return formatLocation(data, dataProperties); return formatLocation(data, dataProperties);
} }
const handleModelValue = (data) => {
emit('update:model-value', data);
};
</script> </script>
<template> <template>
<VnSelectDialog <VnSelectDialog
@ -72,6 +73,7 @@ function showLabel(data) {
v-bind="$attrs" v-bind="$attrs"
clearable clearable
:emit-value="false" :emit-value="false"
:tooltip="t('Create new location')"
> >
<template #form> <template #form>
<CreateNewPostcode <CreateNewPostcode
@ -104,7 +106,9 @@ function showLabel(data) {
<i18n> <i18n>
en: en:
search_by_postalcode: Search by postalcode, town, province or country search_by_postalcode: Search by postalcode, town, province or country
Create new location: Create new location
es: es:
Location: Ubicación Location: Ubicación
Create new location: Crear nueva ubicación
search_by_postalcode: Buscar por código postal, ciudad o país search_by_postalcode: Buscar por código postal, ciudad o país
</i18n> </i18n>

View File

@ -141,6 +141,7 @@ function findKeyInOptions() {
function setOptions(data) { function setOptions(data) {
myOptions.value = JSON.parse(JSON.stringify(data)); myOptions.value = JSON.parse(JSON.stringify(data));
myOptionsOriginal.value = JSON.parse(JSON.stringify(data)); myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
emit('update:options', data);
} }
function filter(val, options) { function filter(val, options) {

View File

@ -0,0 +1,16 @@
<script setup>
const model = defineModel({ type: [String, Number], required: true });
</script>
<template>
<QTime v-model="model" now-btn mask="HH:mm" />
</template>
<style lang="scss" scoped>
.q-time {
width: 230px;
min-width: unset;
:deep(.q-time__header) {
min-height: unset;
height: 50px;
}
}
</style>

View File

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

View File

@ -3,7 +3,6 @@ import { onMounted, ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { date } from 'quasar';
import toDate from 'filters/toDate'; import toDate from 'filters/toDate';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
@ -59,7 +58,6 @@ const $props = defineProps({
}); });
defineExpose({ search, sanitizer }); defineExpose({ search, sanitizer });
const emit = defineEmits([ const emit = defineEmits([
'update:modelValue', 'update:modelValue',
'refresh', 'refresh',
@ -114,9 +112,9 @@ watch(
); );
const isLoading = ref(false); const isLoading = ref(false);
async function search() { async function search(evt) {
try { try {
if ($props.disableSubmitEvent) return; if (evt && $props.disableSubmitEvent) return;
store.filter.where = {}; store.filter.where = {};
isLoading.value = true; isLoading.value = true;
@ -167,7 +165,7 @@ const tagsList = computed(() => {
for (const key of Object.keys(userParams.value)) { for (const key of Object.keys(userParams.value)) {
const value = userParams.value[key]; const value = userParams.value[key];
if (value == null || ($props.hiddenTags || []).includes(key)) continue; if (value == null || ($props.hiddenTags || []).includes(key)) continue;
tagList.push({ label: aliasField(key), value }); tagList.push({ label: key, value });
} }
return tagList; return tagList;
}); });
@ -187,7 +185,6 @@ async function remove(key) {
} }
function formatValue(value) { 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 (typeof value === 'boolean') return value ? t('Yes') : t('No');
if (isNaN(value) && !isNaN(Date.parse(value))) return toDate(value); if (isNaN(value) && !isNaN(Date.parse(value))) return toDate(value);
@ -203,11 +200,6 @@ function sanitizer(params) {
} }
return params; return params;
} }
function aliasField(field) {
const split = field.split('.');
return split[1] ?? split[0];
}
</script> </script>
<template> <template>

View File

@ -58,7 +58,7 @@ defineExpose({
:class="{ zoomIn: zoom }" :class="{ zoomIn: zoom }"
:src="getUrl()" :src="getUrl()"
v-bind="$attrs" v-bind="$attrs"
@click.stop="show = $props.zoom ? true : false" @click.stop="show = $props.zoom"
spinner-color="primary" spinner-color="primary"
/> />
<QDialog v-if="$props.zoom" v-model="show"> <QDialog v-if="$props.zoom" v-model="show">

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { ref } from 'vue'; import { ref, reactive } from 'vue';
import { onBeforeRouteLeave } from 'vue-router'; import { onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
@ -12,36 +12,40 @@ import VnPaginate from 'components/ui/VnPaginate.vue';
import VnUserLink from 'components/ui/VnUserLink.vue'; import VnUserLink from 'components/ui/VnUserLink.vue';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import VnAvatar from 'components/ui/VnAvatar.vue'; import VnAvatar from 'components/ui/VnAvatar.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import VnInput from 'components/common/VnInput.vue';
const $props = defineProps({ const $props = defineProps({
url: { type: String, default: null }, url: { type: String, default: null },
filter: { type: Object, default: () => {} }, filter: { type: Object, default: () => {} },
body: { type: Object, default: () => {} }, body: { type: Object, default: () => {} },
addNote: { type: Boolean, default: false }, addNote: { type: Boolean, default: false },
selectType: { type: Boolean, default: false },
}); });
const { t } = useI18n(); const { t } = useI18n();
const state = useState(); const state = useState();
const quasar = useQuasar(); const quasar = useQuasar();
const currentUser = ref(state.getUser()); const currentUser = ref(state.getUser());
const newNote = ref(''); const newNote = reactive({ text: null, observationTypeFk: null });
const observationTypes = ref([]);
const vnPaginateRef = ref(); const vnPaginateRef = ref();
function handleKeyUp(event) {
if (event.key === 'Enter') {
event.preventDefault();
if (!event.shiftKey) insert();
}
}
async function insert() { async function insert() {
if (!newNote.text || ($props.selectType && !newNote.observationTypeFk)) return;
const body = $props.body; const body = $props.body;
const newBody = { ...body, ...{ text: newNote.value } }; const newBody = {
...body,
...{ text: newNote.text, observationTypeFk: newNote.observationTypeFk },
};
await axios.post($props.url, newBody); await axios.post($props.url, newBody);
await vnPaginateRef.value.fetch(); await vnPaginateRef.value.fetch();
newNote.value = '';
} }
onBeforeRouteLeave((to, from, next) => { onBeforeRouteLeave((to, from, next) => {
if (newNote.value) if (newNote.text)
quasar.dialog({ quasar.dialog({
component: VnConfirm, component: VnConfirm,
componentProps: { componentProps: {
@ -54,6 +58,13 @@ onBeforeRouteLeave((to, from, next) => {
}); });
</script> </script>
<template> <template>
<FetchData
v-if="selectType"
url="ObservationTypes"
:filter="{ fields: ['id', 'description'] }"
auto-load
@on-fetch="(data) => (observationTypes = data)"
/>
<QCard class="q-pa-xs q-mb-xl full-width" v-if="$props.addNote"> <QCard class="q-pa-xs q-mb-xl full-width" v-if="$props.addNote">
<QCardSection horizontal> <QCardSection horizontal>
<VnAvatar :worker-id="currentUser.id" size="md" /> <VnAvatar :worker-id="currentUser.id" size="md" />
@ -62,18 +73,28 @@ onBeforeRouteLeave((to, from, next) => {
{{ t('globals.now') }} {{ t('globals.now') }}
</div> </div>
</QCardSection> </QCardSection>
<QCardSection class="q-pa-xs q-my-none q-py-none" horizontal> <QCardSection class="q-px-xs q-my-none q-py-none">
<QInput <VnRow class="full-width">
v-model="newNote" <VnSelect
class="full-width" :label="t('Observation type')"
v-if="selectType"
url="ObservationTypes"
v-model="newNote.observationTypeFk"
option-label="description"
style="flex: 0.15"
:required="true"
@keyup.enter.stop="insert"
/>
<VnInput
v-model.trim="newNote.text"
type="textarea" type="textarea"
:label="t('Add note here...')" :label="t('Add note here...')"
filled filled
size="lg" size="lg"
autogrow autogrow
autofocus @keyup.enter.stop="insert"
@keyup="handleKeyUp"
clearable clearable
:required="true"
> >
<template #append> <template #append>
<QBtn <QBtn
@ -82,9 +103,12 @@ onBeforeRouteLeave((to, from, next) => {
color="primary" color="primary"
flat flat
@click="insert" @click="insert"
class="q-mb-xs"
dense
/> />
</template> </template>
</QInput> </VnInput>
</VnRow>
</QCardSection> </QCardSection>
</QCard> </QCard>
<VnPaginate <VnPaginate
@ -98,6 +122,10 @@ onBeforeRouteLeave((to, from, next) => {
class="show" class="show"
v-bind="$attrs" v-bind="$attrs"
search-url="notes" search-url="notes"
@on-fetch="
newNote.text = '';
newNote.observationTypeFk = null;
"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<TransitionGroup name="list" tag="div" class="column items-center full-width"> <TransitionGroup name="list" tag="div" class="column items-center full-width">
@ -111,13 +139,28 @@ onBeforeRouteLeave((to, from, next) => {
:descriptor="false" :descriptor="false"
:worker-id="note.workerFk" :worker-id="note.workerFk"
size="md" size="md"
:title="note.worker?.user.nickname"
/> />
<div class="full-width row justify-between q-pa-xs"> <div class="full-width row justify-between q-pa-xs">
<div>
<VnUserLink <VnUserLink
:name="`${note.worker.user.nickname}`" :name="`${note.worker.user.nickname}`"
:worker-id="note.worker.id" :worker-id="note.worker.id"
/> />
{{ toDateHourMin(note.created) }} <QBadge
class="q-ml-xs"
outline
color="grey"
v-if="selectType && note.observationTypeFk"
>
{{
observationTypes.find(
(ot) => ot.id === note.observationTypeFk
)?.description
}}
</QBadge>
</div>
<span v-text="toDateHourMin(note.created)" />
</div> </div>
</QCardSection> </QCardSection>
<QCardSection class="q-pa-xs q-my-none q-py-none"> <QCardSection class="q-pa-xs q-my-none q-py-none">
@ -131,12 +174,6 @@ onBeforeRouteLeave((to, from, next) => {
<style lang="scss" scoped> <style lang="scss" scoped>
.q-card { .q-card {
width: 90%; width: 90%;
@media (max-width: $breakpoint-sm) {
width: 100%;
}
&__section {
word-wrap: break-word;
}
} }
.q-dialog .q-card { .q-dialog .q-card {
width: 400px; width: 400px;
@ -150,11 +187,28 @@ onBeforeRouteLeave((to, from, next) => {
opacity: 0; opacity: 0;
background-color: $primary; background-color: $primary;
} }
.vn-row > :nth-child(2) {
margin-left: 0;
}
@media (max-width: $breakpoint-xs) {
.vn-row > :deep(*) {
margin-left: 0;
}
.q-card {
width: 100%;
&__section {
padding: 0;
}
}
}
</style> </style>
<i18n> <i18n>
es: es:
Add note here...: Añadir nota aquí... Add note here...: Añadir nota aquí...
New note: Nueva nota New note: Nueva nota
Save (Enter): Guardar (Intro) Save (Enter): Guardar (Intro)
Observation type: Tipo de observación
</i18n> </i18n>

View File

@ -108,6 +108,7 @@ async function search() {
...Object.fromEntries(staticParams), ...Object.fromEntries(staticParams),
search: searchText.value, search: searchText.value,
}, },
...{ filter: props.filter },
}; };
if (props.whereFilter) { if (props.whereFilter) {

View File

@ -0,0 +1,33 @@
<script setup>
import { useRoute } from 'vue-router';
import { defineProps } from 'vue';
const props = defineProps({
routeName: {
type: String,
required: true,
},
entityId: {
type: [String, Number],
required: true,
},
url: {
type: String,
default: null,
},
});
const route = useRoute();
const id = props.entityId;
</script>
<template>
<router-link
v-if="route?.name !== routeName"
:to="{ name: routeName, params: { id: id } }"
class="header link"
:href="url"
>
<QIcon name="open_in_new" color="white" size="sm" />
</router-link>
</template>

View File

@ -0,0 +1,55 @@
<script setup>
import { defineProps, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
usesMana: {
type: Boolean,
required: true,
},
manaCode: {
type: String,
required: true,
},
manaVal: {
type: String,
default: 'mana',
},
manaLabel: {
type: String,
default: 'Promotion mana',
},
manaClaimVal: {
type: String,
default: 'manaClaim',
},
claimLabel: {
type: String,
default: 'Claim mana',
},
});
const manaCode = ref(props.manaCode);
</script>
<template>
<div class="column q-gutter-y-sm q-mt-sm">
<QRadio
v-model="manaCode"
dense
:val="manaVal"
:label="t(manaLabel)"
:dark="true"
class="q-mb-sm"
/>
<QRadio
v-model="manaCode"
dense
:val="manaClaimVal"
:label="t(claimLabel)"
:dark="true"
class="q-mb-sm"
/>
</div>
</template>

View File

@ -288,3 +288,7 @@ input::-webkit-inner-spin-button {
color: $info; color: $info;
} }
} }
.no-visible {
visibility: hidden;
}

View File

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

View File

@ -50,6 +50,7 @@ globals:
summary: summary:
basicData: Basic data basicData: Basic data
daysOnward: Days onward daysOnward: Days onward
daysAgo: Days ago
today: Today today: Today
yesterday: Yesterday yesterday: Yesterday
dateFormat: en-GB dateFormat: en-GB
@ -104,6 +105,7 @@ globals:
campaign: Campaign campaign: Campaign
weight: Weight weight: Weight
error: Ups! Something went wrong error: Ups! Something went wrong
recalc: Recalculate
pageTitles: pageTitles:
logIn: Login logIn: Login
addressEdit: Update address addressEdit: Update address
@ -111,7 +113,7 @@ globals:
basicData: Basic data basicData: Basic data
log: Logs log: Logs
parkingList: Parkings list parkingList: Parkings list
agencyList: Agencies list agencyList: Agencies
agency: Agency agency: Agency
workCenters: Work centers workCenters: Work centers
modes: Modes modes: Modes
@ -135,7 +137,7 @@ globals:
fiscalData: Fiscal data fiscalData: Fiscal data
billingData: Billing data billingData: Billing data
consignees: Consignees consignees: Consignees
'address-create': New address address-create: New address
notes: Notes notes: Notes
credits: Credits credits: Credits
greuges: Greuges greuges: Greuges
@ -207,7 +209,7 @@ globals:
roadmap: Roadmap roadmap: Roadmap
stops: Stops stops: Stops
routes: Routes routes: Routes
cmrsList: CMRs list cmrsList: CMRs
RouteList: List RouteList: List
routeCreate: New route routeCreate: New route
RouteRoadmap: Roadmaps RouteRoadmap: Roadmaps
@ -273,6 +275,9 @@ globals:
clientsActionsMonitor: Clients and actions clientsActionsMonitor: Clients and actions
serial: Serial serial: Serial
medical: Mutual medical: Mutual
RouteExtendedList: Router
wasteRecalc: Waste recaclulate
operator: Operator
supplier: Supplier supplier: Supplier
created: Created created: Created
worker: Worker worker: Worker
@ -288,7 +293,10 @@ globals:
createInvoiceIn: Create invoice in createInvoiceIn: Create invoice in
myAccount: My account myAccount: My account
noOne: No one noOne: No one
maxTemperature: Max
minTemperature: Min
params: params:
id: ID
clientFk: Client id clientFk: Client id
salesPersonFk: Sales person salesPersonFk: Sales person
warehouseFk: Warehouse warehouseFk: Warehouse
@ -296,8 +304,13 @@ globals:
from: From from: From
To: To To: To
stateFk: State stateFk: State
email: Email
SSN: SSN
fi: FI
myTeam: My team myTeam: My team
departmentFk: Department departmentFk: Department
changePass: Change password
deleteConfirmTitle: Delete selected elements
changeState: Change state changeState: Change state
errors: errors:
statusUnauthorized: Access denied statusUnauthorized: Access denied
@ -305,6 +318,7 @@ errors:
statusBadGateway: It seems that the server has fall down statusBadGateway: It seems that the server has fall down
statusGatewayTimeout: Could not contact the server statusGatewayTimeout: Could not contact the server
userConfig: Error fetching user config userConfig: Error fetching user config
updateUserConfig: Error updating user config
tokenConfig: Error fetching token config tokenConfig: Error fetching token config
writeRequest: The requested operation could not be completed writeRequest: The requested operation could not be completed
login: login:
@ -498,6 +512,8 @@ ticket:
warehouse: Warehouse warehouse: Warehouse
customerCard: Customer card customerCard: Customer card
alias: Alias alias: Alias
ticketList: Ticket List
newOrder: New Order
boxing: boxing:
expedition: Expedition expedition: Expedition
item: Item item: Item
@ -519,6 +535,7 @@ ticket:
landed: Landed landed: Landed
consigneePhone: Consignee phone consigneePhone: Consignee phone
consigneeMobile: Consignee mobile consigneeMobile: Consignee mobile
consigneeAddress: Consignee address
clientPhone: Client phone clientPhone: Client phone
clientMobile: Client mobile clientMobile: Client mobile
consignee: Consignee consignee: Consignee
@ -547,6 +564,11 @@ ticket:
weight: Weight weight: Weight
goTo: Go to goTo: Go to
summaryAmount: Summary summaryAmount: Summary
purchaseRequest: Purchase request
service: Service
description: Description
attender: Attender
ok: Ok
create: create:
client: Client client: Client
address: Address address: Address
@ -570,7 +592,6 @@ invoiceOut:
client: Client client: Client
company: Company company: Company
customerCard: Customer card customerCard: Customer card
ticketList: Ticket List
summary: summary:
issued: Issued issued: Issued
created: Created created: Created
@ -724,6 +745,7 @@ worker:
locker: Locker locker: Locker
balance: Balance balance: Balance
medical: Medical medical: Medical
operator: Operator
list: list:
name: Name name: Name
email: Email email: Email
@ -796,14 +818,14 @@ worker:
bankEntity: Swift / BIC bankEntity: Swift / BIC
formation: formation:
tableVisibleColumns: tableVisibleColumns:
course: Curso course: Course
startDate: Fecha Inicio startDate: Start date
endDate: Fecha Fin endDate: End date
center: Centro Formación center: Training center
invoice: Factura invoice: Invoice
amount: Importe amount: Amount
remark: Bonficado remark: Remark
hasDiploma: Diploma hasDiploma: Has diploma
medical: medical:
tableVisibleColumns: tableVisibleColumns:
date: Date date: Date
@ -821,6 +843,18 @@ worker:
debit: Debt debit: Debt
credit: Have credit: Have
concept: Concept concept: Concept
operator:
numberOfWagons: Number of wagons
train: Train
itemPackingType: Item packing type
warehouse: Warehouse
sector: Sector
labeler: Printer
linesLimit: Lines limit
volumeLimit: Volume limit
sizeLimit: Size limit
isOnReservationMode: Reservation mode
machine: Machine
wagon: wagon:
pageTitles: pageTitles:
wagons: Wagons wagons: Wagons
@ -860,34 +894,7 @@ wagon:
minHeightBetweenTrays: 'The minimum height between trays is ' minHeightBetweenTrays: 'The minimum height between trays is '
maxWagonHeight: 'The maximum height of the wagon is ' maxWagonHeight: 'The maximum height of the wagon is '
uncompleteTrays: There are incomplete trays uncompleteTrays: There are incomplete trays
route:
pageTitles:
agency: Agency List
routes: Routes
cmrsList: CMRs list
RouteList: List
routeCreate: New route
basicData: Basic Data
summary: Summary
RouteRoadmap: Roadmaps
RouteRoadmapCreate: Create roadmap
tickets: Tickets
log: Log
autonomous: Autonomous
cmr:
list:
results: results
cmrFk: CMR id
hasCmrDms: Attached in gestdoc
'true': 'Yes'
'false': 'No'
ticketFk: Ticketd id
routeFk: Route id
country: Country
clientFk: Client id
shipped: Preparation date
viewCmr: View CMR
downloadCmrs: Download CMRs
supplier: supplier:
list: list:
payMethod: Pay method payMethod: Pay method

View File

@ -49,6 +49,7 @@ globals:
summary: summary:
basicData: Datos básicos basicData: Datos básicos
daysOnward: Días adelante daysOnward: Días adelante
daysAgo: Días atras
today: Hoy today: Hoy
yesterday: Ayer yesterday: Ayer
dateFormat: es-ES dateFormat: es-ES
@ -63,7 +64,7 @@ globals:
shipped: F. envío shipped: F. envío
totalEntries: Ent. totales totalEntries: Ent. totales
amount: Importe amount: Importe
packages: Bultos packages: Embalajes
download: Descargar download: Descargar
downloadPdf: Descargar PDF downloadPdf: Descargar PDF
selectRows: 'Seleccionar las { numberRows } filas(s)' selectRows: 'Seleccionar las { numberRows } filas(s)'
@ -106,6 +107,7 @@ globals:
campaign: Campaña campaign: Campaña
weight: Peso weight: Peso
error: ¡Ups! Algo salió mal error: ¡Ups! Algo salió mal
recalc: Recalcular
pageTitles: pageTitles:
logIn: Inicio de sesión logIn: Inicio de sesión
addressEdit: Modificar consignatario addressEdit: Modificar consignatario
@ -113,7 +115,7 @@ globals:
basicData: Datos básicos basicData: Datos básicos
log: Historial log: Historial
parkingList: Listado de parkings parkingList: Listado de parkings
agencyList: Listado de agencias agencyList: Agencias
agency: Agencia agency: Agencia
workCenters: Centros de trabajo workCenters: Centros de trabajo
modes: Modos modes: Modos
@ -211,12 +213,13 @@ globals:
roadmap: Troncales roadmap: Troncales
stops: Paradas stops: Paradas
routes: Rutas routes: Rutas
cmrsList: Listado de CMRs cmrsList: CMRs
RouteList: Listado RouteList: Listado
routeCreate: Nueva ruta routeCreate: Nueva ruta
RouteRoadmap: Troncales RouteRoadmap: Troncales
RouteRoadmapCreate: Crear troncal RouteRoadmapCreate: Crear troncal
autonomous: Autónomos autonomous: Autónomos
RouteExtendedList: Enrutador
suppliers: Proveedores suppliers: Proveedores
supplier: Proveedor supplier: Proveedor
supplierCreate: Nuevo proveedor supplierCreate: Nuevo proveedor
@ -267,7 +270,7 @@ globals:
tracking: Estados tracking: Estados
components: Componentes components: Componentes
pictures: Fotos pictures: Fotos
packages: Bultos packages: Embalajes
ldap: LDAP ldap: LDAP
samba: Samba samba: Samba
twoFactor: Doble factor twoFactor: Doble factor
@ -277,6 +280,8 @@ globals:
clientsActionsMonitor: Clientes y acciones clientsActionsMonitor: Clientes y acciones
serial: Facturas por serie serial: Facturas por serie
medical: Mutua medical: Mutua
wasteRecalc: Recalcular mermas
operator: Operario
supplier: Proveedor supplier: Proveedor
created: Fecha creación created: Fecha creación
worker: Trabajador worker: Trabajador
@ -292,7 +297,10 @@ globals:
createInvoiceIn: Crear factura recibida createInvoiceIn: Crear factura recibida
myAccount: Mi cuenta myAccount: Mi cuenta
noOne: Nadie noOne: Nadie
maxTemperature: Máx
minTemperature: Mín
params: params:
id: Id
clientFk: Id cliente clientFk: Id cliente
salesPersonFk: Comercial salesPersonFk: Comercial
warehouseFk: Almacén warehouseFk: Almacén
@ -300,8 +308,13 @@ globals:
from: Desde from: Desde
To: Hasta To: Hasta
stateFk: Estado stateFk: Estado
myTeam: Mi equipo
departmentFk: Departamento departmentFk: Departamento
email: Correo
SSN: NSS
fi: NIF
myTeam: Mi equipo
changePass: Cambiar contraseña
deleteConfirmTitle: Eliminar los elementos seleccionados
changeState: Cambiar estado changeState: Cambiar estado
errors: errors:
statusUnauthorized: Acceso denegado statusUnauthorized: Acceso denegado
@ -309,6 +322,7 @@ errors:
statusBadGateway: Parece ser que el servidor ha caído statusBadGateway: Parece ser que el servidor ha caído
statusGatewayTimeout: No se ha podido contactar con el servidor statusGatewayTimeout: No se ha podido contactar con el servidor
userConfig: Error al obtener configuración de usuario 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 tokenConfig: Error al obtener configuración de token
writeRequest: No se pudo completar la operación solicitada writeRequest: No se pudo completar la operación solicitada
login: login:
@ -489,7 +503,7 @@ ticket:
tracking: Estados tracking: Estados
components: Componentes components: Componentes
pictures: Fotos pictures: Fotos
packages: Bultos packages: Embalajes
list: list:
nickname: Alias nickname: Alias
state: Estado state: Estado
@ -507,6 +521,8 @@ ticket:
warehouse: Almacén warehouse: Almacén
customerCard: Ficha del cliente customerCard: Ficha del cliente
alias: Alias alias: Alias
ticketList: Listado de tickets
newOrder: Nuevo pedido
boxing: boxing:
expedition: Expedición expedition: Expedición
item: Artículo item: Artículo
@ -528,6 +544,7 @@ ticket:
landed: Entregado landed: Entregado
consigneePhone: Tel. consignatario consigneePhone: Tel. consignatario
consigneeMobile: Móv. consignatario consigneeMobile: Móv. consignatario
consigneeAddress: Dir. consignatario
clientPhone: Tel. cliente clientPhone: Tel. cliente
clientMobile: Móv. cliente clientMobile: Móv. cliente
consignee: Consignatario consignee: Consignatario
@ -556,6 +573,10 @@ ticket:
weight: Peso weight: Peso
goTo: Ir a goTo: Ir a
summaryAmount: Resumen summaryAmount: Resumen
purchaseRequest: Petición de compra
service: Servicio
description: Descripción
attender: Consignatario
create: create:
client: Cliente client: Cliente
address: Dirección address: Dirección
@ -731,6 +752,7 @@ worker:
balance: Balance balance: Balance
formation: Formación formation: Formación
medical: Mutua medical: Mutua
operator: Operario
list: list:
name: Nombre name: Nombre
email: Email email: Email
@ -819,6 +841,19 @@ worker:
debit: Debe debit: Debe
credit: Haber credit: Haber
concept: Concepto concept: Concepto
operator:
numberOfWagons: Número de vagones
train: tren
itemPackingType: Tipo de embalaje
warehouse: Almacén
sector: Sector
labeler: Impresora
linesLimit: Líneas límite
volumeLimit: Volumen límite
sizeLimit: Tamaño límite
isOnReservationMode: Modo de reserva
machine: Máquina
wagon: wagon:
pageTitles: pageTitles:
wagons: Vagones wagons: Vagones
@ -858,21 +893,6 @@ wagon:
minHeightBetweenTrays: 'La distancia mínima entre bandejas es ' minHeightBetweenTrays: 'La distancia mínima entre bandejas es '
maxWagonHeight: 'La altura máxima del vagón es ' maxWagonHeight: 'La altura máxima del vagón es '
uncompleteTrays: Hay bandejas sin completar uncompleteTrays: Hay bandejas sin completar
route:
cmr:
list:
results: resultados
cmrFk: Id CMR
hasCmrDms: Gestdoc
'true':
'false': 'No'
ticketFk: Id ticket
routeFk: Id ruta
country: País
clientFk: Id cliente
shipped: Fecha preparación
viewCmr: Ver CMR
downloadCmrs: Descargar CMRs
supplier: supplier:
list: list:
payMethod: Método de pago payMethod: Método de pago

View File

@ -10,7 +10,9 @@ import RightMenu from 'src/components/common/RightMenu.vue';
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const tableRef = ref(); const tableRef = ref();
const filter = {
include: { relation: 'role', scope: { fields: ['id', 'name'] } },
};
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
@ -22,7 +24,22 @@ const columns = computed(() => [
}, },
{ {
align: 'left', align: 'left',
name: 'username', name: 'roleFk',
label: t('role'),
columnFilter: {
component: 'select',
name: 'roleFk',
attrs: {
url: 'VnRoles',
optionValue: 'id',
optionLabel: 'name',
},
},
format: ({ role }, dashIfEmpty) => dashIfEmpty(role?.name),
},
{
align: 'left',
name: 'nickname',
label: t('Nickname'), label: t('Nickname'),
isTitle: true, isTitle: true,
component: 'input', component: 'input',
@ -91,6 +108,7 @@ const exprBuilder = (param, value) => {
:expr-builder="exprBuilder" :expr-builder="exprBuilder"
:label="t('account.search')" :label="t('account.search')"
:info="t('account.searchInfo')" :info="t('account.searchInfo')"
:filter="filter"
/> />
<RightMenu> <RightMenu>
<template #right-panel> <template #right-panel>
@ -101,6 +119,7 @@ const exprBuilder = (param, value) => {
ref="tableRef" ref="tableRef"
data-key="AccountUsers" data-key="AccountUsers"
url="VnUsers/preview" url="VnUsers/preview"
:filter="filter"
order="id DESC" order="id DESC"
:columns="columns" :columns="columns"
default-mode="table" default-mode="table"

View File

@ -4,9 +4,12 @@ import { computed, ref, toRefs } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useAcl } from 'src/composables/useAcl';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import VnConfirm from 'src/components/ui/VnConfirm.vue'; import VnConfirm from 'src/components/ui/VnConfirm.vue';
import VnChangePassword from 'src/components/common/VnChangePassword.vue';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
const $props = defineProps({ const $props = defineProps({
hasAccount: { hasAccount: {
type: Boolean, type: Boolean,
@ -62,6 +65,19 @@ async function sync() {
} }
</script> </script>
<template> <template>
<VnChangePassword
ref="changePassRef"
:ask-old-pass="true"
:submit-fn="
async (newPassword, oldPassword) => {
await axios.patch(`Accounts/change-password`, {
userId: entityId,
newPassword,
oldPassword,
});
}
"
/>
<VnConfirm <VnConfirm
v-model="showSyncDialog" v-model="showSyncDialog"
:message="t('account.card.actions.sync.message')" :message="t('account.card.actions.sync.message')"
@ -92,6 +108,17 @@ async function sync() {
/> />
</template> </template>
</VnConfirm> </VnConfirm>
<QItem
v-if="
entityId == account.id &&
useAcl().hasAny([{ model: 'Account', props: '*', accessType: 'WRITE' }])
"
v-ripple
clickable
@click="$refs.changePassRef.show()"
>
<QItemSection>{{ t('globals.changePass') }}</QItemSection>
</QItem>
<QItem <QItem
v-if="account.hasAccount" v-if="account.hasAccount"
v-ripple v-ripple
@ -138,6 +165,5 @@ async function sync() {
<QItem v-ripple clickable @click="showSyncDialog = true"> <QItem v-ripple clickable @click="showSyncDialog = true">
<QItemSection>{{ t('account.card.actions.sync.name') }}</QItemSection> <QItemSection>{{ t('account.card.actions.sync.name') }}</QItemSection>
</QItem> </QItem>
<QSeparator /> <QSeparator />
</template> </template>

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

@ -22,5 +22,6 @@ const noteFilter = computed(() => {
:filter="noteFilter" :filter="noteFilter"
:body="{ clientFk: route.params.id }" :body="{ clientFk: route.params.id }"
style="overflow-y: auto" style="overflow-y: auto"
:select-type="true"
/> />
</template> </template>

View File

@ -92,7 +92,7 @@ const onSubmit = async () => {
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
unpaidClient.value = true; unpaidClient.value = true;
} catch (error) { } catch (error) {
notify('errors.create', 'negative'); notify('errors.writeRequest', 'negative');
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }

View File

@ -3,14 +3,11 @@ import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import { useQuasar } from 'quasar';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnChangePassword from 'src/components/common/VnChangePassword.vue';
const { t } = useI18n(); const { t } = useI18n();
const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const canChangePassword = ref(0); const canChangePassword = ref(0);
@ -21,21 +18,11 @@ const filter = computed(() => {
}; };
}); });
const showChangePasswordDialog = () => {
quasar.dialog({
component: CustomerChangePassword,
componentProps: {
id: route.params.id,
},
});
};
async function hasCustomerRole() { async function hasCustomerRole() {
const { data } = await axios(`Clients/${route.params.id}/hasCustomerRole`); const { data } = await axios(`Clients/${route.params.id}/hasCustomerRole`);
canChangePassword.value = data; canChangePassword.value = data;
} }
</script> </script>
<template> <template>
<FormModel <FormModel
url="VnUsers/preview" url="VnUsers/preview"
@ -69,21 +56,29 @@ async function hasCustomerRole() {
</template> </template>
<template #moreActions> <template #moreActions>
<QBtn <QBtn
:label="t('Change password')" :label="t('globals.changePass')"
color="primary" color="primary"
icon="edit" icon="edit"
:disable="!canChangePassword" :disable="!canChangePassword"
@click="showChangePasswordDialog()" @click="$refs.changePassRef.show"
/> />
</template> </template>
</FormModel> </FormModel>
<VnChangePassword
ref="changePassRef"
:submit-fn="
async (newPass) => {
await axios.patch(`Clients/${$route.params.id}/setPassword`, {
newPassword: newPass,
});
}
"
/>
</template> </template>
<i18n> <i18n>
es: es:
Enable web access: Habilitar acceso web Enable web access: Habilitar acceso web
User: Usuario User: Usuario
Recovery email: Correo de recuperacion Recovery email: Correo de recuperacion
This email is used for user to regain access their account: Este correo electrónico se usa para que el usuario recupere el acceso a su cuenta This email is used for user to regain access their account: Este correo electrónico se usa para que el usuario recupere el acceso a su cuenta
Change password: Cambiar contraseña
</i18n> </i18n>

View File

@ -25,19 +25,31 @@ const { notify } = useNotify();
const { t } = useI18n(); const { t } = useI18n();
const newObservation = ref(null); const newObservation = ref(null);
const obsId = ref(null);
const onSubmit = async () => { const onSubmit = async () => {
try { try {
const data = $props.clients.map((item) => { if (!obsId.value)
return { clientFk: item.clientFk, text: newObservation.value }; obsId.value = (
}); await axios.get('ObservationTypes/findOne', {
await axios.post('ClientObservations', data); params: { filter: { where: { description: 'Finance' } } },
})
).data?.id;
const payload = { const bodyObs = $props.clients.map((item) => {
return {
clientFk: item.clientFk,
text: newObservation.value,
observationTypeFk: obsId.value,
};
});
await axios.post('ClientObservations', bodyObs);
const bodyObsMail = {
defaulters: $props.clients, defaulters: $props.clients,
observation: newObservation.value, observation: newObservation.value,
}; };
await axios.post('Defaulters/observationEmail', payload); await axios.post('Defaulters/observationEmail', bodyObsMail);
await $props.promise(); await $props.promise();

View File

@ -240,7 +240,6 @@ function handleLocation(data, location) {
class="row q-gutter-md q-mb-md" class="row q-gutter-md q-mb-md"
v-for="(note, index) in notes" v-for="(note, index) in notes"
> >
<div class="col">
<VnSelect <VnSelect
:label="t('Observation type')" :label="t('Observation type')"
:options="observationTypes" :options="observationTypes"
@ -249,17 +248,14 @@ function handleLocation(data, location) {
option-value="id" option-value="id"
v-model="note.observationTypeFk" v-model="note.observationTypeFk"
/> />
</div>
<div class="col">
<VnInput <VnInput
:label="t('Description')" :label="t('Description')"
:rules="validate('route.description')" :rules="validate('route.description')"
clearable clearable
v-model="note.description" v-model="note.description"
/> />
</div>
<div class="flex items-center">
<QIcon <QIcon
:style="{ flex: 0, 'align-self': $q.screen.gt.xs ? 'end' : 'center' }"
@click.stop="deleteNote(note.id, index)" @click.stop="deleteNote(note.id, index)"
class="cursor-pointer" class="cursor-pointer"
color="primary" color="primary"
@ -270,9 +266,7 @@ function handleLocation(data, location) {
{{ t('Remove note') }} {{ t('Remove note') }}
</QTooltip> </QTooltip>
</QIcon> </QIcon>
</div>
</VnRow> </VnRow>
<QBtn <QBtn
@click.stop="addNote()" @click.stop="addNote()"
class="cursor-pointer add-icon q-mt-md" class="cursor-pointer add-icon q-mt-md"

View File

@ -1,138 +0,0 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useDialogPluginComponent } from 'quasar';
import useNotify from 'src/composables/useNotify';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FetchData from 'src/components/FetchData.vue';
const { dialogRef } = useDialogPluginComponent();
const { notify } = useNotify();
const { t } = useI18n();
const $props = defineProps({
id: {
type: String,
required: true,
},
promise: {
type: Function,
required: true,
},
});
const userPasswords = ref({});
const closeButton = ref(null);
const isLoading = ref(false);
const newPassword = ref('');
const requestPassword = ref('');
const onSubmit = async () => {
isLoading.value = true;
if (newPassword.value !== requestPassword.value) {
notify(t("Passwords don't match"), 'negative');
isLoading.value = false;
return;
}
const payload = {
newPassword: newPassword.value,
};
try {
await axios.patch(`Clients/${$props.id}/setPassword`, payload);
} catch (error) {
notify('errors.create', 'negative');
} finally {
isLoading.value = false;
if (closeButton.value) closeButton.value.click();
}
};
</script>
<template>
<QDialog ref="dialogRef">
<FetchData
@on-fetch="(data) => (userPasswords = data[0])"
auto-load
url="UserPasswords"
/>
<QCard class="q-pa-lg">
<QCardSection>
<QForm @submit.prevent="onSubmit">
<span
ref="closeButton"
class="row justify-end close-icon"
v-close-popup
>
<QIcon name="close" size="sm" />
</span>
<VnRow class="row q-gutter-md q-mb-md" style="flex-direction: column">
<div class="col">
<VnInput
:label="t('New password')"
clearable
v-model="newPassword"
type="password"
>
<template #append>
<QIcon name="info" class="cursor-info">
<QTooltip>
{{
t('customer.card.passwordRequirements', {
...userPasswords,
})
}}
</QTooltip>
</QIcon>
</template>
</VnInput>
</div>
<div class="col">
<VnInput
:label="t('Request password')"
clearable
v-model="requestPassword"
type="password"
/>
</div>
</VnRow>
<div class="q-mt-lg row justify-end">
<QBtn
:disabled="isLoading"
:label="t('globals.cancel')"
:loading="isLoading"
class="q-ml-sm"
color="primary"
flat
type="reset"
v-close-popup
/>
<QBtn
:disabled="isLoading"
:label="t('Change password')"
:loading="isLoading"
color="primary"
type="submit"
/>
</div>
</QForm>
</QCardSection>
</QCard>
</QDialog>
</template>
<i18n>
es:
New password: Nueva contraseña
Request password: Repetir contraseña
Change password: Cambiar contraseña
Passwords don't match: Las contraseñas no coinciden
</i18n>

View File

@ -138,7 +138,7 @@ const onSubmit = async () => {
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
onDataSaved(data); onDataSaved(data);
} catch (error) { } catch (error) {
notify('errors.create', 'negative'); notify('errors.writeRequest', 'negative');
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }

View File

@ -32,6 +32,16 @@ const filter = {
}, },
{ relation: 'invoiceOut', scope: { fields: ['id'] } }, { relation: 'invoiceOut', scope: { fields: ['id'] } },
{ relation: 'agencyMode', scope: { fields: ['name'] } }, { relation: 'agencyMode', scope: { fields: ['name'] } },
{
relation: 'ticketSales',
scope: {
fields: ['id', 'concept', 'itemFk'],
include: { relation: 'item' },
scope: {
fields: ['id', 'name', 'itemPackingTypeFk'],
},
},
},
], ],
where: { clientFk: route.params.id }, where: { clientFk: route.params.id },
order: ['shipped DESC', 'id'], order: ['shipped DESC', 'id'],
@ -87,7 +97,12 @@ const columns = computed(() => [
label: t('Total'), label: t('Total'),
name: 'total', name: 'total',
}, },
{
jgallego marked this conversation as resolved Outdated

El otro día comenté contigo con Alex que el Windows open mejor no usarlo. aplica aqui?

El otro día comenté contigo con Alex que el Windows open mejor no usarlo. aplica aqui?

El viernes pasado comentamos que habia un composable openReport que hacia esto.
Para este caso concreto lo ideal seria usar la etiqueta a de HTML y la propiedad hRef, pero en este caso no aplica

El viernes pasado comentamos que habia un composable openReport que hacia esto. Para este caso concreto lo ideal seria usar la etiqueta a de HTML y la propiedad hRef, pero en este caso no aplica
align: 'left',
name: 'itemPackingTypeFk',
label: t('ticketSale.packaging'),
format: (row) => getItemPackagingType(row.ticketSales),
},
{ {
align: 'right', align: 'right',
label: '', label: '',
@ -142,6 +157,23 @@ const setShippedColor = (date) => {
const rowClick = ({ id }) => const rowClick = ({ id }) =>
window.open(router.resolve({ params: { id }, name: 'TicketSummary' }).href, '_blank'); window.open(router.resolve({ params: { id }, name: 'TicketSummary' }).href, '_blank');
const getItemPackagingType = (ticketSales) => {
if (!ticketSales?.length) return '-';
const packagingTypes = ticketSales.reduce((types, sale) => {
const { itemPackingTypeFk } = sale.item;
if (
!types.includes(itemPackingTypeFk) &&
(itemPackingTypeFk === 'H' || itemPackingTypeFk === 'V')
) {
types.push(itemPackingTypeFk);
}
return types;
}, []);
return dashIfEmpty(packagingTypes.join(', ') || '-');
};
</script> </script>
<template> <template>

View File

@ -11,6 +11,7 @@ import { toDate, toCurrency } from 'src/filters';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import axios from 'axios'; import axios from 'axios';
import FetchedTags from 'src/components/ui/FetchedTags.vue'; import FetchedTags from 'src/components/ui/FetchedTags.vue';
import VnToSummary from 'src/components/ui/VnToSummary.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -163,14 +164,12 @@ const fetchEntryBuys = async () => {
data-key="EntrySummary" data-key="EntrySummary"
> >
<template #header-left> <template #header-left>
<router-link <VnToSummary
v-if="route?.name !== 'EntrySummary'" v-if="route?.name !== 'EntrySummary'"
:to="{ name: 'EntrySummary', params: { id: entityId } }" :route-name="'EntrySummary'"
class="header link" :entity-id="entityId"
:href="entryUrl" :url="entryUrl"
> />
<QIcon name="open_in_new" color="white" size="sm" />
</router-link>
</template> </template>
<template #header> <template #header>
<span>{{ entry.id }} - {{ entry.supplier.nickname }}</span> <span>{{ entry.id }} - {{ entry.supplier.nickname }}</span>

View File

@ -0,0 +1,302 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState';
import { useQuasar } from 'quasar';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import FetchData from 'components/FetchData.vue';
import FormModelPopup from 'components/FormModelPopup.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnRow from 'components/ui/VnRow.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import EntryStockBoughtFilter from './EntryStockBoughtFilter.vue';
import VnTable from 'components/VnTable/VnTable.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import EntryStockBoughtDetail from 'src/pages/Entry/EntryStockBoughtDetail.vue';
const { t } = useI18n();
const quasar = useQuasar();
const state = useState();
const user = state.getUser();
const columns = [
{
align: 'left',
label: 'Id',
name: 'id',
isId: true,
columnFilter: false,
visible: false,
},
{
align: 'left',
name: 'workerFk',
label: t('Buyer'),
isTitle: true,
component: 'select',
cardVisible: true,
create: true,
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
where: { role: 'buyer' },
optionFilter: 'firstName',
optionLabel: 'name',
optionValue: 'id',
useLike: false,
},
columnFilter: false,
},
{
align: 'center',
label: t('Reserve'),
name: 'reserve',
columnFilter: false,
create: true,
component: 'number',
summation: true,
},
{
align: 'center',
label: t('Bought'),
name: 'bought',
summation: true,
cardVisible: true,
columnFilter: false,
},
{
align: 'left',
label: t('Date'),
name: 'dated',
component: 'date',
visible: false,
create: true,
},
{
align: 'left',
name: 'tableActions',
actions: [
{
title: t('View more details'),
icon: 'search',
isPrimary: true,
action: (row) => {
quasar.dialog({
component: EntryStockBoughtDetail,
componentProps: {
workerFk: row.workerFk,
dated: userParams.value.dated,
},
});
},
},
],
},
];
const fetchDataRef = ref();
const travelDialogRef = ref(false);
const tableRef = ref();
const travel = ref(null);
const userParams = ref({
dated: Date.vnNew(),
});
const filter = ref({
fields: ['id', 'm3', 'warehouseInFk'],
include: [
{
relation: 'warehouseIn',
scope: {
fields: ['code'],
},
},
],
where: {
shipped: (userParams.value.dated
? new Date(userParams.value.dated)
: Date.vnNew()
).setHours(0, 0, 0, 0),
m3: { neq: null },
},
});
const setUserParams = async ({ dated }) => {
const shipped = (dated ? new Date(dated) : Date.vnNew()).setHours(0, 0, 0, 0);
filter.value.where.shipped = shipped;
fetchDataRef.value?.fetch();
};
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>
<template #st-data>
<FetchData
ref="fetchDataRef"
url="Travels"
auto-load
:filter="filter"
@on-fetch="
(data) => {
travel = data.find(
(data) => data.warehouseIn?.code.toLowerCase() === 'vnh'
);
}
"
/>
<VnRow class="travel">
<div v-if="travel">
<span style="color: var(--vn-label-color)">
{{ t('Purchase Spaces') }}:
</span>
<span>
{{ travel?.m3 }}
</span>
<QBtn
v-if="travel?.m3"
style="max-width: 20%"
flat
icon="edit"
@click="openDialog()"
:title="t('Edit travel')"
color="primary"
/>
</div>
</VnRow>
</template>
</VnSubToolbar>
<QDialog v-model="travelDialogRef" :maximized="true" :class="['vn-row', 'wrap']">
<FormModelPopup
:url-update="`Travels/${travel?.id}`"
model="travel"
:title="t('Travel m3')"
:form-initial-data="{ id: travel?.id, m3: travel?.m3 }"
@on-data-saved="fetchDataRef.fetch()"
>
<template #form-inputs="{ data }">
<VnInput
v-model="data.id"
:label="t('id')"
type="number"
disable
readonly
/>
<VnInput v-model="data.m3" :label="t('m3')" type="number" />
</template>
</FormModelPopup>
</QDialog>
<RightMenu>
<template #right-panel>
<EntryStockBoughtFilter
data-key="StockBoughts"
@set-user-params="setUserParams"
/>
</template>
</RightMenu>
<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
:column-search="false"
>
<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="scss" scoped>
.travel {
margin-bottom: 0px;
}
.table-container {
display: flex;
justify-content: center;
}
.column {
display: flex;
flex-direction: column;
align-items: center;
min-width: 35%;
}
.text-negative {
color: $negative !important;
}
</style>
<i18n>
es:
Edit travel: Editar envío
Travel: Envíos
Purchase Spaces: Espacios de compra
Buyer: Comprador
Reserve: Reservado
Bought: Comprado
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

@ -0,0 +1,126 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnTable from 'components/VnTable/VnTable.vue';
import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
const { t } = useI18n();
const tableRef = ref();
const $props = defineProps({
workerFk: {
type: Number,
required: true,
},
dated: {
type: Date,
required: true,
},
});
const customUrl = `StockBoughts/getStockBoughtDetail?workerFk=${$props.workerFk}&date=${$props.dated}`;
const columns = [
{
align: 'left',
label: t('Entry'),
name: 'entryFk',
isTitle: true,
isId: true,
columnFilter: false,
},
{
align: 'left',
name: 'itemFk',
label: t('Item'),
columnFilter: false,
},
{
align: 'left',
label: t('Name'),
name: 'itemName',
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>
<div class="container">
<VnTable
ref="tableRef"
data-key="StockBoughtsDetail"
:url="customUrl"
order="itemName DESC"
:columns="columns"
:right-search="false"
:disable-infinite-scroll="true"
:disable-option="{ card: true }"
:limit="0"
auto-load
>
<template #column-entryFk="{ row }">
<span class="link">
{{ row?.entryFk }}
<EntryDescriptorProxy :id="row.entryFk" />
</span>
</template>
<template #column-itemName="{ row }">
<span class="link">
{{ row?.itemName }}
<ItemDescriptorProxy :id="row.itemFk" />
</span>
</template>
</VnTable>
</div>
</QDialog>
</template>
<style lang="css" scoped>
.container {
max-width: 50vw;
overflow: auto;
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>
es:
Buyer: Comprador
Reserve: Reservado
Bought: Comprado
More: Más
Date: Fecha
Entry: Entrada
Item: Artículo
Name: Nombre
Volume: Volumen
Packaging: Embalage
</i18n>

View File

@ -0,0 +1,63 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { onMounted } from 'vue';
import { useStateStore } from 'stores/useStateStore';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const stateStore = useStateStore();
const emit = defineEmits(['set-user-params']);
const setUserParams = (params) => {
emit('set-user-params', params);
};
onMounted(async () => {
stateStore.rightDrawer = true;
});
</script>
<template>
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
search-url="table"
@set-user-params="setUserParams"
>
<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 class="q-my-sm">
<QItemSection>
<VnInputDate
id="date"
v-model="params.dated"
:label="t('Date')"
is-outlined
/>
</QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
dated: Date
workerFk: Worker
es:
Date: Fecha
params:
dated: Date
workerFk: Trabajador
</i18n>

View File

@ -0,0 +1,72 @@
<script setup>
import { ref, computed, watch } from 'vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
const isLoading = ref(false);
const dateFrom = ref();
const dateTo = ref();
const optionsTo = computed(() => (date) => {
if (!dateFrom.value) return true;
return new Date(date) >= new Date(dateFrom.value);
});
watch(dateFrom, (newDateFrom) => {
if (dateTo.value && new Date(dateTo.value) < new Date(newDateFrom))
dateTo.value = newDateFrom;
});
const recalc = async () => {
const { notify } = useNotify();
const params = {
schema: 'bs',
params: [new Date(dateFrom.value), new Date(dateTo.value)],
};
try {
isLoading.value = true;
await axios.post('Applications/waste_addSales/execute-proc', params);
notify('wasteRecalc.recalcOk', 'positive');
} catch (err) {
console.error(err);
} finally {
isLoading.value = false;
}
};
</script>
<template>
<div class="q-pa-lg row justify-center">
<QCard class="bg-light" style="width: 300px">
<QCardSection>
<VnInputDate
class="q-mb-lg"
v-model="dateFrom"
:label="$t('globals.from')"
rounded
dense
/>
<VnInputDate
class="q-mb-lg"
v-model="dateTo"
:options="optionsTo"
:label="$t('globals.to')"
:disable="!dateFrom"
rounded
dense
/>
<QBtn
color="primary"
text-color="white"
:label="$t('globals.recalc')"
:loading="isLoading"
:disable="isLoading || !(dateFrom && dateTo)"
@click="recalc()"
/>
</QCardSection>
</QCard>
</div>
</template>

View File

@ -9,22 +9,27 @@ import VnTable from 'components/VnTable/VnTable.vue';
const { t } = useI18n(); const { t } = useI18n();
const quasar = useQuasar(); const quasar = useQuasar();
const params = {
daysOnward: 7,
daysAgo: 3,
};
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
name: 'id', name: 'id',
label: t('customer.extendedList.tableVisibleColumns.id'), label: t('myEntries.id'),
columnFilter: false, columnFilter: false,
isTitle: true, isTitle: true,
}, },
{ {
visible: false, visible: false,
align: 'right', align: 'right',
label: t('shipped'), label: t('myEntries.shipped'),
name: 'shipped', name: 'shipped',
columnFilter: { columnFilter: {
name: 'fromShipped', name: 'fromShipped',
label: t('fromShipped'), label: t('myEntries.fromShipped'),
component: 'date', component: 'date',
}, },
format: ({ shipped }) => toDate(shipped), format: ({ shipped }) => toDate(shipped),
@ -32,11 +37,11 @@ const columns = computed(() => [
{ {
visible: false, visible: false,
align: 'left', align: 'left',
label: t('shipped'), label: t('myEntries.shipped'),
name: 'shipped', name: 'shipped',
columnFilter: { columnFilter: {
name: 'toShipped', name: 'toShipped',
label: t('toShipped'), label: t('myEntries.toShipped'),
component: 'date', component: 'date',
}, },
format: ({ shipped }) => toDate(shipped), format: ({ shipped }) => toDate(shipped),
@ -44,14 +49,14 @@ const columns = computed(() => [
}, },
{ {
align: 'right', align: 'right',
label: t('shipped'), label: t('myEntries.shipped'),
name: 'shipped', name: 'shipped',
columnFilter: false, columnFilter: false,
format: ({ shipped }) => toDate(shipped), format: ({ shipped }) => toDate(shipped),
}, },
{ {
align: 'right', align: 'right',
label: t('landed'), label: t('myEntries.landed'),
name: 'landed', name: 'landed',
columnFilter: false, columnFilter: false,
format: ({ landed }) => toDate(landed), format: ({ landed }) => toDate(landed),
@ -59,26 +64,36 @@ const columns = computed(() => [
{ {
align: 'right', align: 'right',
label: t('globals.wareHouseIn'), label: t('myEntries.wareHouseIn'),
name: 'warehouseInFk', name: 'warehouseInFk',
format: (row) => row.warehouseInName, format: (row) => {
row.warehouseInName;
},
cardVisible: true, cardVisible: true,
columnFilter: { columnFilter: {
name: 'warehouseInFk',
label: t('myEntries.warehouseInFk'),
component: 'select', component: 'select',
attrs: { attrs: {
url: 'warehouses', url: 'warehouses',
fields: ['id', 'name'], fields: ['id', 'name'],
optionLabel: 'name', optionLabel: 'name',
optionValue: 'id', optionValue: 'id',
},
alias: 't', alias: 't',
},
inWhere: true, inWhere: true,
}, },
}, },
{ {
align: 'left', align: 'left',
label: t('globals.daysOnward'), label: t('myEntries.daysOnward'),
name: 'days', name: 'daysOnward',
visible: false,
},
{
align: 'left',
label: t('myEntries.daysAgo'),
name: 'daysAgo',
visible: false, visible: false,
}, },
{ {
@ -88,6 +103,7 @@ const columns = computed(() => [
{ {
title: t('printLabels'), title: t('printLabels'),
icon: 'print', icon: 'print',
isPrimary: true,
action: (row) => printBuys(row.id), action: (row) => printBuys(row.id),
}, },
], ],
@ -114,9 +130,11 @@ const printBuys = (rowId) => {
data-key="myEntriesList" data-key="myEntriesList"
url="Entries/filter" url="Entries/filter"
:columns="columns" :columns="columns"
:user-params="params"
default-mode="card" default-mode="card"
order="shipped DESC" order="shipped DESC"
auto-load auto-load
chip-locale="myEntries"
/> />
</template> </template>

View File

@ -6,9 +6,17 @@ entryFilter:
filter: filter:
search: General search search: General search
reference: Reference reference: Reference
landed: Landed myEntries:
shipped: Shipped id: ID
fromShipped: Shipped(from) landed: Landed
toShipped: Shipped(to) shipped: Shipped
printLabels: Print stickers fromShipped: Shipped(from)
viewLabel: View sticker toShipped: Shipped(to)
printLabels: Print stickers
viewLabel: View sticker
wareHouseIn: Warehouse in
warehouseInFk: Warehouse in
daysOnward: Days onward
daysAgo: Days ago
wasteRecalc:
recalcOk: The wastes were successfully recalculated

View File

@ -9,10 +9,17 @@ entryFilter:
filter: filter:
search: Búsqueda general search: Búsqueda general
reference: Referencia reference: Referencia
myEntries:
landed: F. llegada id: ID
shipped: F. salida landed: F. llegada
fromShipped: F. salida(desde) shipped: F. salida
toShipped: F. salida(hasta) fromShipped: F. salida(desde)
printLabels: Imprimir etiquetas toShipped: F. salida(hasta)
viewLabel: Ver etiqueta printLabels: Imprimir etiquetas
viewLabel: Ver etiqueta
wareHouseIn: Alm. entrada
warehouseInFk: Alm. entrada
daysOnward: Días adelante
daysAgo: Días atras
wasteRecalc:
recalcOk: Se han recalculado las mermas correctamente

View File

@ -274,10 +274,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
:label="t('invoiceIn.summary.company')" :label="t('invoiceIn.summary.company')"
:value="entity.company?.code" :value="entity.company?.code"
/> />
<VnLv <VnLv :label="t('invoiceIn.isBooked')" :value="invoiceIn?.isBooked" />
:label="t('invoiceIn.summary.booked')"
:value="invoiceIn?.isBooked"
/>
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<QCardSection class="q-pa-none"> <QCardSection class="q-pa-none">

View File

@ -116,7 +116,7 @@ const activities = ref([]);
<QItem> <QItem>
<QItemSection> <QItemSection>
<QCheckbox <QCheckbox
:label="t('params.isBooked')" :label="t('invoiceIn.isBooked')"
v-model="params.isBooked" v-model="params.isBooked"
@update:model-value="searchFn()" @update:model-value="searchFn()"
toggle-indeterminate toggle-indeterminate
@ -170,7 +170,7 @@ es:
awb: AWB awb: AWB
amount: Importe amount: Importe
issued: Emitida issued: Emitida
isBooked: Conciliada isBooked: Contabilizada
account: Cuenta contable account: Cuenta contable
created: Creada created: Creada
dued: Vencida dued: Vencida

View File

@ -65,7 +65,7 @@ const cols = computed(() => [
{ {
align: 'left', align: 'left',
name: 'isBooked', name: 'isBooked',
label: t('invoiceIn.list.isBooked'), label: t('invoiceIn.isBooked'),
columnFilter: false, columnFilter: false,
}, },
{ {

View File

@ -1,5 +1,6 @@
invoiceIn: invoiceIn:
serial: Serial serial: Serial
isBooked: Is booked
list: list:
ref: Reference ref: Reference
supplier: Supplier supplier: Supplier
@ -7,7 +8,6 @@ invoiceIn:
serial: Serial serial: Serial
file: File file: File
issued: Issued issued: Issued
isBooked: Is booked
awb: AWB awb: AWB
amount: Amount amount: Amount
card: card:
@ -31,7 +31,6 @@ invoiceIn:
sage: Sage withholding sage: Sage withholding
vat: Undeductible VAT vat: Undeductible VAT
company: Company company: Company
booked: Booked
expense: Expense expense: Expense
taxableBase: Taxable base taxableBase: Taxable base
rate: Rate rate: Rate

View File

@ -1,5 +1,6 @@
invoiceIn: invoiceIn:
serial: Serie serial: Serie
isBooked: Contabilizada
list: list:
ref: Referencia ref: Referencia
supplier: Proveedor supplier: Proveedor
@ -7,7 +8,6 @@ invoiceIn:
shortIssued: F. emisión shortIssued: F. emisión
file: Fichero file: Fichero
issued: Fecha emisión issued: Fecha emisión
isBooked: Conciliada
awb: AWB awb: AWB
amount: Importe amount: Importe
card: card:
@ -31,7 +31,6 @@ invoiceIn:
sage: Retención sage sage: Retención sage
vat: Iva no deducible vat: Iva no deducible
company: Empresa company: Empresa
booked: Contabilizada
expense: Gasto expense: Gasto
taxableBase: Base imp. taxableBase: Base imp.
rate: Tasa rate: Tasa

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import { useArrayData } from 'src/composables/useArrayData';
import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue';
import TicketDescriptorProxy from '../Ticket/Card/TicketDescriptorProxy.vue'; import TicketDescriptorProxy from '../Ticket/Card/TicketDescriptorProxy.vue';
import WorkerDescriptorProxy from '../Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from '../Worker/Card/WorkerDescriptorProxy.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
const { t } = useI18n(); const { t } = useI18n();
const tableRef = ref(); const tableRef = ref();
@ -64,7 +65,8 @@ const columns = computed(() => [
cardVisible: true, cardVisible: true,
attrs: { attrs: {
url: 'Clients', url: 'Clients',
fields: ['id', 'name'], optionLabel: 'socialName',
optionValue: 'socialName',
}, },
columnField: { columnField: {
component: null, component: null,
@ -191,10 +193,33 @@ const downloadCSV = async () => {
<WorkerDescriptorProxy :id="row.comercialId" /> <WorkerDescriptorProxy :id="row.comercialId" />
</span> </span>
</template> </template>
<template #moreFilterPanel="{ params }">
<VnInputDate
:label="t('params.from')"
v-model="params.from"
class="q-px-xs q-pr-lg"
filled
dense
/>
<VnInputDate
:label="t('params.to')"
v-model="params.to"
class="q-px-xs q-pr-lg"
filled
dense
/>
</template>
</VnTable> </VnTable>
</template> </template>
<i18n> <i18n>
es: es:
Download as CSV: Descargar como CSV Download as CSV: Descargar como CSV
params:
from: Desde
to: Hasta
en:
params:
from: From
to: To
</i18n> </i18n>

View File

@ -514,7 +514,7 @@ function handleOnDataSave({ CrudModelRef }) {
</template> </template>
<template #column-minPrice="props"> <template #column-minPrice="props">
<QTd class="col"> <QTd class="col">
<div class="row"> <div class="row" style="align-items: center">
<QCheckbox <QCheckbox
:model-value="props.row.hasMinPrice" :model-value="props.row.hasMinPrice"
@update:model-value="updateMinPrice($event, props)" @update:model-value="updateMinPrice($event, props)"
@ -600,11 +600,14 @@ function handleOnDataSave({ CrudModelRef }) {
.q-table th, .q-table th,
.q-table td { .q-table td {
padding-inline: 5px !important; 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 { .q-table tbody td {
max-width: none; max-width: none;
.q-td.col { .q-td.col {
& .vnInputDate { & .vnInputDate {
min-width: 90px; min-width: 90px;

View File

@ -229,7 +229,7 @@ onBeforeMount(() => {
> >
<template #body-cell-id="{ row }"> <template #body-cell-id="{ row }">
<QTd> <QTd>
<QBtn flat color="primary"> {{ row.ticketFk }}</QBtn> <QBtn flat class="link"> {{ row.ticketFk }}</QBtn>
<TicketDescriptorProxy :id="row.ticketFk" /> <TicketDescriptorProxy :id="row.ticketFk" />
</QTd> </QTd>
</template> </template>
@ -251,7 +251,7 @@ onBeforeMount(() => {
</template> </template>
<template #body-cell-requester="{ row }"> <template #body-cell-requester="{ row }">
<QTd> <QTd>
<QBtn flat dense color="primary"> {{ row.requesterName }}</QBtn> <QBtn flat dense class="link"> {{ row.requesterName }}</QBtn>
<WorkerDescriptorProxy :id="row.requesterFk" /> <WorkerDescriptorProxy :id="row.requesterFk" />
</QTd> </QTd>
</template> </template>
@ -292,7 +292,7 @@ onBeforeMount(() => {
</template> </template>
<template #body-cell-concept="{ row }"> <template #body-cell-concept="{ row }">
<QTd> <QTd>
<QBtn flat dense color="primary"> {{ row.itemDescription }}</QBtn> <QBtn flat dense class="link"> {{ row.itemDescription }}</QBtn>
<ItemDescriptorProxy :id="row.itemFk" /> <ItemDescriptorProxy :id="row.itemFk" />
</QTd> </QTd>
</template> </template>

View File

@ -174,6 +174,16 @@ const decrement = (paramsObj, key) => {
</VnSelect> </VnSelect>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem>
<QItemSection>
<QCheckbox
v-model="params.myTeam"
:label="t('params.myTeam')"
@update:model-value="searchFn()"
toggle-indeterminate
/>
</QItemSection>
</QItem>
<QCard bordered> <QCard bordered>
<QItem> <QItem>
<QItemSection> <QItemSection>
@ -274,11 +284,11 @@ en:
to: To to: To
mine: For me mine: For me
state: State state: State
myTeam: My team
dateFiltersTooltip: Cannot choose a range of dates and days onward at the same time dateFiltersTooltip: Cannot choose a range of dates and days onward at the same time
denied: Denied denied: Denied
accepted: Accepted accepted: Accepted
pending: Pending pending: Pending
es: es:
params: params:
search: Búsqueda general search: Búsqueda general
@ -291,6 +301,7 @@ es:
to: Hasta to: Hasta
mine: Para mi mine: Para mi
state: Estado state: Estado
myTeam: Mi equipo
dateFiltersTooltip: No se puede seleccionar un rango de fechas y días en adelante a la vez dateFiltersTooltip: No se puede seleccionar un rango de fechas y días en adelante a la vez
denied: Denegada denied: Denegada
accepted: Aceptada accepted: Aceptada

View File

@ -6,6 +6,7 @@ import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.v
import CardSummary from 'components/ui/CardSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import VnToSummary from 'src/components/ui/VnToSummary.vue';
onUpdated(() => summaryRef.value.fetch()); onUpdated(() => summaryRef.value.fetch());
@ -55,6 +56,11 @@ async function setItemTypeData(data) {
> >
<QIcon name="open_in_new" color="white" size="sm" /> <QIcon name="open_in_new" color="white" size="sm" />
</router-link> </router-link>
<VnToSummary
v-if="route?.name !== 'ItemTypeSummary'"
:route-name="'ItemTypeSummary'"
:entity-id="entityId"
/>
</template> </template>
<template #header> <template #header>
<span> <span>

View File

@ -139,6 +139,7 @@ const openTab = (id) =>
:disable-option="{ card: true }" :disable-option="{ card: true }"
> >
<template #top-left> <template #top-left>
<div class="q-mt-sm">
<QBtn <QBtn
icon="refresh" icon="refresh"
size="md" size="md"
@ -158,7 +159,7 @@ const openTab = (id) =>
color="primary" color="primary"
@click=" @click="
openConfirmationModal( openConfirmationModal(
$t('salesOrdersTable.deleteConfirmTitle'), $t('globals.deleteConfirmTitle'),
$t('salesOrdersTable.deleteConfirmMessage'), $t('salesOrdersTable.deleteConfirmMessage'),
removeOrders removeOrders
) )
@ -166,6 +167,7 @@ const openTab = (id) =>
> >
<QTooltip>{{ t('salesOrdersTable.delete') }}</QTooltip> <QTooltip>{{ t('salesOrdersTable.delete') }}</QTooltip>
</QBtn> </QBtn>
</div>
</template> </template>
<template #column-dateSend="{ row }"> <template #column-dateSend="{ row }">
<QTd> <QTd>

View File

@ -15,7 +15,6 @@ salesOrdersTable:
dateMake: Make date dateMake: Make date
client: Client client: Client
salesPerson: Salesperson salesPerson: Salesperson
deleteConfirmTitle: Delete selected elements
deleteConfirmMessage: All the selected elements will be deleted. Are you sure you want to continue? deleteConfirmMessage: All the selected elements will be deleted. Are you sure you want to continue?
agency: Agency agency: Agency
import: Import import: Import

View File

@ -15,7 +15,6 @@ salesOrdersTable:
dateMake: Fecha de realización dateMake: Fecha de realización
client: Cliente client: Cliente
salesPerson: Comercial salesPerson: Comercial
deleteConfirmTitle: Eliminar los elementos seleccionados
deleteConfirmMessage: Todos los elementos seleccionados serán eliminados. ¿Seguro que quieres continuar? deleteConfirmMessage: Todos los elementos seleccionados serán eliminados. ¿Seguro que quieres continuar?
agency: Agencia agency: Agencia
import: Importe import: Importe

View File

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

View File

@ -14,6 +14,7 @@ import OrderFilter from './Card/OrderFilter.vue';
import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from '../Customer/Card/CustomerDescriptorProxy.vue';
import WorkerDescriptorProxy from '../Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from '../Worker/Card/WorkerDescriptorProxy.vue';
import { toDateTimeFormat } from 'src/filters/date'; import { toDateTimeFormat } from 'src/filters/date';
import { onMounted } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
const { t } = useI18n(); const { t } = useI18n();
@ -22,7 +23,6 @@ const tableRef = ref();
const agencyList = ref([]); const agencyList = ref([]);
const addressesList = ref([]); const addressesList = ref([]);
jsegarra marked this conversation as resolved Outdated
Outdated
Review

Si ya no se usa, quitar

Si ya no se usa, quitar
const route = useRoute(); const route = useRoute();
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
@ -175,6 +175,13 @@ const getDateColor = (date) => {
if (comparation == 0) return 'bg-warning'; if (comparation == 0) return 'bg-warning';
if (comparation < 0) return 'bg-success'; 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> </script>
<template> <template>
<OrderSearchbar /> <OrderSearchbar />
@ -190,7 +197,7 @@ const getDateColor = (date) => {
:order="['landed DESC', 'clientFk ASC', 'id DESC']" :order="['landed DESC', 'clientFk ASC', 'id DESC']"
:create="{ :create="{
urlCreate: 'Orders/new', urlCreate: 'Orders/new',
title: 'Create Order', title: t('module.cerateOrder'),
onDataSaved: (url) => { onDataSaved: (url) => {
tableRef.redirect(url); tableRef.redirect(url);
}, },

View File

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

View File

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

View File

@ -27,12 +27,15 @@ const columns = computed(() => [
condition: () => true, condition: () => true,
}, },
isId: true, isId: true,
columnFilter: false,
}, },
{ {
align: 'left', align: 'left',
label: t('globals.name'), label: t('globals.name'),
name: 'name', name: 'name',
isTitle: true, isTitle: true,
columnFilter: false,
columnClass: 'expand',
}, },
{ {
align: 'left', align: 'left',
@ -70,8 +73,9 @@ const columns = computed(() => [
data-key="AgencyList" data-key="AgencyList"
:expr-builder="exprBuilder" :expr-builder="exprBuilder"
/> />
<div class="list-container">
<div class="list">
<VnTable <VnTable
ref="tableRef"
data-key="AgencyList" data-key="AgencyList"
url="Agencies" url="Agencies"
order="name" order="name"
@ -81,7 +85,21 @@ const columns = computed(() => [
redirect="agency" redirect="agency"
default-mode="card" default-mode="card"
/> />
</div>
</div>
</template> </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> <i18n>
es: es:
isOwn: Tiene propietario isOwn: Tiene propietario

View File

@ -46,13 +46,13 @@ async function deleteWorCenter(id) {
} }
</script> </script>
<template> <template>
<div class="centerCard">
<FetchData <FetchData
url="workCenters" url="workCenters"
sort-by="name" sort-by="name"
@on-fetch="(data) => (warehouses = data)" @on-fetch="(data) => (warehouses = data)"
auto-load auto-load
/> />
<div class="centerCard">
<VnPaginate <VnPaginate
ref="paginate" ref="paginate"
data-key="AgencyWorkCenters" data-key="AgencyWorkCenters"

View File

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

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

@ -2,31 +2,18 @@
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useQuasar } from 'quasar'; import { toHour } from 'src/filters';
import { toDate } from 'src/filters';
import { useRouter } from 'vue-router';
import axios from 'axios';
import RouteSearchbar from 'pages/Route/Card/RouteSearchbar.vue'; 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 RouteSummary from 'pages/Route/Card/RouteSummary.vue';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import RouteFilter from 'pages/Route/Card/RouteFilter.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 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 { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const quasar = useQuasar();
const selectedRows = ref([]);
const tableRef = ref([]); const tableRef = ref([]);
const confirmationDialog = ref(false);
const startingDate = ref(null);
const router = useRouter();
const routeFilter = { const routeFilter = {
include: [ include: [
{ {
@ -40,156 +27,70 @@ const routeFilter = {
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
isId: true,
name: 'id', name: 'id',
label: 'Id', label: 'Id',
chip: { chip: {
condition: () => true, condition: () => true,
}, },
isId: true,
columnFilter: false, columnFilter: false,
}, },
{ {
align: 'left', align: 'left',
name: 'workerFk', name: 'workerFk',
label: t('Worker'), label: t('route.Worker'),
create: true, 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, cardVisible: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), 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, columnFilter: false,
cardVisible: true,
create: true,
component: 'date',
format: ({ date }) => toDate(date),
}, },
{ {
align: 'left', align: 'left',
name: 'from', name: 'agencyName',
label: t('From'), label: t('route.Agency'),
visible: false,
cardVisible: true, cardVisible: true,
create: true, create: true,
component: 'date', columnClass: 'expand',
format: ({ date }) => toDate(date), columnFilter: false,
}, },
{ {
align: 'left', align: 'left',
name: 'to', name: 'vehiclePlateNumber',
label: t('To'), label: t('route.Vehicle'),
visible: false,
cardVisible: true, cardVisible: true,
create: true, create: true,
component: 'date', columnFilter: false,
format: ({ date }) => toDate(date),
},
{
align: 'center',
name: 'm3',
label: t('Volume'),
cardVisible: true,
columnClass: 'shrink',
}, },
{ {
align: 'left', align: 'left',
name: 'started', name: 'started',
label: t('hourStarted'), label: t('route.hourStarted'),
component: 'time', cardVisible: true,
columnFilter: false, columnFilter: false,
format: (row) => toHour(row.started),
}, },
{ {
align: 'left', align: 'left',
name: 'finished', name: 'finished',
label: t('hourFinished'), label: t('route.hourFinished'),
component: 'time', cardVisible: true,
columnFilter: false, columnFilter: false,
}, format: (row) => toHour(row.started),
{
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,
}, },
{ {
align: 'left', align: 'left',
name: 'description', name: 'description',
label: t('Description'), label: t('route.Description'),
cardVisible: true,
isTitle: true, isTitle: true,
create: true, create: true,
component: 'input',
field: 'description', field: 'description',
columnFilter: false,
}, },
{ {
align: 'left', align: 'left',
name: 'isOk', name: 'isOk',
label: t('Served'), label: t('route.Served'),
component: 'checkbox', component: 'checkbox',
columnFilter: false, columnFilter: false,
columnClass: 'shrink', columnClass: 'shrink',
@ -198,212 +99,43 @@ const columns = computed(() => [
align: 'right', align: 'right',
name: 'tableActions', name: 'tableActions',
actions: [ actions: [
{
title: t('Add tickets'),
icon: 'vn:ticketAdd',
action: (row) => openTicketsDialog(row?.id),
},
{ {
title: t('components.smartCard.viewSummary'), title: t('components.smartCard.viewSummary'),
icon: 'preview', icon: 'preview',
action: (row) => viewSummary(row?.id, RouteSummary), action: (row) => viewSummary(row?.id, RouteSummary),
},
{
title: t('Route summary'),
icon: 'arrow_forward',
isPrimary: true, 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> </script>
<template> <template>
<RouteSearchbar /> <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> <RightMenu>
<template #right-panel> <template #right-panel>
<RouteFilter data-key="RouteList" /> <RouteFilter data-key="RouteList" />
</template> </template>
</RightMenu> </RightMenu>
<VnTable <VnTable
class="route-list"
ref="tableRef"
data-key="RouteList" data-key="RouteList"
url="Routes/filter" url="Routes/filter"
:columns="columns" :columns="columns"
:right-search="false" :right-search="false"
:is-editable="true"
:filter="routeFilter" :filter="routeFilter"
redirect="route" redirect="route"
:row-click="false"
:create="{ :create="{
urlCreate: 'Routes', urlCreate: 'Routes',
title: t('Create route'), title: t('route.createRoute'),
onDataSaved: ({ id }) => tableRef.redirect(id), onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {}, formInitialData: {},
}" }"
save-url="Routes/crud"
:disable-option="{ card: true }"
table-height="85vh" table-height="85vh"
v-model:selected="selectedRows"
:table="{
'row-key': 'id',
selection: 'multiple',
}"
> >
<template #moreBeforeActions> <template #column-workerFk="{ row }">
<QBtn <span class="link" @click.stop>
icon="vn:clone" {{ row?.workerUserName }}
color="primary" <WorkerDescriptorProxy :id="row?.workerFk" v-if="row?.workerFk" />
class="q-mr-sm" </span>
: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> </template>
</VnTable> </VnTable>
</template> </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: [ actions: [
{ {
title: t('Ver cmr'), title: t('Ver cmr'),
icon: 'visibility', icon: 'preview',
isPrimary: true, isPrimary: true,
action: (row) => viewSummary(row?.id, RoadmapSummary), action: (row) => viewSummary(row?.id, RoadmapSummary),
}, },

View File

@ -342,10 +342,7 @@ const openSmsDialog = async () => {
</template> </template>
<template #body-cell-city="{ value, row }"> <template #body-cell-city="{ value, row }">
<QTd auto-width> <QTd auto-width>
<span <span class="link" @click="goToBuscaman(row)">
class="text-primary cursor-pointer"
@click="goToBuscaman(row)"
>
{{ value }} {{ value }}
<QTooltip>{{ t('Open buscaman') }}</QTooltip> <QTooltip>{{ t('Open buscaman') }}</QTooltip>
</span> </span>
@ -353,7 +350,7 @@ const openSmsDialog = async () => {
</template> </template>
<template #body-cell-client="{ value, row }"> <template #body-cell-client="{ value, row }">
<QTd auto-width> <QTd auto-width>
<span class="text-primary cursor-pointer"> <span class="link">
{{ value }} {{ value }}
<CustomerDescriptorProxy :id="row?.clientFk" /> <CustomerDescriptorProxy :id="row?.clientFk" />
</span> </span>
@ -361,7 +358,7 @@ const openSmsDialog = async () => {
</template> </template>
<template #body-cell-ticket="{ value, row }"> <template #body-cell-ticket="{ value, row }">
<QTd auto-width class="text-center"> <QTd auto-width class="text-center">
<span class="text-primary cursor-pointer"> <span class="link">
{{ value }} {{ value }}
<TicketDescriptorProxy :id="row?.id" /> <TicketDescriptorProxy :id="row?.id" />
</span> </span>

View File

@ -0,0 +1,39 @@
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
cmr:
list:
results: results
cmrFk: CMR id
hasCmrDms: Attached in gestdoc
'true': 'Yes'
'false': 'No'
ticketFk: Ticketd id
routeFk: Route id
country: Country
clientFk: Client id
shipped: Preparation date
viewCmr: View CMR
downloadCmrs: Download CMRs

View File

@ -0,0 +1,39 @@
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
cmr:
list:
results: resultados
cmrFk: Id CMR
hasCmrDms: Gestdoc
'true':
'false': 'No'
ticketFk: Id ticket
routeFk: Id ruta
country: País
clientFk: Id cliente
shipped: Fecha preparación
viewCmr: Ver CMR
downloadCmrs: Descargar CMRs

View File

@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import FetchedTags from 'components/ui/FetchedTags.vue'; import FetchedTags from 'components/ui/FetchedTags.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
@ -115,7 +114,7 @@ const totalNewPrice = computed(() => {
const totalDifference = computed(() => { const totalDifference = computed(() => {
return rows.value.reduce((acc, item) => acc + item.component?.difference || 0, 0); 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 haveDifferences = computed(() => _ticketData.value.sale?.haveDifferences);
const ticketHaveNegatives = () => { const ticketHaveNegatives = () => {
let _haveNegatives = false; let _haveNegatives = false;
@ -145,8 +144,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
@on-fetch="(data) => (ticketUpdateActions = data)" @on-fetch="(data) => (ticketUpdateActions = data)"
auto-load auto-load
/> />
<RightMenu> <QDrawer side="right" :width="265" v-model="stateStore.rightDrawer">
<template #right-panel>
<QCard <QCard
class="q-pa-md q-mb-md q-ma-md color-vn-text" class="q-pa-md q-mb-md q-ma-md color-vn-text"
bordered bordered
@ -158,18 +156,18 @@ onUnmounted(() => (stateStore.rightDrawer = false));
{{ t('basicData.total') }} {{ t('basicData.total') }}
</span> </span>
</QCardSection> </QCardSection>
<QCardSection class="column items-center" horizontal> <QCardSection class="column items-left" horizontal>
<span> <span>
{{ t('basicData.price') }}: {{ t('basicData.price') }}:
{{ toCurrency(totalPrice) }} {{ toCurrency(totalPrice) }}
</span> </span>
</QCardSection> </QCardSection>
<QCardSection class="column items-center" horizontal> <QCardSection class="column items-left" horizontal>
<span> <span>
{{ t('basicData.newPrice') }}: {{ toCurrency(totalNewPrice) }} {{ t('basicData.newPrice') }}: {{ toCurrency(totalNewPrice) }}
</span> </span>
</QCardSection> </QCardSection>
<QCardSection class="column items-center" horizontal> <QCardSection class="column items-left" horizontal>
<span> <span>
{{ t('basicData.difference') }}: {{ toCurrency(totalDifference) }} {{ t('basicData.difference') }}: {{ toCurrency(totalDifference) }}
</span> </span>
@ -220,10 +218,9 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</QIcon> </QIcon>
</QCardSection> </QCardSection>
</QCard> </QCard>
</template> </QDrawer>
</RightMenu>
<QTable <QTable
:visible-columns="showMovablecolumn" :visible-columns="showMovableColumn"
:rows="rows" :rows="rows"
:columns="columns" :columns="columns"
row-key="id" row-key="id"
@ -233,15 +230,15 @@ onUnmounted(() => (stateStore.rightDrawer = false));
flat flat
> >
<template #body-cell-item="{ row }"> <template #body-cell-item="{ row }">
<QTd> <QTd @click.stop class="link">
<QBtn flat color="primary"> <QBtn flat>
{{ row.itemFk }} {{ row.itemFk }}
<ItemDescriptorProxy :id="row.itemFk" /> <ItemDescriptorProxy :id="row.itemFk" />
</QBtn> </QBtn>
</QTd> </QTd>
</template> </template>
<template #body-cell-description="{ row }"> <template #body-cell-description="{ row }">
<QTd> <QTd style="display: contents">
<div class="column"> <div class="column">
<span>{{ row.item.name }}</span> <span>{{ row.item.name }}</span>
<span class="color-vn-label">{{ row.item.subName }}</span> <span class="color-vn-label">{{ row.item.subName }}</span>

View File

@ -12,6 +12,8 @@ import VnInputTime from 'components/common/VnInputTime.vue';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useAcl } from 'src/composables/useAcl';
import { useValidator } from 'src/composables/useValidator';
import { toTimeFormat } from 'filters/date.js'; import { toTimeFormat } from 'filters/date.js';
const $props = defineProps({ const $props = defineProps({
@ -23,18 +25,21 @@ const $props = defineProps({
}); });
const emit = defineEmits(['updateForm']); const emit = defineEmits(['updateForm']);
const { validate } = useValidator();
const { notify } = useNotify(); const { notify } = useNotify();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const agencyFetchRef = ref(null); const canEditZone = useAcl().hasAny([
const zonesFetchRef = ref(null); { model: 'Ticket', props: 'editZone', accessType: 'WRITE' },
]);
const agencyFetchRef = ref();
const warehousesOptions = ref([]); const warehousesOptions = ref([]);
const companiesOptions = ref([]); const companiesOptions = ref([]);
const agenciesOptions = ref([]); const agenciesOptions = ref([]);
const zonesOptions = ref([]); const zonesOptions = ref([]);
const addresses = ref([]); const addresses = ref([]);
const zoneSelectRef = ref();
const formData = ref($props.formData); const formData = ref($props.formData);
watch( watch(
@ -43,6 +48,8 @@ watch(
{ deep: true } { deep: true }
); );
onMounted(() => onFormModelInit());
const agencyByWarehouseFilter = computed(() => ({ const agencyByWarehouseFilter = computed(() => ({
fields: ['id', 'name'], fields: ['id', 'name'],
order: 'name ASC', order: 'name ASC',
@ -51,18 +58,16 @@ const agencyByWarehouseFilter = computed(() => ({
}, },
})); }));
const zonesFilter = computed(() => ({ const zoneWhere = computed(() => {
fields: ['id', 'name'], return formData.value?.agencyModeFk
order: 'name ASC',
where: formData.value?.agencyModeFk
? { ? {
shipped: formData.value?.shipped, shipped: formData.value?.shipped,
addressFk: formData.value?.addressFk, addressFk: formData.value?.addressFk,
agencyModeFk: formData.value?.agencyModeFk, agencyModeFk: formData.value?.agencyModeFk,
warehouseFk: formData.value?.warehouseFk, warehouseFk: formData.value?.warehouseFk,
} }
: {}, : {};
})); });
const getLanded = async (params) => { const getLanded = async (params) => {
try { try {
@ -269,7 +274,17 @@ const redirectToCustomerAddress = () => {
}); });
}; };
onMounted(() => onFormModelInit()); async function getZone(options) {
if (!zoneId.value) return;
const zone = options.find((z) => z.id == zoneId.value);
if (zone) return;
const { data } = await axios.get('Zones/' + zoneId.value, {
params: { filter: JSON.stringify({ fields: ['id', 'name'] }) },
});
zoneSelectRef.value.opts.push(data);
}
</script> </script>
<template> <template>
<FetchData <FetchData
@ -293,13 +308,6 @@ onMounted(() => onFormModelInit());
@on-fetch="(data) => (agenciesOptions = data)" @on-fetch="(data) => (agenciesOptions = data)"
auto-load auto-load
/> />
<FetchData
ref="zonesFetchRef"
url="Zones/includingExpired"
:filter="zonesFilter"
@on-fetch="(data) => (zonesOptions = data)"
auto-load
/>
<QForm> <QForm>
<VnRow> <VnRow>
<VnSelect <VnSelect
@ -313,6 +321,7 @@ onMounted(() => onFormModelInit());
hide-selected hide-selected
map-options map-options
:required="true" :required="true"
:rules="validate('basicData.client')"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -333,6 +342,7 @@ onMounted(() => onFormModelInit());
hide-selected hide-selected
map-options map-options
:required="true" :required="true"
:rules="validate('basicData.warehouse')"
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>
@ -345,6 +355,7 @@ onMounted(() => onFormModelInit());
hide-selected hide-selected
map-options map-options
:required="true" :required="true"
:rules="validate('basicData.address')"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -392,6 +403,7 @@ onMounted(() => onFormModelInit());
:label="t('basicData.alias')" :label="t('basicData.alias')"
v-model="formData.nickname" v-model="formData.nickname"
:required="true" :required="true"
:rules="validate('basicData.alias')"
/> />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md no-wrap"> <VnRow class="row q-gutter-md q-mb-md no-wrap">
@ -404,6 +416,7 @@ onMounted(() => onFormModelInit());
hide-selected hide-selected
map-options map-options
:required="true" :required="true"
:rules="validate('basicData.company')"
/> />
<VnSelect <VnSelect
:label="t('basicData.agency')" :label="t('basicData.agency')"
@ -414,17 +427,22 @@ onMounted(() => onFormModelInit());
hide-selected hide-selected
map-options map-options
@focus="agencyFetchRef.fetch()" @focus="agencyFetchRef.fetch()"
:rules="validate('basicData.agency')"
/> />
<VnSelect <VnSelect
ref="zoneSelectRef"
:label="t('basicData.zone')" :label="t('basicData.zone')"
v-model="zoneId" v-model="zoneId"
option-value="id" option-value="id"
option-label="name" option-label="name"
:options="zonesOptions" url="Zones/includingExpired"
hide-selected :fields="['id', 'name']"
map-options sort-by="id"
:where="zoneWhere"
:rules="validate('basicData.zone')"
:required="true" :required="true"
@focus="zonesFetchRef.fetch()" :disable="!canEditZone"
@update:options="getZone"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -444,16 +462,19 @@ onMounted(() => onFormModelInit());
:label="t('basicData.shipped')" :label="t('basicData.shipped')"
v-model="formData.shipped" v-model="formData.shipped"
:required="true" :required="true"
:rules="validate('basicData.shipped')"
/> />
<VnInputTime <VnInputTime
:label="t('basicData.shippedHour')" :label="t('basicData.shippedHour')"
v-model="formData.shipped" v-model="formData.shipped"
:required="true" :required="true"
:rules="validate('basicData.shippedHour')"
/> />
<VnInputDate <VnInputDate
:label="t('basicData.landed')" :label="t('basicData.landed')"
v-model="formData.landed" v-model="formData.landed"
:required="true" :required="true"
:rules="validate('basicData.landed')"
/> />
</VnRow> </VnRow>
</QForm> </QForm>

View File

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

View File

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

View File

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

View File

@ -2,13 +2,13 @@
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate } from 'src/filters';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import CardDescriptor from 'components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
import TicketDescriptorMenu from './TicketDescriptorMenu.vue'; import TicketDescriptorMenu from './TicketDescriptorMenu.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import { toDateTimeFormat } from 'src/filters/date';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -30,13 +30,24 @@ const filter = {
{ {
relation: 'address', relation: 'address',
scope: { scope: {
fields: ['id', 'name', 'mobile', 'phone'], fields: ['id', 'name', 'mobile', 'phone', 'incotermsFk'],
}, },
}, },
{ {
relation: 'client', relation: 'client',
scope: { scope: {
fields: ['id', 'name', 'salesPersonFk', 'phone', 'mobile', 'email'], fields: [
'id',
'name',
'salesPersonFk',
'phone',
'mobile',
'email',
'isActive',
'isFreezed',
'isTaxDataChecked',
'hasElectronicInvoice',
],
include: [ include: [
{ {
relation: 'user', relation: 'user',
@ -87,6 +98,10 @@ const filter = {
}; };
const data = ref(useCardDescription()); const data = ref(useCardDescription());
function ticketFilter(ticket) {
return JSON.stringify({ clientFk: ticket.clientFk });
}
</script> </script>
<template> <template>
@ -128,7 +143,10 @@ const data = ref(useCardDescription());
/> />
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('ticket.card.shipped')" :value="toDate(entity.shipped)" /> <VnLv
:label="t('ticket.card.shipped')"
:value="toDateTimeFormat(entity.shipped)"
/>
<VnLv <VnLv
v-if="entity.agencyMode" v-if="entity.agencyMode"
:label="t('ticket.card.agency')" :label="t('ticket.card.agency')"
@ -138,7 +156,39 @@ const data = ref(useCardDescription());
<VnLv :label="t('ticket.card.alias')" :value="entity.nickname" /> <VnLv :label="t('ticket.card.alias')" :value="entity.nickname" />
</template> </template>
<template #icons="{ entity }"> <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 <QIcon
v-if="entity.isDeleted == true" v-if="entity.isDeleted == true"
name="vn:deletedTicket" name="vn:deletedTicket"
@ -159,6 +209,27 @@ const data = ref(useCardDescription());
> >
<QTooltip>{{ t('ticket.card.customerCard') }}</QTooltip> <QTooltip>{{ t('ticket.card.customerCard') }}</QTooltip>
</QBtn> </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> </QCardActions>
</template> </template>
</CardDescriptor> </CardDescriptor>
@ -168,4 +239,8 @@ const data = ref(useCardDescription());
es: es:
This ticket is deleted: Este ticket está eliminado This ticket is deleted: Este ticket está eliminado
Go to module index: Ir al índice del modulo 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> </i18n>

View File

@ -1,14 +1,19 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { ref } from 'vue'; import { ref, toRefs } from 'vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { usePrintService } from 'composables/usePrintService'; import { usePrintService } from 'composables/usePrintService';
import SendEmailDialog from 'components/common/SendEmailDialog.vue'; import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import VnSmsDialog from 'components/common/VnSmsDialog.vue'; import VnSmsDialog from 'components/common/VnSmsDialog.vue';
import toDate from 'filters/toDate'; 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 VnInputNumber from 'src/components/common/VnInputNumber.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
@ -18,15 +23,26 @@ const props = defineProps({
required: true, required: true,
}, },
}); });
const route = useRoute();
const { push, currentRoute } = useRouter(); const { push, currentRoute } = useRouter();
const { dialog, notify } = useQuasar(); const { dialog, notify } = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const { openReport, sendEmail } = usePrintService(); const { openReport, sendEmail } = usePrintService();
const ticketSummary = useArrayData('TicketSummary'); const ticketSummary = useArrayData('TicketSummary');
const ticket = ref(props.ticket); const { ticket } = toRefs(props);
const ticketId = currentRoute.value.params.id; 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 weight = ref();
const hasDocuwareFile = ref();
const quasar = useQuasar();
const actions = { const actions = {
clone: async () => { clone: async () => {
const opts = { message: t('Ticket cloned'), type: 'positive' }; const opts = { message: t('Ticket cloned'), type: 'positive' };
@ -76,6 +92,7 @@ const actions = {
notify({ notify({
message: t('You can undo this action within the first hour'), message: t('You can undo this action within the first hour'),
icon: 'info', icon: 'info',
type: 'warning',
}); });
push({ name: 'TicketList' }); push({ name: 'TicketList' });
@ -87,10 +104,14 @@ const actions = {
function openDeliveryNote(type = 'deliveryNote', documentType = 'pdf') { function openDeliveryNote(type = 'deliveryNote', documentType = 'pdf') {
const path = `Tickets/${ticket.value.id}/delivery-note-${documentType}`; const path = `Tickets/${ticket.value.id}/delivery-note-${documentType}`;
openReport(path, { openReport(
path,
{
recipientId: ticket.value.clientFk, recipientId: ticket.value.clientFk,
type: type, type: type,
}); },
'_blank'
);
} }
function sendDeliveryNoteConfirmation(type = 'deliveryNote', documentType = 'pdf') { function sendDeliveryNoteConfirmation(type = 'deliveryNote', documentType = 'pdf') {
@ -173,34 +194,334 @@ function openConfirmDialog(callback) {
dialog({ dialog({
component: VnConfirm, component: VnConfirm,
componentProps: { 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], 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,
negative: true,
};
const { data } = await axios.post(`Tickets/cloneAll`, params);
if (data) {
const [refundTicket] = data;
notify(t('refundTicketCreated', { ticketId: refundTicket.id }), 'positive');
push({ name: 'TicketSale', params: { id: refundTicket.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;
}
async function docuwareDownload() {
await axios.get(`Tickets/${ticketId}/docuwareDownload`);
}
async function hasDocuware() {
const { data } = await axios.post(`Docuwares/${ticketId}/checkFile`, {
fileCabinet: 'deliveryNote',
signed: true,
});
hasDocuwareFile.value = data;
}
async function uploadDocuware(force) {
console.log('force: ', force);
if (!force)
return quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Send PDF to tablet'),
message: t('Are you sure you want to replace this delivery note?'),
},
})
.onOk(async () => {
uploadDocuware(true);
});
const { data } = await axios.post(`Docuwares/upload`, {
fileCabinet: 'deliveryNote',
ticketIds: [parseInt(ticketId)],
});
if (data) notify({ message: t('PDF sent!'), type: 'positive' });
}
</script> </script>
<template> <template>
<FetchData
:url="
route.path.startsWith('/ticket')
? `Tickets/${ticketId}/isEditable`
: `Tickets/${ticket}/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> <QItem v-ripple clickable>
<QItemSection avatar> <QItemSection avatar>
<QIcon name="picture_as_pdf" /> <QIcon name="picture_as_pdf" />
</QItemSection> </QItemSection>
<QItemSection>{{ t('Open Delivery Note...') }}</QItemSection> <QItemSection>{{ t('Show Delivery Note...') }}</QItemSection>
<QItemSection side> <QItemSection side>
<QIcon name="keyboard_arrow_right" /> <QIcon name="keyboard_arrow_right" />
</QItemSection> </QItemSection>
<QMenu anchor="top end" self="top start" auto-close bordered> <QMenu
anchor="top end"
self="top start"
auto-close
bordered
@click="hasDocuware()"
>
<QList> <QList>
<QItem @click="openDeliveryNote('deliveryNote')" v-ripple clickable> <QItem @click="openDeliveryNote('deliveryNote')" v-ripple clickable>
<QItemSection>{{ t('With prices') }}</QItemSection> <QItemSection>{{ t('as PDF') }}</QItemSection>
</QItem> </QItem>
<QItem @click="openDeliveryNote('withoutPrices')" v-ripple clickable> <QItem @click="openDeliveryNote('withoutPrices')" v-ripple clickable>
<QItemSection>{{ t('Without prices') }}</QItemSection> <QItemSection>{{ t('as PDF without prices') }}</QItemSection>
</QItem>
<QItem
v-if="hasDocuwareFile"
@click="docuwareDownload()"
v-ripple
clickable
>
<QItemSection>{{ t('as PDF signed') }}</QItemSection>
</QItem> </QItem>
<QItem <QItem
@click="openDeliveryNote('deliveryNote', 'csv')" @click="openDeliveryNote('deliveryNote', 'csv')"
v-ripple v-ripple
clickable clickable
> >
<QItemSection>{{ t('As CSV') }}</QItemSection> <QItemSection>{{ t('as CSV') }}</QItemSection>
</QItem> </QItem>
</QList> </QList>
</QMenu> </QMenu>
@ -213,28 +534,24 @@ function openConfirmDialog(callback) {
<QItemSection side> <QItemSection side>
<QIcon name="keyboard_arrow_right" /> <QIcon name="keyboard_arrow_right" />
</QItemSection> </QItemSection>
<QMenu anchor="top end" self="top start" auto-close> <QMenu anchor="top end" self="top start" auto-close @click="hasDocuware()">
<QList> <QList>
<QItem <QItem
@click="sendDeliveryNoteConfirmation('deliveryNote')" @click="sendDeliveryNoteConfirmation('deliveryNote')"
v-ripple v-ripple
clickable clickable
> >
<QItemSection>{{ t('With prices') }}</QItemSection> <QItemSection>{{ t('Send PDF') }}</QItemSection>
</QItem> </QItem>
<QItem <QItem @click="uploadDocuware(!hasDocuwareFile)" v-ripple clickable>
@click="sendDeliveryNoteConfirmation('withoutPrices')" <QItemSection>{{ t('Send PDF to tablet') }}</QItemSection>
v-ripple
clickable
>
<QItemSection>{{ t('Without prices') }}</QItemSection>
</QItem> </QItem>
<QItem <QItem
@click="sendDeliveryNoteConfirmation('deliveryNote', 'csv')" @click="sendDeliveryNoteConfirmation('deliveryNote', 'csv')"
v-ripple v-ripple
clickable clickable
> >
<QItemSection>{{ t('As CSV') }}</QItemSection> <QItemSection>{{ t('Send CSV') }}</QItemSection>
</QItem> </QItem>
</QList> </QList>
</QMenu> </QMenu>
@ -243,8 +560,26 @@ function openConfirmDialog(callback) {
<QItemSection avatar> <QItemSection avatar>
<QIcon name="receipt" /> <QIcon name="receipt" />
</QItemSection> </QItemSection>
<QItemSection>{{ t('Open Proforma Invoice') }}</QItemSection> <QItemSection>{{ t('Show Proforma') }}</QItemSection>
</QItem> </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> <QItem v-ripple clickable>
<QItemSection avatar> <QItemSection avatar>
<QIcon name="sms" /> <QIcon name="sms" />
@ -259,24 +594,72 @@ function openConfirmDialog(callback) {
<QItemSection>{{ t('Pending payment') }}</QItemSection> <QItemSection>{{ t('Pending payment') }}</QItemSection>
</QItem> </QItem>
<QItem @click="showSmsDialog('minAmount')" v-ripple clickable> <QItem @click="showSmsDialog('minAmount')" v-ripple clickable>
<QItemSection>{{ t('Minimum amount') }}</QItemSection> <QItemSection>{{ t('Minimum import') }}</QItemSection>
</QItem> </QItem>
<QItem <QItem
@click="showSmsDialogWithChanges('orderChanges')" @click="showSmsDialogWithChanges('orderChanges')"
v-ripple v-ripple
clickable clickable
> >
<QItemSection>{{ t('Order changes') }}</QItemSection> <QItemSection>{{ t('Notify changes') }}</QItemSection>
</QItem> </QItem>
</QList> </QList>
</QMenu> </QMenu>
</QItem> </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> <QItem @click="openConfirmDialog('clone')" v-ripple clickable>
<QItemSection avatar> <QItemSection avatar>
<QIcon name="content_copy" /> <QIcon name="content_copy" />
</QItemSection> </QItemSection>
<QItemSection>{{ t('To clone ticket') }}</QItemSection> <QItemSection>{{ t('To clone ticket') }}</QItemSection>
</QItem> </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> <QItem @click="$refs.weightDialog.show()" v-ripple clickable>
<QItemSection avatar> <QItemSection avatar>
<QIcon name="weight" /> <QIcon name="weight" />
@ -307,29 +690,63 @@ function openConfirmDialog(callback) {
</template> </template>
</VnConfirm> </VnConfirm>
</template> </template>
<style lang="scss" scoped>
.weekdaysBtn {
margin: 1%;
}
</style>
<i18n> <i18n>
en: en:
addTurn: Add turn
invoiceIds: "Invoices have been generated with the following ids: {invoiceIds}" invoiceIds: "Invoices have been generated with the following ids: {invoiceIds}"
es: es:
Open Delivery Note...: Abrir albarán... Show Delivery Note...: Ver albarán...
Send Delivery Note...: Enviar albarán... Send Delivery Note...: Enviar albarán...
With prices: Con precios as PDF: como PDF
Without prices: Sin precios as PDF without prices: como PDF sin precios
As CSV: Como CSV as CSV: Como CSV
Open Proforma Invoice: Abrir factura proforma Send PDF: Enviar PDF
Send PDF to tablet: Enviar PDF a tablet
Send CSV: Enviar CSV
Show Proforma: Ver proforma
Delete ticket: Eliminar ticket Delete ticket: Eliminar ticket
Send SMS...: Enviar SMS Send SMS...: Enviar SMS...
Pending payment: Pago pendiente Pending payment: Pago pendiente
Minimum amount: Importe mínimo Minimum import: Importe mínimo
Order changes: Cambios del pedido Notify changes: Notificar cambios
Ticket deleted: Ticket eliminado Ticket deleted: Ticket eliminado
You can undo this action within the first hour: Puedes deshacer esta acción dentro de la primera hora You can undo this action within the first hour: Puedes deshacer esta acción dentro de la primera hora
To clone ticket: Clonar ticket To clone ticket: Clonar ticket
Ticket cloned: Ticked clonado Ticket cloned: Ticked clonado
It was not able to clone the ticket: No se pudo clonar el ticket 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 Set weight: Establecer peso
Weight set: Peso establecido Weight set: Peso establecido
This ticket may be invoiced, do you want to continue?: Es posible que se facture este ticket, desea continuar? 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}" 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
as PDF signed: como PDF firmado
Are you sure you want to replace this delivery note?: ¿Seguro que quieres reemplazar este albarán?
</i18n> </i18n>

View File

@ -1,8 +1,8 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import VnUsesMana from 'components/ui/VnUsesMana.vue';
const $props = defineProps({ const $props = defineProps({
mana: { mana: {
@ -13,12 +13,21 @@ const $props = defineProps({
type: Number, type: Number,
default: 0, default: 0,
}, },
usesMana: {
type: Boolean,
default: false,
},
manaCode: {
type: String,
default: 'mana',
},
}); });
const emit = defineEmits(['save', 'cancel']); const emit = defineEmits(['save', 'cancel']);
const { t } = useI18n(); const { t } = useI18n();
const QPopupProxyRef = ref(null); const QPopupProxyRef = ref(null);
const manaCode = ref($props.manaCode);
const save = () => { const save = () => {
emit('save'); emit('save');
@ -47,6 +56,9 @@ const cancel = () => {
</div> </div>
</div> </div>
</div> </div>
<div v-if="usesMana" class="column q-gutter-y-sm q-mt-sm">
<VnUsesMana :mana-code="manaCode" />
</div>
<div class="row"> <div class="row">
<QBtn <QBtn
color="primary" color="primary"

View File

@ -17,6 +17,7 @@ const ticketNotesCrudRef = ref(null);
const observationTypes = ref([]); const observationTypes = ref([]);
const arrayData = useArrayData('TicketNotes'); const arrayData = useArrayData('TicketNotes');
const { store } = arrayData; const { store } = arrayData;
const isSaving = ref(false);
const crudModelFilter = reactive({ const crudModelFilter = reactive({
where: { ticketFk: route.params.id }, where: { ticketFk: route.params.id },
@ -33,11 +34,28 @@ watch(
await ticketNotesCrudRef.value.reload(); await ticketNotesCrudRef.value.reload();
} }
); );
function handleDelete(row) {
ticketNotesCrudRef.value.remove([row]);
}
async function handleSave() {
if (!isSaving.value) {
isSaving.value = true;
await ticketNotesCrudRef.value?.saveChanges();
isSaving.value = false;
}
}
</script> </script>
<template> <template>
<FetchData <FetchData
@on-fetch="(data) => (observationTypes = data)" @on-fetch="
(data) =>
(observationTypes = data.map((type) => {
type.label = t(`ticketNotes.observationTypes.${type.description}`);
return type;
}))
"
auto-load auto-load
url="ObservationTypes" url="ObservationTypes"
/> />
@ -64,7 +82,7 @@ watch(
:label="t('ticketNotes.observationType')" :label="t('ticketNotes.observationType')"
:options="observationTypes" :options="observationTypes"
hide-selected hide-selected
option-label="description" option-label="label"
option-value="id" option-value="id"
v-model="row.observationTypeFk" v-model="row.observationTypeFk"
:disable="!!row.id" :disable="!!row.id"
@ -73,13 +91,14 @@ watch(
:label="t('ticketNotes.description')" :label="t('ticketNotes.description')"
v-model="row.description" v-model="row.description"
class="col" class="col"
@keyup.enter="handleSave"
/> />
<QIcon <QIcon
name="delete" name="delete"
size="sm" size="sm"
class="cursor-pointer" class="cursor-pointer"
color="primary" color="primary"
@click="ticketNotesCrudRef.remove([row])" @click="handleDelete(row)"
> >
<QTooltip> <QTooltip>
{{ t('ticketNotes.removeNote') }} {{ t('ticketNotes.removeNote') }}

View File

@ -10,6 +10,7 @@ import FetchData from 'components/FetchData.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import VnRow from 'src/components/ui/VnRow.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();

View File

@ -1,27 +1,35 @@
<script setup> <script setup>
import { ref, computed, watch, reactive } from 'vue'; import { ref, computed, watch, reactive } from 'vue';
import axios from 'axios';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import VnInput from 'src/components/common/VnInput.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import CrudModel from 'src/components/CrudModel.vue';
import TicketCreateRequest from './TicketCreateRequest.vue';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import { toDateFormat } from 'src/filters/date.js'; import { toDateFormat } from 'src/filters/date.js';
import VnTable from 'src/components/VnTable/VnTable.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'src/components/FetchData.vue';
import { useVnConfirm } from 'composables/useVnConfirm';
import useNotify from 'src/composables/useNotify.js';
import { onMounted } from 'vue';
import { useStateStore } from 'src/stores/useStateStore';
const route = useRoute(); const route = useRoute();
const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const createTicketRequestDialogRef = ref(null); const tableRef = ref();
const crudModelRef = ref(null); const attendersOptions = ref([]);
const { openConfirmationModal } = useVnConfirm();
const { notify } = useNotify();
watch( watch(
() => route.params.id, () => route.params.id,
async (val) => { async (val) => {
crudModelFilter.where.ticketFk = val; crudModelFilter.where.ticketFk = val;
crudModelRef.value.reload(); tableRef.value.reload();
} }
); );
@ -72,64 +80,81 @@ const crudModelFilter = reactive({
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left',
label: t('purchaseRequest.id'), label: t('purchaseRequest.id'),
name: 'id', name: 'id',
field: 'id', chip: {
align: 'left', condition: () => true,
columnFilter: null, },
isId: true,
}, },
{ {
align: 'left',
label: t('purchaseRequest.description'), label: t('purchaseRequest.description'),
name: 'description', name: 'description',
field: 'description', columnClass: 'expand',
align: 'left',
format: (val) => dashIfEmpty(val),
}, },
{ {
align: 'left',
label: t('purchaseRequest.created'), label: t('purchaseRequest.created'),
name: 'created', name: 'created',
field: 'created', format: (row) => toDateFormat(row.created),
align: 'left', cardVisible: true,
format: (val) => toDateFormat(val),
}, },
{ {
align: 'left',
label: t('purchaseRequest.requester'), label: t('purchaseRequest.requester'),
name: 'requester', name: 'requesterFk',
align: 'left', cardVisible: true,
sortable: true, format: (row) => dashIfEmpty(row.requester?.user?.nickname),
}, },
{ {
align: 'left',
label: t('purchaseRequest.atender'), label: t('purchaseRequest.atender'),
name: 'atender', name: 'attenderFk',
align: 'left', cardVisible: true,
format: (row) => dashIfEmpty(row.atender?.user?.nickname),
}, },
{ {
align: 'left',
label: t('purchaseRequest.quantity'), label: t('purchaseRequest.quantity'),
name: 'quantity', name: 'quantity',
align: 'left',
}, },
{ {
align: 'left',
label: t('purchaseRequest.price'), label: t('purchaseRequest.price'),
name: 'price', name: 'price',
align: 'left',
}, },
{ {
align: 'left',
label: t('purchaseRequest.saleFk'), label: t('purchaseRequest.saleFk'),
name: 'saleFk', name: 'saleFk',
align: 'left', cardVisible: true,
}, },
{ {
align: 'left',
label: t('purchaseRequest.state'), label: t('purchaseRequest.state'),
name: 'state', name: 'isOk',
field: 'isOk', cardVisible: true,
align: 'left',
format: (val) => t(getRequestState(val)),
}, },
{ {
label: '', align: 'right',
name: 'actions', name: 'tableActions',
align: 'left', actions: [
columnFilter: null, {
title: t('globals.delete'),
icon: 'delete',
isPrimary: true,
action: (row) =>
openConfirmationModal(
t('You are going to delete this ticket purchase request'),
t(
'This ticket will be removed from ticket purchase requests! Continue anyway?'
),
() => removeLine(row.id)
),
},
],
}, },
]); ]);
@ -140,131 +165,117 @@ const getRequestState = (state) => {
case false: case false:
return 'Denied'; return 'Denied';
case true: case true:
return 'Acepted'; return 'Accepted';
} }
}; };
const isEditable = (isOk) => isOk !== null; const isEditable = (isOk) => isOk !== null;
const removeLine = async (row) => crudModelRef.value.remove([row]); async function removeLine(id) {
try {
await axios.delete(`TicketRequests/${id}`);
notify(t('globals.dataSaved'), 'positive');
location.reload();
} catch (err) {
console.error('Error ', err);
}
}
const openCreateModal = () => createTicketRequestDialogRef.value.show(); onMounted(() => (stateStore.rightDrawer = false));
</script> </script>
<template> <template>
<QPage class="column items-center q-pa-md"> <FetchData
<CrudModel url="TicketRequests/getItemTypeWorker"
:filter="{ fields: ['id', 'nickname'], order: 'nickname ASC' }"
auto-load
@on-fetch="(data) => (attendersOptions = data)"
/>
<VnTable
ref="tableRef"
data-key="PurchaseRequests" data-key="PurchaseRequests"
url="TicketRequests" url="TicketRequests"
ref="crudModelRef" :create="{
urlCreate: 'TicketRequests',
title: t('Create request'),
onDataSaved: ({ id }) => tableRef.reload(id),
formInitialData: {
ticketFk: route.params.id,
},
}"
save-url="TicketRequests/crud"
:filter="crudModelFilter" :filter="crudModelFilter"
:order="['created ASC']" :columns="columns"
:default-remove="false" :is-editable="true"
:default-save="false" :right-search="false"
:default-reset="false" :column-search="false"
:limit="0"
auto-load auto-load
> >
<template #body="{ rows }"> <template #column-description="{ row }">
<QTable <VnInput v-model="row.description" :disable="isEditable(row.isOk)" />
:rows="rows"
:columns="columns"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
@row-click="(_, row) => redirectToTicketSummary(row.ticketFk)"
>
<template #body-cell-description="{ row }">
<QTd @click.stop>
<VnInput
v-model="row.description"
@blur="crudModelRef.saveChanges()"
:disable="isEditable(row.isOk)"
/>
</QTd>
</template> </template>
<template #body-cell-requester="{ row }"> <template #column-requesterFk="{ row }">
<QTd @click.stop> <span class="link" @click.stop>
<QBtn flat color="primary">
{{ row.requester?.user?.nickname }} {{ row.requester?.user?.nickname }}
<WorkerDescriptorProxy :id="row.requesterFk" /> <WorkerDescriptorProxy :id="row.requesterFk" />
</QBtn> </span>
</QTd>
</template> </template>
<template #body-cell-atender="{ row }"> <template #column-attenderFk="{ row }">
<QTd @click.stop> <span class="link" @click.stop>
<QBtn flat color="primary">
{{ row.atender?.user?.nickname }} {{ row.atender?.user?.nickname }}
<WorkerDescriptorProxy :id="row.attenderFk" /> <WorkerDescriptorProxy :id="row.attenderFk" />
</QBtn> </span>
</QTd>
</template> </template>
<template #body-cell-quantity="{ row }"> <template #column-quantity="{ row }">
<QTd @click.stop> <VnInput v-model="row.quantity" :disable="isEditable(row.isOk)" />
</template>
<template #column-price="{ row }">
<span @click.stop>
<VnInput v-model="row.price" :disable="isEditable(row.isOk)">
{{ row.price }}
</VnInput>
</span>
</template>
<template #column-saleFk="{ row }">
<QTd style="width: 3%">
<span class="link" @click.stop>
{{ dashIfEmpty(row.sale?.itemFk) }}
<ItemDescriptorProxy :id="row.sale?.itemFk" /> </span
></QTd>
</template>
<template #column-isOk="{ row }">
{{ t(getRequestState(row.isOk)) }}
</template>
<template #more-create-dialog="{ data }">
<VnInput <VnInput
v-model="row.quantity" v-model="data.description"
@blur="crudModelRef.saveChanges()" :label="t('purchaseRequest.description')"
:disable="isEditable(row.isOk)" />
<VnSelect
:label="t('purchaseRequest.atender')"
v-model="data.attenderFk"
:options="attendersOptions"
hide-selected
option-label="nickname"
option-value="id"
/> />
</QTd>
</template>
<template #body-cell-price="{ row }">
<QTd @click.stop>
<VnInput <VnInput
v-model="row.price" v-model="data.quantity"
@blur="crudModelRef.saveChanges()" :label="t('purchaseRequest.quantity')"
:disable="isEditable(row.isOk)" type="number"
min="1"
/> />
</QTd> <VnInput
</template> v-model="data.price"
<template #body-cell-saleFk="{ row }"> :label="t('purchaseRequest.price')"
<QTd @click.stop> type="number"
<QBtn v-if="row.saleFk" flat color="primary"> min="0"
{{ row.sale.itemFk }}
<ItemDescriptorProxy :id="row.sale.itemFk" />
</QBtn>
</QTd>
</template>
<template #body-cell-actions="{ row }">
<QTd>
<QIcon
@click.stop="removeLine(row)"
class="q-ml-sm cursor-pointer"
color="primary"
name="delete"
size="sm"
>
<QTooltip>
{{ t('globals.delete') }}
</QTooltip>
</QIcon>
</QTd>
</template>
</QTable>
</template>
</CrudModel>
<QDialog
ref="createTicketRequestDialogRef"
transition-show="scale"
transition-hide="scale"
>
<TicketCreateRequest @on-request-created="crudModelRef.reload()" />
</QDialog>
<QPageSticky :offset="[20, 20]">
<QBtn
@click="openCreateModal()"
color="primary"
fab
icon="add"
shortcut="+"
/> />
<QTooltip class="text-no-wrap"> </template>
{{ t('purchaseRequest.newRequest') }} </VnTable>
</QTooltip>
</QPageSticky>
</QPage>
</template> </template>
<i18n> <i18n>
es: es:
New: Nueva New: Nueva

View File

@ -16,11 +16,13 @@ import TicketSaleMoreActions from './TicketSaleMoreActions.vue';
import TicketTransfer from './TicketTransfer.vue'; import TicketTransfer from './TicketTransfer.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { toCurrency, toPercentage, dashIfEmpty } from 'src/filters'; import { toCurrency, toPercentage } from 'src/filters';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import axios from 'axios'; import axios from 'axios';
import VnTable from 'src/components/VnTable/VnTable.vue';
import VnUsesMana from 'src/components/ui/VnUsesMana.vue';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -33,7 +35,8 @@ const stateBtnDropdownRef = ref(null);
const arrayData = useArrayData('ticketData'); const arrayData = useArrayData('ticketData');
const { store } = arrayData; const { store } = arrayData;
const selectedRows = ref([]);
const hasSelectedRows = computed(() => selectedRows.value.length > 0);
const ticketConfig = ref(null); const ticketConfig = ref(null);
const isLocked = ref(false); const isLocked = ref(false);
const isTicketEditable = ref(false); const isTicketEditable = ref(false);
@ -47,94 +50,98 @@ const transfer = ref({
lastActiveTickets: [], lastActiveTickets: [],
sales: [], sales: [],
}); });
const tableRef = ref([]);
watch( watch(
() => route.params.id, () => route.params.id,
async () => await getSales() () => tableRef.value.reload()
); );
const columns = computed(() => [ const columns = computed(() => [
{ {
label: '', align: 'left',
name: 'statusIcons', name: 'statusIcons',
align: 'left',
}, },
{ {
label: '', align: 'center',
name: 'picture', label: t('lines.image'),
align: 'left', name: 'image',
columnField: {
component: VnImg,
attrs: (id) => {
return {
id,
width: '50px',
};
},
},
columnFilter: false,
}, },
{ {
align: 'left',
label: t('ticketSale.visible'), label: t('ticketSale.visible'),
name: 'visible', name: 'visible',
field: 'visible', format: (row, dashIfEmpty) => dashIfEmpty(row.visible),
align: 'left',
sortable: true,
}, },
{ {
align: 'left',
label: t('ticketSale.available'), label: t('ticketSale.available'),
name: 'available', name: 'available',
field: 'available', format: (row, dashIfEmpty) => dashIfEmpty(row.available),
align: 'left',
sortable: true,
}, },
{ {
align: 'left',
label: t('ticketSale.id'), label: t('ticketSale.id'),
name: 'itemFk', name: 'itemFk',
field: 'itemFk',
align: 'left',
sortable: true,
}, },
{ {
align: 'left',
label: t('ticketSale.quantity'), label: t('ticketSale.quantity'),
name: 'quantity', name: 'quantity',
field: 'quantity', format: (row) => toCurrency(row.quantity),
align: 'left',
sortable: true,
}, },
{ {
align: 'left',
label: t('ticketSale.item'), label: t('ticketSale.item'),
name: 'item', name: 'item',
field: 'item', format: (row) => row?.item?.name,
align: 'left', columnClass: 'expand',
sortable: true,
}, },
{ {
align: 'left',
label: t('ticketSale.price'), label: t('ticketSale.price'),
name: 'price', name: 'price',
field: 'price', format: (row) => toCurrency(row.price),
align: 'left',
sortable: true,
format: (val) => toCurrency(val),
}, },
{ {
align: 'left',
label: t('ticketSale.discount'), label: t('ticketSale.discount'),
name: 'discount', name: 'discount',
field: 'discount', format: (row) => toPercentage(row.discount),
align: 'left',
sortable: true,
}, },
{ {
align: 'left',
label: t('ticketSale.amount'), label: t('ticketSale.amount'),
name: 'amount', name: 'amount',
field: 'amount', format: (row) => parseInt(row.amount * row.quantity),
align: 'left',
sortable: true,
format: (val) => toCurrency(val),
}, },
{ {
align: 'left',
label: t('ticketSale.packaging'), label: t('ticketSale.packaging'),
name: 'itemPackingTypeFk', name: 'itemPackingTypeFk',
field: 'item', format: (row, dashIfEmpty) => dashIfEmpty(row?.item?.itemPackingTypeFk),
align: 'left',
sortable: true,
format: (val) => dashIfEmpty(val?.itemPackingTypeFk),
}, },
{ {
label: '', align: 'right',
name: 'history', name: 'tableActions',
align: 'left', actions: [
columnFilter: null, {
title: t('ticketSale.history'),
icon: 'history',
isPrimary: true,
action: (row) => goToLog(row.id),
},
],
}, },
]); ]);
@ -155,15 +162,6 @@ const onSalesFetched = (salesData) => {
for (let sale of salesData) sale.amount = getSaleTotal(sale); for (let sale of salesData) sale.amount = getSaleTotal(sale);
}; };
const getSales = async () => {
try {
const { data } = await axios.get(`Tickets/${route.params.id}/getSales`);
onSalesFetched(data);
} catch (err) {
console.error('Error fetching sales', err);
}
};
const getSaleTotal = (sale) => { const getSaleTotal = (sale) => {
if (sale.quantity == null || sale.price == null) return null; if (sale.quantity == null || sale.price == null) return null;
@ -175,7 +173,7 @@ const getSaleTotal = (sale) => {
const resetChanges = async () => { const resetChanges = async () => {
arrayData.fetch({ append: false }); arrayData.fetch({ append: false });
getSales(); tableRef.value.reload();
}; };
const updateQuantity = async (sale) => { const updateQuantity = async (sale) => {
@ -210,6 +208,7 @@ const addSale = async (sale) => {
sale.item = newSale.item; sale.item = newSale.item;
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
window.location.reload();
} catch (err) { } catch (err) {
console.error('Error adding sale', err); console.error('Error adding sale', err);
} }
@ -259,7 +258,7 @@ const getMana = async () => {
const selectedValidSales = computed(() => { const selectedValidSales = computed(() => {
if (!sales.value) return; if (!sales.value) return;
return selectedSales.value.filter((sale) => sale.id != undefined); return [...selectedRows.value];
}); });
const onOpenEditPricePopover = async (sale) => { const onOpenEditPricePopover = async (sale) => {
@ -374,7 +373,7 @@ const changeTicketState = async (val) => {
}; };
const removeSelectedSales = () => { const removeSelectedSales = () => {
selectedSales.value.forEach((sale) => { selectedRows.value.forEach((sale) => {
const index = sales.value.indexOf(sale); const index = sales.value.indexOf(sale);
sales.value.splice(index, 1); sales.value.splice(index, 1);
}); });
@ -382,19 +381,29 @@ const removeSelectedSales = () => {
const removeSales = async () => { const removeSales = async () => {
try { try {
const params = { sales: selectedValidSales.value, ticketId: store.data.id }; const params = {
sales: selectedRows.value.filter((sale) => sale.id),
ticketId: store.data.id,
};
selectedRows.value
.filter((sale) => !sale.id)
.forEach((sale) =>
tableRef.value.CrudModelRef.formData.splice(sale.$index, 1)
);
if (params.sales.length == 0) return;
await axios.post('Sales/deleteSales', params); await axios.post('Sales/deleteSales', params);
removeSelectedSales(); removeSelectedSales();
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
window.location.reload();
} catch (err) { } catch (err) {
console.error('Error deleting sales', err); console.error('Error deleting sales', err);
} }
}; };
const insertRow = () => sales.value.push({ ...DEFAULT_EDIT });
const setTransferParams = async () => { const setTransferParams = async () => {
try { try {
selectedSales.value = selectedValidSales.value;
const checkedSales = JSON.parse(JSON.stringify(selectedSales.value)); const checkedSales = JSON.parse(JSON.stringify(selectedSales.value));
transfer.value = { transfer.value = {
lastActiveTickets: [], lastActiveTickets: [],
@ -417,10 +426,64 @@ const setTransferParams = async () => {
onMounted(async () => { onMounted(async () => {
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
getConfig(); getConfig();
getSales();
}); });
onUnmounted(() => (stateStore.rightDrawer = false)); onUnmounted(() => (stateStore.rightDrawer = false));
const items = ref([]);
const newRow = ref({});
const updateItem = (row) => {
const selectedItem = items.value.find((item) => item.id === row.itemFk);
if (selectedItem) {
row.item = selectedItem;
row.itemFk = selectedItem.id;
row.price = selectedItem.price;
row.discount = 0;
row.quantity = 0;
row.amount = row.price * row.quantity;
}
endNewRow(selectedItem);
};
function handleOnDataSave({ CrudModelRef }) {
const { copy } = addRow(CrudModelRef.formData);
CrudModelRef.insert(copy);
}
const addRow = (original = null) => {
let copy = null;
if (!original) {
copy = { isNew: true };
} else {
copy = {
itemFk: original.itemFk,
item: original.item,
quantity: original.quantity,
price: original.price,
discount: original.discount,
amount: original.amount,
isNew: true,
};
}
newRow.value = copy;
return { original, copy };
};
const endNewRow = (row) => {
if (row.itemFk && row.quantity) {
row.isNew = false;
}
};
watch(
() => newRow.value.itemFk,
(newItemFk) => {
if (newItemFk) {
updateItem(newRow.value);
}
}
);
</script> </script>
<template> <template>
@ -471,7 +534,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
:ticket="store.data" :ticket="store.data"
:is-ticket-editable="isTicketEditable" :is-ticket-editable="isTicketEditable"
:sales="selectedValidSales" :sales="selectedValidSales"
:disable="!selectedSales.length" :disable="!hasSelectedRows"
:mana="mana" :mana="mana"
:ticket-config="ticketConfig" :ticket-config="ticketConfig"
@get-mana="getMana()" @get-mana="getMana()"
@ -481,7 +544,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
<QBtn <QBtn
color="primary" color="primary"
icon="delete" icon="delete"
:disable="!isTicketEditable || !selectedSales.length" :disable="!isTicketEditable || !hasSelectedRows"
@click=" @click="
openConfirmationModal( openConfirmationModal(
t('Continue anyway?'), t('Continue anyway?'),
@ -495,7 +558,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
<QBtn <QBtn
color="primary" color="primary"
icon="vn:splitline" icon="vn:splitline"
:disable="!isTicketEditable || !selectedSales.length" :disable="!isTicketEditable || !hasSelectedRows"
@click="setTransferParams()" @click="setTransferParams()"
> >
<QTooltip>{{ t('Transfer lines') }}</QTooltip> <QTooltip>{{ t('Transfer lines') }}</QTooltip>
@ -508,44 +571,58 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</QBtnGroup> </QBtnGroup>
</template> </template>
</VnSubToolbar> </VnSubToolbar>
<QDrawer side="right" :width="270" v-model="stateStore.rightDrawer"> <QDrawer side="right" :width="265" v-model="stateStore.rightDrawer">
<div <div
class="q-pa-md q-mb-md q-ma-md color-vn-text" class="q-pa-md q-mb-md q-ma-md color-vn-text"
style="border: 2px solid black" style="border: 2px solid black"
> >
<QCardSection class="justify-center text-subtitle1" horizontal> <QCardSection class="justify-end text-subtitle1" horizontal>
<span class="q-mr-xs color-vn-label" <span class="q-mr-xs color-vn-label"
>{{ t('ticketSale.subtotal') }}: >{{ t('ticketSale.subtotal') }}:
</span> </span>
<span>{{ toCurrency(store.data?.totalWithoutVat) }}</span> <span>{{ toCurrency(store.data?.totalWithoutVat) }}</span>
</QCardSection> </QCardSection>
<QCardSection class="justify-center text-subtitle1" horizontal> <QCardSection class="justify-end text-subtitle1" horizontal>
<span class="q-mr-xs color-vn-label"> {{ t('ticketSale.tax') }}: </span> <span class="q-mr-xs color-vn-label"> {{ t('ticketSale.tax') }}: </span>
<span>{{ <span>{{
toCurrency(store.data?.totalWithVat - store.data?.totalWithoutVat) toCurrency(store.data?.totalWithVat - store.data?.totalWithoutVat)
}}</span> }}</span>
</QCardSection> </QCardSection>
<QCardSection <QCardSection class="justify-end text-weight-bold text-subtitle1" horizontal>
class="justify-center text-weight-bold text-subtitle1"
horizontal
>
<span class="q-mr-xs color-vn-label"> {{ t('ticketSale.total') }}: </span> <span class="q-mr-xs color-vn-label"> {{ t('ticketSale.total') }}: </span>
<span>{{ toCurrency(store.data?.totalWithVat) }}</span> <span>{{ toCurrency(store.data?.totalWithVat) }}</span>
</QCardSection> </QCardSection>
</div></QDrawer </div></QDrawer
> >
<QTable <VnTable
:rows="sales" ref="tableRef"
data-key="TicketSales"
:url="`Tickets/${route.params.id}/getSales`"
:columns="columns" :columns="columns"
row-key="id" v-model:selected="selectedRows"
:pagination="{ rowsPerPage: 0 }" :bottom="true"
class="full-width q-mt-md" :table="{
selection="multiple" 'row-key': 'id',
v-model:selected="selectedSales" selection: 'multiple',
:no-data-label="t('globals.noResults')" }"
:right-search="false"
:column-search="false"
:disable-option="{ card: true }"
auto-load
@on-fetch="onSalesFetched"
:create="{
onDataSaved: handleOnDataSave,
}"
:create-as-dialog="false"
:crud-model="{
paginate: false,
}"
:default-remove="false"
:default-reset="false"
:default-save="false"
:disabled-attr="isTicketEditable"
> >
<template #body-cell-statusIcons="{ row }"> <template #column-statusIcons="{ row }">
<QTd class="q-gutter-x-xs">
<router-link <router-link
v-if="row.claim?.claimFk" v-if="row.claim?.claimFk"
:to="{ name: 'ClaimBasicData', params: { id: row.claim?.claimFk } }" :to="{ name: 'ClaimBasicData', params: { id: row.claim?.claimFk } }"
@ -587,7 +664,6 @@ onUnmounted(() => (stateStore.rightDrawer = false));
{{ t('ticketSale.hasComponentLack') }} {{ t('ticketSale.hasComponentLack') }}
</QTooltip> </QTooltip>
</QIcon> </QIcon>
</QTd>
</template> </template>
<template #body-cell-picture="{ row }"> <template #body-cell-picture="{ row }">
<QTd> <QTd>
@ -596,78 +672,79 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</div> </div>
</QTd> </QTd>
</template> </template>
<template #body-cell-visible="{ row }"> <template #column-image="{ row }">
<QTd @click.stop> <div class="image-wrapper">
<VnImg :id="parseInt(row?.item?.id)" class="rounded" />
</div>
</template>
<template #column-visible="{ row }">
<QBadge :color="row.visible < 0 ? 'alert' : 'transparent'" dense> <QBadge :color="row.visible < 0 ? 'alert' : 'transparent'" dense>
{{ row.visible }} {{ row.visible }}
</QBadge> </QBadge>
</QTd>
</template> </template>
<template #body-cell-available="{ row }"> <template #column-available="{ row }">
<QTd @click.stop>
<QBadge :color="row.available < 0 ? 'alert' : 'transparent'" dense> <QBadge :color="row.available < 0 ? 'alert' : 'transparent'" dense>
{{ row.available }} {{ row.available }}
</QBadge> </QBadge>
</QTd>
</template> </template>
<template #body-cell-itemFk="{ row }"> <template #column-itemFk="{ row }">
<QTd @click.stop>
<div v-if="row.id">
<QBtn flat color="primary" dense>
{{ row.itemFk }}
</QBtn>
<ItemDescriptorProxy :id="row.itemFk" />
</div>
<VnSelect <VnSelect
v-else v-if="row.isNew"
hide-selected url="Items/WithName"
:fields="['id', 'name']"
:sort-by="['id DESC']"
:options="items"
option-label="name" option-label="name"
option-value="id" option-value="id"
url="Items/withName"
:fields="['id', 'name']"
sort-by="id DESC"
@update:model-value="changeQuantity(row)"
v-model="row.itemFk" v-model="row.itemFk"
@update:model-value="updateItem(row)"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
<QItemSection> <QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel> <QItemLabel>
<QItemLabel caption>{{ scope.opt?.name }}</QItemLabel> {{ scope.opt?.id }} - {{ scope.opt?.name }}</QItemLabel
>
</QItemSection> </QItemSection>
</QItem> </QItem>
</template> </template>
</VnSelect> </VnSelect>
</QTd> <span v-else class="link" @click.stop>
{{ row?.itemFk }}
<ItemDescriptorProxy :id="row?.itemFk" />
</span>
</template> </template>
<template #body-cell-quantity="{ row }"> <template #column-item="{ row }">
<QTd @click.stop> <div class="row column full-width justify-between items-start">
{{ row?.item?.name }}
<div v-if="row?.item?.subName" class="subName">
{{ row?.item?.subName.toUpperCase() }}
</div>
</div>
<FetchedTags :item="row" :max-length="6" />
<QPopupProxy v-if="row.id && isTicketEditable">
<VnInput v-model="row.concept" @change="updateConcept(row)" />
</QPopupProxy>
</template>
<template #column-quantity="{ row }">
<VnInput <VnInput
v-if="isTicketEditable" v-if="row.isNew"
v-model.number="row.quantity"
type="number"
@blur="changeQuantity(row)"
@focus="edit.oldQuantity = row.quantity"
/>
<VnInput
v-else-if="isTicketEditable"
v-model.number="row.quantity" v-model.number="row.quantity"
@keyup.enter="changeQuantity(row)"
@blur="changeQuantity(row)" @blur="changeQuantity(row)"
@focus="edit.oldQuantity = row.quantity" @focus="edit.oldQuantity = row.quantity"
/> />
<span v-else>{{ row.quantity }}</span> <span v-else>{{ row.quantity }}</span>
</QTd>
</template> </template>
<template #body-cell-item="{ row }"> <template #column-price="{ row }">
<QTd class="col">
<div class="column">
<span>{{ row.concept }}</span>
<span class="color-vn-label">{{ row.item?.subName }}</span>
<FetchedTags v-if="row.item" :item="row.item" />
<QPopupProxy v-if="row.id && isTicketEditable">
<VnInput v-model="row.concept" @change="updateConcept(row)" />
</QPopupProxy>
</div>
</QTd>
</template>
<template #body-cell-price="{ row }">
<QTd>
<template v-if="isTicketEditable && row.id"> <template v-if="isTicketEditable && row.id">
<QBtn flat color="primary" dense @click="onOpenEditPricePopover(row)"> <QBtn flat class="link" dense @click="onOpenEditPricePopover(row)">
{{ toCurrency(row.price) }} {{ toCurrency(row.price) }}
</QBtn> </QBtn>
<TicketEditManaProxy <TicketEditManaProxy
@ -684,22 +761,17 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</TicketEditManaProxy> </TicketEditManaProxy>
</template> </template>
<span v-else>{{ toCurrency(row.price) }}</span> <span v-else>{{ toCurrency(row.price) }}</span>
</QTd>
</template> </template>
<template #body-cell-discount="{ row }"> <template #column-discount="{ row }">
<QTd>
<template v-if="!isLocked && row.id"> <template v-if="!isLocked && row.id">
<QBtn <QBtn flat class="link" dense @click="onOpenEditDiscountPopover(row)">
flat
color="primary"
dense
@click="onOpenEditDiscountPopover(row)"
>
{{ toPercentage(row.discount / 100) }} {{ toPercentage(row.discount / 100) }}
</QBtn> </QBtn>
<TicketEditManaProxy <TicketEditManaProxy
:mana="mana" :mana="mana"
:new-price="getNewPrice" :new-price="getNewPrice"
:uses-mana="usesMana"
:mana-code="manaCode"
@save="changeDiscount(row)" @save="changeDiscount(row)"
> >
<VnInput <VnInput
@ -707,47 +779,19 @@ onUnmounted(() => (stateStore.rightDrawer = false));
:label="t('ticketSale.discount')" :label="t('ticketSale.discount')"
type="number" type="number"
/> />
<div v-if="usesMana" class="column q-gutter-y-sm q-mt-sm">
<VnUsesMana :mana-code="manaCode" />
</div>
</TicketEditManaProxy> </TicketEditManaProxy>
</template> </template>
<span v-else>{{ toPercentage(row.discount / 100) }}</span> <span v-else>{{ toPercentage(row.discount / 100) }}</span>
</QTd>
</template> </template>
<template #body-cell-history="{ row }"> <template #column-amount="{ row }">
<QTd> {{ toCurrency(row.quantity * row.price) }}
<QBtn
v-if="row.$hasLogs"
@click.stop="goToLog(row.id)"
color="primary"
icon="history"
size="md"
flat
>
<QTooltip class="text-no-wrap">
{{ t('ticketSale.history') }}
</QTooltip>
</QBtn>
</QTd>
</template> </template>
<template #bottom-row> </VnTable>
<QBtn
class="cursor-pointer fill-icon q-ml-md q-my-lg"
color="primary"
icon="add_circle"
size="md"
round
flat
shortcut="+"
:disable="!isTicketEditable"
@click="insertRow()"
>
<QTooltip>
{{ t('Add item') }}
</QTooltip>
</QBtn>
</template>
</QTable>
<QPageSticky :offset="[20, 20]"> <QPageSticky :offset="[20, 20]" style="z-index: 2">
<QBtn @click="newOrderFromTicket()" color="primary" fab icon="add" shortcut="+" /> <QBtn @click="newOrderFromTicket()" color="primary" fab icon="add" shortcut="+" />
<QTooltip class="text-no-wrap"> <QTooltip class="text-no-wrap">
{{ t('Add item to basket') }} {{ t('Add item to basket') }}
@ -755,6 +799,18 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</QPageSticky> </QPageSticky>
</template> </template>
<style lang="scss" scoped>
.image-wrapper {
height: 50px;
width: 50px;
}
.subName {
text-transform: uppercase;
color: var(--vn-label-color);
}
</style>
<i18n> <i18n>
es: es:
New item: Nuevo artículo New item: Nuevo artículo

View File

@ -44,7 +44,7 @@ const props = defineProps({
}, },
}); });
const router = useRouter(); const { push } = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const { dialog } = useQuasar(); const { dialog } = useQuasar();
const { notify } = useNotify(); const { notify } = useNotify();
@ -143,7 +143,7 @@ const onCreateClaimAccepted = async () => {
try { try {
const params = { ticketId: ticket.value.id, sales: props.sales }; const params = { ticketId: ticket.value.id, sales: props.sales };
const { data } = await axios.post(`Claims/createFromSales`, params); const { data } = await axios.post(`Claims/createFromSales`, params);
router.push({ name: 'ClaimBasicData', params: { id: data.id } }); push({ name: 'ClaimBasicData', params: { id: data.id } });
} catch (error) { } catch (error) {
console.error('Error creating claim: ', error); console.error('Error creating claim: ', error);
} }
@ -170,7 +170,7 @@ const createRefund = async (withWarehouse) => {
const { data } = await axios.post('Tickets/cloneAll', params); const { data } = await axios.post('Tickets/cloneAll', params);
const [refundTicket] = data; const [refundTicket] = data;
notify(t('refundTicketCreated', { ticketId: refundTicket.id }), 'positive'); notify(t('refundTicketCreated', { ticketId: refundTicket.id }), 'positive');
router.push({ name: 'TicketSale', params: { id: refundTicket.id } }); push({ name: 'TicketSale', params: { id: refundTicket.id } });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

View File

@ -25,7 +25,7 @@ const saleTrackingFetchDataRef = ref(null);
const sales = ref([]); const sales = ref([]);
const saleTrackings = ref([]); const saleTrackings = ref([]);
const itemShelvignsSales = ref([]); const itemShelvingsSales = ref([]);
const saleTrackingUrl = computed(() => `SaleTrackings/${route.params.id}/filter`); const saleTrackingUrl = computed(() => `SaleTrackings/${route.params.id}/filter`);
const oldQuantity = ref(null); const oldQuantity = ref(null);
@ -88,7 +88,7 @@ const logTableColumns = computed(() => [
label: t('ticketSaleTracking.original'), label: t('ticketSaleTracking.original'),
name: 'original', name: 'original',
field: 'originalQuantity', field: 'originalQuantity',
align: 'original', align: 'left',
sortable: true, sortable: true,
}, },
{ {
@ -177,7 +177,7 @@ const getItemShelvingSales = async (sale) => {
const { data } = await axios.get(`ItemShelvingSales/filter`, { const { data } = await axios.get(`ItemShelvingSales/filter`, {
params: { filter: JSON.stringify(filter) }, params: { filter: JSON.stringify(filter) },
}); });
itemShelvignsSales.value = data; itemShelvingsSales.value = data;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@ -337,7 +337,7 @@ const qCheckBoxController = (sale, action) => {
:no-data-label="t('globals.noResults')" :no-data-label="t('globals.noResults')"
> >
<template #body-cell-isChecked="{ row }"> <template #body-cell-isChecked="{ row }">
<QTd @click.stop> <QTd @click.stop style="width: 20%">
<QCheckbox <QCheckbox
:model-value="!!row.hasSaleGroupDetail" :model-value="!!row.hasSaleGroupDetail"
color="pink" color="pink"
@ -396,12 +396,14 @@ const qCheckBoxController = (sale, action) => {
</QTd> </QTd>
</template> </template>
<template #body-cell-item="{ row }"> <template #body-cell-item="{ row }">
<QTd @click.stop> <QTd @click.stop style="width: 20%">
<div> <div>
<QBtn flat color="primary"> <span class="link">
<QBtn flat>
{{ row.itemFk }} {{ row.itemFk }}
</QBtn> </QBtn>
<ItemDescriptorProxy :id="row.itemFk" /> <ItemDescriptorProxy :id="row.itemFk" />
</span>
</div> </div>
</QTd> </QTd>
</template> </template>
@ -416,8 +418,18 @@ const qCheckBoxController = (sale, action) => {
</div> </div>
</QTd> </QTd>
</template> </template>
<template #body-cell-quantity="{ row }">
<QTd style="width: 10%">
{{ row.quantity }}
</QTd>
</template>
<template #body-cell-parking="{ row }">
<QTd style="width: 10%">
{{ dashIfEmpty(row.parkingFk) }}
</QTd>
</template>
<template #body-cell-actions="{ row }"> <template #body-cell-actions="{ row }">
<QTd> <QTd style="width: 20%">
<QBtn <QBtn
@click.stop="showLog(row)" @click.stop="showLog(row)"
color="primary" color="primary"
@ -452,12 +464,14 @@ const qCheckBoxController = (sale, action) => {
data-key="saleTrackingLog" data-key="saleTrackingLog"
:rows="saleTrackings" :rows="saleTrackings"
:columns="logTableColumns" :columns="logTableColumns"
class="q-pa-sm" class="q-pa-sm full-width"
> >
<template #body-cell-worker="{ row }"> <template #body-cell-worker="{ row }">
<QTd auto-width> <QTd>
<QBtn flat dense color="primary">{{ row.name }}</QBtn> <QBtn flat class="link">
{{ row.name }}
<WorkerDescriptorProxy :id="row.workerFk" /> <WorkerDescriptorProxy :id="row.workerFk" />
</QBtn>
</QTd> </QTd>
</template> </template>
</QTable> </QTable>
@ -469,12 +483,12 @@ const qCheckBoxController = (sale, action) => {
> >
<QTable <QTable
data-key="itemShelvingsSales" data-key="itemShelvingsSales"
:rows="itemShelvignsSales" :rows="itemShelvingsSales"
:columns="shelvingsTableColumns" :columns="shelvingsTableColumns"
class="q-pa-sm" class="q-pa-sm full-width"
> >
<template #body-cell-quantity="{ row }"> <template #body-cell-quantity="{ row }">
<QTd auto-width> <QTd>
<VnInput <VnInput
v-model.number="row.quantity" v-model.number="row.quantity"
@keyup.enter="updateQuantity(row)" @keyup.enter="updateQuantity(row)"
@ -484,13 +498,15 @@ const qCheckBoxController = (sale, action) => {
</QTd> </QTd>
</template> </template>
<template #body-cell-worker="{ row }"> <template #body-cell-worker="{ row }">
<QTd auto-width> <QTd>
<QBtn flat dense color="primary">{{ row.name }}</QBtn> <QBtn flat class="link">
{{ row.name }}
<WorkerDescriptorProxy :id="row.userFk" /> <WorkerDescriptorProxy :id="row.userFk" />
</QBtn>
</QTd> </QTd>
</template> </template>
<template #body-cell-shelving="{ row }"> <template #body-cell-shelving="{ row }">
<QTd auto-width> <QTd>
<VnSelect <VnSelect
url="Shelvings" url="Shelvings"
hide-selected hide-selected
@ -503,7 +519,7 @@ const qCheckBoxController = (sale, action) => {
</QTd> </QTd>
</template> </template>
<template #body-cell-parking="{ row }"> <template #body-cell-parking="{ row }">
<QTd auto-width> <QTd>
<VnSelect <VnSelect
url="Parkings" url="Parkings"
hide-selected hide-selected
@ -538,4 +554,10 @@ $estados: (
} }
} }
} }
@media (min-width: 560px) {
.q-dialog__inner--minimized > div {
max-width: 900px;
}
}
</style> </style>

View File

@ -25,7 +25,7 @@ const { notify } = useNotify();
const selected = ref([]); const selected = ref([]);
const defaultTaxClass = ref(null); const defaultTaxClass = ref(null);
const isSaving = ref(false);
const crudModelFilter = computed(() => ({ const crudModelFilter = computed(() => ({
where: { ticketFk: route.params.id }, where: { ticketFk: route.params.id },
})); }));
@ -50,7 +50,7 @@ const createRefund = async () => {
if (!selected.value.length) return; if (!selected.value.length) return;
const params = { const params = {
servicesIds: selected.value.map((s) => +s.ticketFk), servicesIds: selected.value.map((s) => +s.id),
withWarehouse: false, withWarehouse: false,
negative: true, negative: true,
}; };
@ -104,7 +104,31 @@ const columns = computed(() => [
sortable: true, sortable: true,
align: 'left', align: 'left',
}, },
{
label: '',
name: 'actions',
align: 'left',
columnFilter: null,
},
]); ]);
async function deleteService(row) {
const serviceId = row.id;
if (!row.id) ticketServiceCrudRef.value.reset();
else {
const { data } = await axios.delete(`TicketServices/${serviceId}`);
if (data) notify('Service deleted successfully', 'positive');
ticketServiceCrudRef.value.reload();
}
}
async function handleSave() {
if (!isSaving.value) {
isSaving.value = true;
await ticketServiceCrudRef.value?.saveChanges();
isSaving.value = false;
}
}
</script> </script>
<template> <template>
@ -123,6 +147,8 @@ const columns = computed(() => [
:data-required="crudModelRequiredData" :data-required="crudModelRequiredData"
auto-load auto-load
v-model:selected="selected" v-model:selected="selected"
:order="['description ASC']"
:default-remove="false"
> >
<template #moreBeforeActions> <template #moreBeforeActions>
<QBtn <QBtn
@ -177,9 +203,25 @@ const columns = computed(() => [
v-model.number="row.price" v-model.number="row.price"
type="number" type="number"
min="0" min="0"
@keyup.enter="handleSave"
/> />
</QTd> </QTd>
</template> </template>
<template #body-cell-actions="{ row }">
<QTd auto-width>
<QIcon
color="primary"
name="delete"
class="cursor-pointer"
size="sm"
@click.stop="deleteService(row)"
>
<QTooltip class="text-no-wrap">
{{ t('globals.delete') }}
</QTooltip>
</QIcon>
</QTd>
</template>
</QTable> </QTable>
</template> </template>
</CrudModel> </CrudModel>
@ -193,3 +235,4 @@ const columns = computed(() => [
/> />
</QPageSticky> </QPageSticky>
</template> </template>
ñ

View File

@ -1,4 +1,5 @@
<script setup> <script setup>
import RouteDescriptorProxy from 'pages/Route/Card/RouteDescriptorProxy.vue';
import { onMounted, ref, computed } from 'vue'; import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -16,7 +17,10 @@ import { useArrayData } from 'composables/useArrayData';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import VnTitle from 'src/components/common/VnTitle.vue'; import VnTitle from 'src/components/common/VnTitle.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import TicketDescriptorMenu from './TicketDescriptorMenu.vue';
import VnToSummary from 'src/components/ui/VnToSummary.vue';
const route = useRoute(); const route = useRoute();
const { notify } = useNotify(); const { notify } = useNotify();
@ -66,7 +70,7 @@ function isEditable() {
async function changeState(value) { async function changeState(value) {
try { try {
stateBtnDropdownRef.value.hide(); stateBtnDropdownRef.value?.hide();
const formData = { const formData = {
ticketFk: entityId.value, ticketFk: entityId.value,
code: value, code: value,
@ -79,19 +83,36 @@ async function changeState(value) {
console.error('Error changing ticket state', err); console.error('Error changing ticket state', err);
} }
} }
function toTicketUrl(section) {
return '#/ticket/' + entityId.value + '/' + section;
}
function isOnTicketCard() {
const currentPath = route.path;
return currentPath.startsWith('/ticket');
}
</script> </script>
<template> <template>
<FetchData <FetchData
url="States/editableStates" url="States/editableStates"
@on-fetch="(data) => (editableStates = data)" :filter="{ fields: ['code', 'name', 'id', 'alertLevel'], order: 'name ASC' }"
auto-load auto-load
@on-fetch="(data) => (editableStates = data)"
/> />
<CardSummary <CardSummary
ref="summaryRef" ref="summaryRef"
:url="`Tickets/${entityId}/summary`" :url="`Tickets/${entityId}/summary`"
data-key="TicketSummary" data-key="TicketSummary"
> >
<template #header-left>
<VnToSummary
v-if="route?.name !== 'TicketSummary'"
:route-name="'TicketSummary'"
:entity-id="entityId"
:url="ticketUrl"
/>
</template>
<template #header="{ entity }"> <template #header="{ entity }">
<div> <div>
Ticket #{{ entity.id }} - {{ entity.client?.name }} ({{ Ticket #{{ entity.id }} - {{ entity.client?.name }} ({{
@ -122,14 +143,17 @@ async function changeState(value) {
<template #body="{ entity }"> <template #body="{ entity }">
<QCard class="vn-one"> <QCard class="vn-one">
<VnTitle <VnTitle
:url="ticketUrl + 'basic-data/step-one'" :url="toTicketUrl('basic-data')"
:text="t('globals.summary.basicData')" :text="t('globals.summary.basicData')"
/> />
<VnLv :label="t('ticket.summary.state')"> <VnLv v-if="entity.ticketState" :label="t('ticket.summary.state')">
<template #value> <template #value>
<QChip :color="entity.ticketState?.state?.classColor ?? 'dark'"> <QBadge
{{ entity.ticketState?.state?.name }} text-color="black"
</QChip> :color="entity.ticketState.state.classColor"
>
{{ entity.ticketState.state.name }}
</QBadge>
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('ticket.summary.salesPerson')"> <VnLv :label="t('ticket.summary.salesPerson')">
@ -144,7 +168,14 @@ async function changeState(value) {
:label="t('ticket.summary.agency')" :label="t('ticket.summary.agency')"
:value="entity.agencyMode?.name" :value="entity.agencyMode?.name"
/> />
<VnLv :label="t('ticket.summary.zone')" :value="entity?.zone?.name" /> <VnLv :label="t('ticket.summary.zone')">
<template #value>
<span class="link" @click.stop>
{{ entity?.zone?.name }}
<ZoneDescriptorProxy :id="entity.zoneFk" />
</span>
</template>
</VnLv>
<VnLv <VnLv
:label="t('ticket.summary.warehouse')" :label="t('ticket.summary.warehouse')"
:value="entity.warehouse?.name" :value="entity.warehouse?.name"
@ -164,7 +195,14 @@ async function changeState(value) {
</a> </a>
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('ticket.summary.route')" :value="entity.routeFk" /> <VnLv :label="t('ticket.summary.route')">
<template #value>
<span class="link">
{{ entity.routeFk }}
<RouteDescriptorProxy :id="entity.routeFk" />
</span>
</template>
</VnLv>
<VnLv :label="t('ticket.summary.invoice')"> <VnLv :label="t('ticket.summary.invoice')">
<template #value> <template #value>
<span :class="{ link: entity.refFk }"> <span :class="{ link: entity.refFk }">
@ -181,9 +219,9 @@ async function changeState(value) {
:value="dashIfEmpty(entity.weight)" :value="dashIfEmpty(entity.weight)"
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one" style="flex: 2 1">
<VnTitle <VnTitle
:url="ticketUrl + 'basic-data/step-one'" :url="toTicketUrl('basic-data')"
:text="t('globals.summary.basicData')" :text="t('globals.summary.basicData')"
/> />
<VnLv <VnLv
@ -226,7 +264,7 @@ async function changeState(value) {
</QCard> </QCard>
<QCard class="vn-one" v-if="entity.notes.length"> <QCard class="vn-one" v-if="entity.notes.length">
<VnTitle <VnTitle
:url="ticketUrl + 'observation'" :url="toTicketUrl('observation')"
:text="t('ticket.pageTitles.notes')" :text="t('ticket.pageTitles.notes')"
/> />
<QVirtualScroll <QVirtualScroll
@ -245,12 +283,16 @@ async function changeState(value) {
" "
> >
<span class="label" style="margin-right: 4px"> <span class="label" style="margin-right: 4px">
{{ item.observationType.description }}:</span ({{
> t(
`ticketNotes.observationTypes.${item.observationType.description}`
)
}}):
</span>
<span>{{ item.description }}</span> <span>{{ item.description }}</span>
</QItem></QVirtualScroll </QItem>
> </QVirtualScroll>
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<VnTitle :text="t('ticket.summary.summaryAmount')" /> <VnTitle :text="t('ticket.summary.summaryAmount')" />
@ -265,13 +307,14 @@ async function changeState(value) {
/> />
<VnLv <VnLv
:label="t('ticket.summary.total')" :label="t('ticket.summary.total')"
:value="toCurrency(entity.totalWithVat)" :value="toCurrency(ticket.totalWithVat)"
style="font-weight: bold"
/> />
</div> </div>
</QCard> </QCard>
<QCard class="vn-max"> <QCard class="vn-max">
<VnTitle <VnTitle
:url="ticketUrl + 'sale'" :url="toTicketUrl('sale')"
:text="t('ticket.summary.saleLines')" :text="t('ticket.summary.saleLines')"
/> />
<QTable :rows="entity.sales" style="text-align: center"> <QTable :rows="entity.sales" style="text-align: center">
@ -294,11 +337,10 @@ async function changeState(value) {
</template> </template>
<template #body="props"> <template #body="props">
<QTr :props="props"> <QTr :props="props">
<QTd> <QTd class="q-gutter-x-xs">
<QBtn <QBtn
flat flat
round round
size="xs"
icon="vn:claims" icon="vn:claims"
v-if="props.row.claim" v-if="props.row.claim"
color="primary" color="primary"
@ -309,15 +351,14 @@ async function changeState(value) {
}, },
}" }"
> >
<QTooltip <QTooltip>
>{{ t('ticket.summary.claim') }}: {{ t('ticket.summary.claim') }}:
{{ props.row.claim.claimFk }}</QTooltip {{ props.row.claim.claimFk }}
> </QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
flat flat
round round
size="xs"
icon="vn:claims" icon="vn:claims"
v-if="props.row.claimBeginning" v-if="props.row.claimBeginning"
color="primary" color="primary"
@ -328,27 +369,27 @@ async function changeState(value) {
}, },
}" }"
> >
<QTooltip <QTooltip>
>{{ t('ticket.summary.claim') }}: {{ t('ticket.summary.claim') }}:
{{ props.row.claimBeginning.claimFk }}</QTooltip {{ props.row.claimBeginning.claimFk }}
> </QTooltip>
</QBtn> </QBtn>
<QIcon <QIcon
name="warning" name="warning"
v-show="props.row.visible < 0" v-show="props.row.visible < 0"
size="xs"
color="primary" color="primary"
size="xs"
> >
<QTooltip <QTooltip>
>{{ t('ticket.summary.visible') }}: {{ t('ticket.summary.visible') }}:
{{ props.row.visible }}</QTooltip {{ props.row.visible }}
> </QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon
name="vn:reserva" name="vn:reserved"
v-show="props.row.reserved" v-show="props.row.reserved"
size="xs"
color="primary" color="primary"
size="xs"
> >
<QTooltip> <QTooltip>
{{ t('ticket.summary.reserved') }} {{ t('ticket.summary.reserved') }}
@ -357,8 +398,8 @@ async function changeState(value) {
<QIcon <QIcon
name="vn:unavailable" name="vn:unavailable"
v-show="props.row.itemShortage" v-show="props.row.itemShortage"
size="xs"
color="primary" color="primary"
size="xs"
> >
<QTooltip> <QTooltip>
{{ t('ticket.summary.itemShortage') }} {{ t('ticket.summary.itemShortage') }}
@ -367,8 +408,8 @@ async function changeState(value) {
<QIcon <QIcon
name="vn:components" name="vn:components"
v-show="props.row.hasComponentLack" v-show="props.row.hasComponentLack"
size="xs"
color="primary" color="primary"
size="xs"
> >
<QTooltip> <QTooltip>
{{ t('ticket.summary.hasComponentLack') }} {{ t('ticket.summary.hasComponentLack') }}
@ -385,8 +426,34 @@ async function changeState(value) {
/> />
</QBtn> </QBtn>
</QTd> </QTd>
<QTd>{{ props.row.visible }}</QTd> <QTd>
<QTd>{{ props.row.available }}</QTd> <QChip
v-if="props.row.visible < 0"
dense
rounded
:color="'negative'"
text-color="white"
>
{{ props.row.visible }}
</QChip>
<span v-else>
{{ props.row.visible }}
</span>
</QTd>
<QTd>
<QChip
v-if="props.row.available < 0"
dense
rounded
:color="'negative'"
text-color="white"
>
{{ props.row.available }}
</QChip>
<span v-else>
{{ props.row.available }}
</span>
</QTd>
<QTd>{{ props.row.quantity }}</QTd> <QTd>{{ props.row.quantity }}</QTd>
<QTd class="description-cell"> <QTd class="description-cell">
<div class="row full-width justify-between"> <div class="row full-width justify-between">
@ -416,14 +483,11 @@ async function changeState(value) {
</template> </template>
</QTable> </QTable>
</QCard> </QCard>
<QCard <QCard class="vn-max" v-if="ticket.packagings.length != 0">
class="vn-max" <VnTitle :url="toTicketUrl('package')" :text="t('globals.packages')" />
v-if="entity.packagings.length || entity.services.length" <QTable :rows="ticket.packagings" flat style="text-align: center">
>
<VnTitle :url="ticketUrl + 'package'" :text="t('globals.packages')" />
<QTable :rows="entity.packagings" flat>
<template #header="props"> <template #header="props">
<QTr :props="props"> <QTr class="tr-header" :props="props">
<QTh auto-width>{{ t('ticket.summary.created') }}</QTh> <QTh auto-width>{{ t('ticket.summary.created') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.package') }}</QTh> <QTh auto-width>{{ t('ticket.summary.package') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.quantity') }}</QTh> <QTh auto-width>{{ t('ticket.summary.quantity') }}</QTh>
@ -437,13 +501,15 @@ async function changeState(value) {
</QTr> </QTr>
</template> </template>
</QTable> </QTable>
</QCard>
<QCard class="vn-max" v-if="ticket.services.length != 0">
<VnTitle <VnTitle
:url="ticketUrl + 'service'" :url="toTicketUrl('service')"
:text="t('ticket.summary.service')" :text="t('ticket.summary.service')"
/> />
<QTable :rows="entity.services" flat> <QTable :rows="ticket.services" flat style="text-align: center">
<template #header="props"> <template #header="props">
<QTr :props="props"> <QTr class="tr-header" :props="props">
<QTh auto-width>{{ t('ticket.summary.quantity') }}</QTh> <QTh auto-width>{{ t('ticket.summary.quantity') }}</QTh>
<QTh auto-width>{{ t('globals.description') }}</QTh> <QTh auto-width>{{ t('globals.description') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.price') }}</QTh> <QTh auto-width>{{ t('ticket.summary.price') }}</QTh>
@ -464,12 +530,62 @@ async function changeState(value) {
</template> </template>
</QTable> </QTable>
</QCard> </QCard>
<QCard class="vn-max" v-if="ticket.requests.length != 0">
<VnTitle
:url="toTicketUrl('request')"
:text="t('ticket.summary.purchaseRequest')"
/>
<QTable :rows="ticket.requests" flat style="text-align: center">
<template #header="props">
<QTr class="tr-header" :props="props">
<QTh auto-width>{{ t('ticket.summary.description') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.created') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.requester') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.attender') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.quantity') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.price') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.item') }}</QTh>
<QTh auto-width>{{ t('ticket.summary.ok') }}</QTh>
</QTr>
</template>
<template #body="props">
<QTr :props="props">
<QTd>{{ props.row.description }}</QTd>
<QTd>{{ toDate(props.row.created) }}</QTd>
<QTd>{{ props.row.requester?.user?.username }}</QTd>
<QTd>{{ props.row.atender?.user?.username }}</QTd>
<QTd>{{ props.row.quantity }}</QTd>
<QTd>{{ toCurrency(props.row.price) }}</QTd>
<QTd>
<span class="link" v-if="props.row.isOk">
{{ props.row.itemFk }}
<ItemDescriptorProxy :id="props.row.itemFk" />
</span>
</QTd>
<QTd>
<QCheckbox
v-model="props.row.isOk"
disable
:toggle-indeterminate="false"
>
<QTooltip v-if="props.row.isOk">
{{ t('Accepted') }}
</QTooltip>
<QTooltip v-else>
{{ t('Denied') }}
</QTooltip>
</QCheckbox>
</QTd>
</QTr>
</template>
</QTable>
</QCard>
</template> </template>
</CardSummary> </CardSummary>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.notes { .notes {
width: max-content; width: fit-content;
} }
.q-card.q-card--dark.q-dark.vn-one { .q-card.q-card--dark.q-dark.vn-one {

View File

@ -96,8 +96,8 @@ const openCreateModal = () => createTrackingDialogRef.value.show();
:no-data-label="t('globals.noResults')" :no-data-label="t('globals.noResults')"
> >
<template #body-cell-worker="{ row }"> <template #body-cell-worker="{ row }">
<QTd @click.stop> <QTd>
<QBtn flat color="primary"> <QBtn flat class="link" @click.stop>
{{ row.user?.name }} {{ row.user?.name }}
<WorkerDescriptorProxy :id="row.user?.worker?.id" /> <WorkerDescriptorProxy :id="row.user?.worker?.id" />
</QBtn> </QBtn>

View File

@ -5,17 +5,18 @@ import { useRoute } from 'vue-router';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import FetchedTags from 'components/ui/FetchedTags.vue'; import FetchedTags from 'components/ui/FetchedTags.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import axios from 'axios'; import axios from 'axios';
import VnTable from 'src/components/VnTable/VnTable.vue';
const route = useRoute(); const route = useRoute();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const salesRef = ref(null); const salesRef = ref(null);
const tableRef = ref();
watch( watch(
() => route.params.id, () => route.params.id,
@ -34,37 +35,42 @@ const salesFilter = computed(() => ({
const sales = ref([]); const sales = ref([]);
const packingTypeVolume = ref([]); const packingTypeVolume = ref([]);
const rows = computed(() => sales.value);
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left',
label: t('volume.item'), label: t('volume.item'),
name: 'item', name: 'itemFk',
align: 'left', chip: {
condition: () => true,
},
isId: true,
}, },
{ {
align: 'left',
label: t('volume.description'), label: t('volume.description'),
name: 'description', name: 'concept',
align: 'left', columnClass: 'expand',
cardVisible: true,
}, },
{ {
align: 'left',
label: t('volume.packingType'), label: t('volume.packingType'),
name: 'quantity', name: 'itemPackingTypeFk',
field: (row) => row.item.itemPackingTypeFk, cardVisible: true,
align: 'left', format: (row, dashIfEmpty) => dashIfEmpty(row.item.itemPackingTypeFk),
format: (val) => dashIfEmpty(val),
}, },
{ {
align: 'left',
label: t('volume.quantity'), label: t('volume.quantity'),
name: 'quantity', name: 'quantity',
field: 'quantity', cardVisible: true,
align: 'left',
}, },
{ {
label: t('volume.volumeQuantity'),
name: 'quantity',
field: (row) => row.saleVolume?.volume,
align: 'left', align: 'left',
label: t('volume.volumeQuantity'),
name: 'volume',
cardVisible: true,
}, },
]); ]);
@ -87,9 +93,7 @@ const applyVolumes = async (salesData) => {
} }
}; };
onMounted(() => { onMounted(() => (stateStore.rightDrawer = true));
stateStore.rightDrawer = true;
});
onUnmounted(() => (stateStore.rightDrawer = false)); onUnmounted(() => (stateStore.rightDrawer = false));
</script> </script>
@ -102,8 +106,12 @@ onUnmounted(() => (stateStore.rightDrawer = false));
@on-fetch="(data) => applyVolumes(data)" @on-fetch="(data) => applyVolumes(data)"
auto-load auto-load
/> />
<RightMenu v-if="packingTypeVolume.length"> <QDrawer
<template #right-panel> v-if="packingTypeVolume.length"
side="right"
:width="265"
v-model="stateStore.rightDrawer"
>
<QCard <QCard
v-for="(packingType, index) in packingTypeVolume" v-for="(packingType, index) in packingTypeVolume"
:key="index" :key="index"
@ -122,32 +130,31 @@ onUnmounted(() => (stateStore.rightDrawer = false));
<span> {{ t('volume.volume') }}: {{ packingType.volume }} </span> <span> {{ t('volume.volume') }}: {{ packingType.volume }} </span>
</QCardSection> </QCardSection>
</QCard> </QCard>
</template> </QDrawer>
</RightMenu>
<QTable <VnTable
:rows="rows" ref="tableRef"
data-key="TicketVolume"
:url="`Tickets/${route.params.id}/getSales`"
:columns="columns" :columns="columns"
row-key="id" :right-search="false"
:pagination="{ rowsPerPage: 0 }" :column-search="false"
class="full-width q-mt-md" :order="['itemPackingTypeFk']"
:no-data-label="t('globals.noResults')" auto-load
> >
<template #body-cell-item="{ row }"> <template #column-itemFk="{ row }">
<QTd> <span class="link">
<QBtn flat color="primary">
{{ row.itemFk }} {{ row.itemFk }}
<ItemDescriptorProxy :id="row.itemFk" /> <ItemDescriptorProxy :id="row.itemFk" />
</QBtn> </span>
</QTd>
</template> </template>
<template #body-cell-description="{ row }"> <template #column-concept="{ row }">
<QTd>
<div class="column">
<span>{{ row.item.name }}</span> <span>{{ row.item.name }}</span>
<span class="color-vn-label">{{ row.item.subName }}</span> <span class="color-vn-label q-pl-md">{{ row.item.subName }}</span>
<FetchedTags :item="row.item" /> <FetchedTags :item="row.item" />
</div>
</QTd>
</template> </template>
</QTable> <template #column-volume="{ rowIndex }">
<span>{{ packingTypeVolume?.[rowIndex]?.volume }}</span>
</template>
</VnTable>
</template> </template>

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