0
0
Fork 0

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

This commit is contained in:
Carlos Satorres 2024-06-21 12:42:19 +02:00
commit 8363f813de
17 changed files with 545 additions and 399 deletions

View File

@ -83,6 +83,10 @@ const $props = defineProps({
default: '',
description: 'It is used for redirect on click "save and continue"',
},
reload: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed(
@ -201,6 +205,7 @@ async function save() {
if ($props.urlCreate) notify('globals.dataCreated', 'positive');
updateAndEmit('onDataSaved', formData.value, response?.data);
if ($props.reload) await arrayData.fetch({});
} catch (err) {
console.error(err);
notify('errors.writeRequest', 'negative');

View File

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

View File

@ -17,26 +17,6 @@ const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const claimFilter = {
fields: [
'id',
'clientFk',
'created',
'workerFk',
'claimStateFk',
'packages',
'pickup',
],
include: [
{
relation: 'client',
scope: {
fields: ['name'],
},
},
],
};
const claimStates = ref([]);
const claimStatesCopy = ref([]);
const optionsList = ref([]);
@ -87,11 +67,10 @@ const statesFilter = {
/>
<FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load />
<FormModel
:url="`Claims/${route.params.id}`"
model="Claim"
:url-update="`Claims/updateClaim/${route.params.id}`"
:filter="claimFilter"
model="claim"
auto-load
:reload="true"
>
<template #form="{ data, validate, filter }">
<VnRow class="row q-gutter-md q-mb-md">

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ const balanceDueTotal = ref(0);
const selected = ref([]);
const tableColumnComponents = {
client: {
clientFk: {
component: QBtn,
props: () => ({ flat: true, class: 'link', noCaps: true }),
event: () => {},
@ -40,7 +40,7 @@ const tableColumnComponents = {
props: () => ({ flat: true, class: 'link', noCaps: true }),
event: () => {},
},
department: {
departmentName: {
component: 'span',
props: () => {},
event: () => {},
@ -102,12 +102,12 @@ const columns = computed(() => [
align: 'left',
field: 'clientName',
label: t('Client'),
name: 'client',
name: 'clientFk',
sortable: true,
},
{
align: 'left',
field: 'isWorker',
field: ({ isWorker }) => Boolean(isWorker),
label: t('Is worker'),
name: 'isWorker',
},
@ -122,7 +122,7 @@ const columns = computed(() => [
align: 'left',
field: 'departmentName',
label: t('Department'),
name: 'department',
name: 'departmentName',
sortable: true,
},
{
@ -204,48 +204,24 @@ const viewAddObservation = (rowsSelected) => {
});
};
const departments = ref(new Map());
const onFetch = async (data) => {
const salesPersonFks = data.map((item) => item.salesPersonFk);
const departmentNames = salesPersonFks.map(async (salesPersonFk) => {
try {
const { data: workerDepartment } = await axios.get(
`WorkerDepartments/${salesPersonFk}`
);
const { data: department } = await axios.get(
`Departments/${workerDepartment.departmentFk}`
);
departments.value.set(salesPersonFk, department.name);
} catch (error) {
console.error('Err: ', error);
}
});
const recoveryData = await axios.get('Recoveries');
const recoveries = recoveryData.data.map(({ clientFk, finished }) => ({
clientFk,
finished,
}));
await Promise.all(departmentNames);
data.forEach((item) => {
item.departmentName = departments.value.get(item.salesPersonFk);
item.isWorker = item.businessTypeFk === 'worker';
const recovery = recoveries.find(({ clientFk }) => clientFk === item.clientFk);
item.finished = recovery?.finished === null;
});
for (const element of data) element.isWorker = element.businessTypeFk === 'worker';
balanceDueTotal.value = data.reduce((acc, { amount = 0 }) => acc + amount, 0);
};
function exprBuilder(param, value) {
switch (param) {
case 'clientFk':
return { [`d.${param}`]: value?.id };
return { [`d.${param}`]: value };
case 'creditInsurance':
case 'amount':
case 'workerFk':

View File

@ -1,7 +1,6 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
@ -16,14 +15,13 @@ const props = defineProps({
},
});
const clients = ref();
const salespersons = ref();
const countries = ref();
const authors = ref();
const departments = ref();
</script>
<template>
<FetchData @on-fetch="(data) => (clients = data)" auto-load url="Clients" />
<FetchData
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (salespersons = data)"
@ -36,6 +34,7 @@ const authors = ref();
auto-load
url="Workers/activeWithInheritedRole"
/>
<FetchData @on-fetch="(data) => (departments = data)" auto-load url="Departments" />
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
@ -47,29 +46,22 @@ const authors = ref();
<template #body="{ params, searchFn }">
<QItem class="q-mb-sm">
<QItemSection v-if="clients">
<VnSelect
:label="t('Client')"
:options="clients"
dense
emit-value
hide-selected
map-options
option-label="name"
option-value="id"
outlined
rounded
use-input
v-model="params.clientFk"
@update:model-value="searchFn()"
auto-load
/>
</QItemSection>
<QItemSection v-else>
<QSkeleton class="full-width" type="QInput" />
</QItemSection>
<VnSelect
:label="t('Client')"
url="Clients"
dense
option-label="name"
option-value="id"
outlined
rounded
emit-value
hide-selected
map-options
v-model="params.clientFk"
use-input
@update:model-value="searchFn()"
/>
</QItem>
<QItem class="q-mb-sm">
<QItemSection v-if="salespersons">
<VnSelect
@ -93,6 +85,29 @@ const authors = ref();
<QSkeleton class="full-width" type="QInput" />
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
<QItemSection v-if="departments">
<VnSelect
:input-debounce="0"
:label="t('Departments')"
:options="departments"
dense
emit-value
hide-selected
map-options
option-label="name"
option-value="id"
outlined
rounded
use-input
v-model="params.departmentFk"
@update:model-value="searchFn()"
/>
</QItemSection>
<QItemSection v-else>
<QSkeleton class="full-width" type="QInput" />
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
<QItemSection v-if="countries">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<script setup>
import { onBeforeMount, computed, ref } from 'vue';
import { onBeforeMount, onMounted, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { Notify } from 'quasar';
import axios from 'axios';
@ -10,10 +10,12 @@ import CmrFilter from './CmrFilter.vue';
import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import { useStateStore } from 'src/stores/useStateStore';
const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const state = useStateStore();
const selected = ref([]);
const warehouses = ref([]);
@ -81,6 +83,9 @@ onBeforeMount(async () => {
const { data } = await axios.get('Warehouses');
warehouses.value = data;
});
onMounted(() => (state.rightDrawer = true));
function getApiUrl() {
return new URL(window.location).origin;
}

View File

@ -20,8 +20,8 @@ const { t } = useI18n();
const quasar = useQuasar();
const entityId = computed(() => $props.id || route.params.id);
const URL_KEY = 'NotificationSubscriptions';
const active = ref();
const available = ref();
const active = ref(new Map());
const available = ref(new Map());
async function toggleNotification(notification) {
try {
@ -56,6 +56,7 @@ const swapEntry = (from, to, key) => {
};
function setNotifications(data) {
console.log('data: ', data);
active.value = new Map(data.active);
available.value = new Map(data.available);
}