Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 4988-agencySection
gitea/salix-front/pipeline/pr-dev There was a failure building this commit Details

This commit is contained in:
Pablo Natek 2024-04-25 09:59:17 +02:00
commit e184f70be5
77 changed files with 11441 additions and 7258 deletions

View File

@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2420.01]
## [2418.01] ## [2418.01]
## [2416.01] - 2024-04-18 ## [2416.01] - 2024-04-18

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,12 @@
<script setup> <script setup>
import { ref, reactive } from 'vue'; import { ref, markRaw } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue'; import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnRow from 'components/ui/VnRow.vue';
import { QCheckbox } from 'quasar';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
@ -28,11 +31,16 @@ const $props = defineProps({
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify(); const { notify } = useNotify();
const formData = reactive({ const inputs = {
field: null, input: markRaw(VnInput),
newValue: null, number: markRaw(VnInput),
}); date: markRaw(VnInputDate),
checkbox: markRaw(QCheckbox),
select: markRaw(VnSelectFilter),
};
const newValue = ref(null);
const selectedField = ref(null);
const closeButton = ref(null); const closeButton = ref(null);
const isLoading = ref(false); const isLoading = ref(false);
@ -47,8 +55,8 @@ const submitData = async () => {
isLoading.value = true; isLoading.value = true;
const rowsToEdit = $props.rows.map((row) => ({ id: row.id, itemFk: row.itemFk })); const rowsToEdit = $props.rows.map((row) => ({ id: row.id, itemFk: row.itemFk }));
const payload = { const payload = {
field: formData.field, field: selectedField.value.field,
newValue: formData.newValue, newValue: newValue.value,
lines: rowsToEdit, lines: rowsToEdit,
}; };
@ -75,19 +83,20 @@ const closeForm = () => {
<span class="countLines">{{ ` ${rows.length} ` }}</span> <span class="countLines">{{ ` ${rows.length} ` }}</span>
<span class="title">{{ t('buy(s)') }}</span> <span class="title">{{ t('buy(s)') }}</span>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter <VnSelectFilter
:label="t('Field to edit')" :label="t('Field to edit')"
:options="fieldsOptions" :options="fieldsOptions"
hide-selected hide-selected
option-label="label" option-label="label"
option-value="field" v-model="selectedField"
v-model="formData.field" />
<component
:is="inputs[selectedField?.component || 'input']"
v-bind="selectedField?.attrs || {}"
v-model="newValue"
:label="t('Value')"
style="width: 200px"
/> />
</div>
<div class="col">
<VnInput :label="t('Value')" v-model="formData.newValue" />
</div>
</VnRow> </VnRow>
<div class="q-mt-lg row justify-end"> <div class="q-mt-lg row justify-end">
<QBtn <QBtn

View File

@ -102,7 +102,7 @@ onMounted(async () => {
}); });
onBeforeRouteLeave((to, from, next) => { onBeforeRouteLeave((to, from, next) => {
if (hasChanges.value) if (hasChanges.value && $props.observeFormChanges)
quasar.dialog({ quasar.dialog({
component: VnConfirm, component: VnConfirm,
componentProps: { componentProps: {

View File

@ -76,6 +76,16 @@ defineExpose({
<p>{{ subtitle }}</p> <p>{{ subtitle }}</p>
<slot name="form-inputs" :data="data" :validate="validate" /> <slot name="form-inputs" :data="data" :validate="validate" />
<div class="q-mt-lg row justify-end"> <div class="q-mt-lg row justify-end">
<QBtn
:label="t('globals.cancel')"
:title="t('globals.cancel')"
type="reset"
color="primary"
flat
:disabled="isLoading"
:loading="isLoading"
v-close-popup
/>
<QBtn <QBtn
:label="t('globals.save')" :label="t('globals.save')"
:title="t('globals.save')" :title="t('globals.save')"
@ -84,17 +94,6 @@ defineExpose({
:disabled="isLoading" :disabled="isLoading"
:loading="isLoading" :loading="isLoading"
/> />
<QBtn
:label="t('globals.cancel')"
:title="t('globals.cancel')"
type="reset"
color="primary"
flat
class="q-ml-sm"
:disabled="isLoading"
:loading="isLoading"
v-close-popup
/>
</div> </div>
</template> </template>
</FormModel> </FormModel>

View File

@ -56,14 +56,6 @@ const closeForm = () => {
<p>{{ subtitle }}</p> <p>{{ subtitle }}</p>
<slot name="form-inputs" /> <slot name="form-inputs" />
<div class="q-mt-lg row justify-end"> <div class="q-mt-lg row justify-end">
<QBtn
v-if="defaultSubmitButton"
:label="customSubmitButtonLabel || t('globals.save')"
type="submit"
color="primary"
:disabled="isLoading"
:loading="isLoading"
/>
<QBtn <QBtn
v-if="defaultCancelButton" v-if="defaultCancelButton"
:label="t('globals.cancel')" :label="t('globals.cancel')"
@ -74,6 +66,14 @@ const closeForm = () => {
:loading="isLoading" :loading="isLoading"
v-close-popup v-close-popup
/> />
<QBtn
v-if="defaultSubmitButton"
:label="customSubmitButtonLabel || t('globals.save')"
type="submit"
color="primary"
:disabled="isLoading"
:loading="isLoading"
/>
<slot name="customButtons" /> <slot name="customButtons" />
</div> </div>
</QCard> </QCard>

View File

@ -0,0 +1,359 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnInput from 'components/common/VnInput.vue';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
import axios from 'axios';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
customTags: {
type: Array,
default: () => [],
},
exprBuilder: {
type: Function,
default: null,
},
});
const itemCategories = ref([]);
const selectedCategoryFk = ref(null);
const selectedTypeFk = ref(null);
const itemTypesOptions = ref([]);
const suppliersOptions = ref([]);
const tagOptions = ref([]);
const tagValues = ref([]);
const categoryList = computed(() => {
return (itemCategories.value || [])
.filter((category) => category.display)
.map((category) => ({
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
});
const selectedCategory = computed(() =>
(itemCategories.value || []).find(
(category) => category?.id === selectedCategoryFk.value
)
);
const selectedType = computed(() => {
return (itemTypesOptions.value || []).find(
(type) => type?.id === selectedTypeFk.value
);
});
const selectCategory = async (params, categoryId, search) => {
if (params.categoryFk === categoryId) {
resetCategory(params);
search();
return;
}
selectedCategoryFk.value = categoryId;
params.categoryFk = categoryId;
await fetchItemTypes(categoryId);
search();
};
const resetCategory = (params) => {
selectedCategoryFk.value = null;
itemTypesOptions.value = null;
if (params) {
params.categoryFk = null;
params.typeFk = null;
}
};
const applyTags = (params, search) => {
params.tags = tagValues.value
.filter((tag) => tag.selectedTag && tag.value)
.map((tag) => ({
tagFk: tag.selectedTag.id,
tagName: tag.selectedTag.name,
value: tag.value,
}));
search();
};
const fetchItemTypes = async (id) => {
try {
const filter = {
fields: ['id', 'name', 'categoryFk'],
where: { categoryFk: id },
include: 'category',
order: 'name ASC',
};
const { data } = await axios.get('ItemTypes', {
params: { filter: JSON.stringify(filter) },
});
itemTypesOptions.value = data;
} catch (err) {
console.error('Error fetching item types', err);
}
};
const getCategoryClass = (category, params) => {
if (category.id === params?.categoryFk) {
return 'active';
}
};
const getSelectedTagValues = async (tag) => {
try {
tag.value = null;
const filter = {
fields: ['value'],
order: 'value ASC',
limit: 30,
};
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get(`Tags/${tag.selectedTag.id}/filterValue`, {
params,
});
tag.valueOptions = data;
} catch (err) {
console.error('Error getting selected tag values');
}
};
const removeTag = (index, params, search) => {
(tagValues.value || []).splice(index, 1);
applyTags(params, search);
};
</script>
<template>
<FetchData
url="ItemCategories"
limit="30"
auto-load
@on-fetch="(data) => (itemCategories = data)"
/>
<FetchData
url="Suppliers"
limit="30"
auto-load
:filter="{ fields: ['id', 'name', 'nickname'], order: 'name ASC', limit: 30 }"
@on-fetch="(data) => (suppliersOptions = data)"
/>
<FetchData
url="Tags"
:filter="{ fields: ['id', 'name', 'isFree'] }"
auto-load
limit="30"
@on-fetch="(data) => (tagOptions = data)"
/>
<VnFilterPanel
:data-key="props.dataKey"
:expr-builder="exprBuilder"
:custom-tags="customTags"
>
<template #tags="{ tag, formatFn }">
<strong v-if="tag.label === 'categoryFk'">
{{ t(selectedCategory?.name || '') }}
</strong>
<strong v-else-if="tag.label === 'typeFk'">
{{ t(selectedType?.name || '') }}
</strong>
<div v-else class="q-gutter-x-xs">
<strong>{{ t(`components.itemsFilterPanel.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #customTags="{ tags, params }">
<template v-for="tag in tags" :key="tag.label">
<VnFilterPanelChip
v-for="chip in tag.value"
:key="chip"
removable
@remove="removeTagChip(chip, params, searchFn)"
>
<div class="q-gutter-x-xs">
<strong>{{ chip.tagName }}: </strong>
<span>"{{ chip.value }}"</span>
</div>
</VnFilterPanelChip>
</template>
</template>
<template #body="{ params, searchFn }">
<QItem class="category-filter q-mt-md">
<QBtn
dense
flat
round
v-for="category in categoryList"
:key="category.name"
:class="['category', getCategoryClass(category, params)]"
:icon="category.icon"
@click="selectCategory(params, category.id, searchFn)"
>
<QTooltip>
{{ t(category.name) }}
</QTooltip>
</QBtn>
</QItem>
<QItem class="q-my-md">
<QItemSection>
<VnSelectFilter
:label="t('components.itemsFilterPanel.typeFk')"
v-model="params.typeFk"
:options="itemTypesOptions"
option-value="id"
option-label="name"
dense
outlined
rounded
use-input
:disable="!selectedCategoryFk"
@update:model-value="
(value) => {
selectedTypeFk = value;
searchFn();
}
"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption>
{{ opt.categoryName }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QItemSection>
</QItem>
<QSeparator />
<slot name="body" :params="params" :search-fn="searchFn" />
<QItem
v-for="(value, index) in tagValues"
:key="value"
class="q-mt-md filter-value"
>
<QItemSection class="col">
<VnSelectFilter
:label="t('components.itemsFilterPanel.tag')"
v-model="value.selectedTag"
:options="tagOptions"
option-label="name"
dense
outlined
rounded
:emit-value="false"
use-input
:is-clearable="false"
@update:model-value="getSelectedTagValues(value)"
/>
</QItemSection>
<QItemSection class="col">
<VnSelectFilter
v-if="!value?.selectedTag?.isFree && value.valueOptions"
:label="t('components.itemsFilterPanel.value')"
v-model="value.value"
:options="value.valueOptions || []"
option-value="value"
option-label="value"
dense
outlined
rounded
emit-value
use-input
:disable="!value"
:is-clearable="false"
@update:model-value="applyTags(params, searchFn)"
/>
<VnInput
v-else
v-model="value.value"
:label="t('components.itemsFilterPanel.value')"
:disable="!value"
is-outlined
:is-clearable="false"
@keyup.enter="applyTags(params, searchFn)"
/>
</QItemSection>
<QIcon
name="delete"
class="fill-icon-on-hover q-px-xs"
color="primary"
size="sm"
@click="removeTag(index, params, searchFn)"
/>
</QItem>
<QItem class="q-mt-lg">
<QIcon
name="add_circle"
class="fill-icon-on-hover q-px-xs"
color="primary"
size="sm"
@click="tagValues.push({})"
/>
</QItem>
</template>
</VnFilterPanel>
</template>
<style lang="scss" scoped>
.category-filter {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
.category {
padding: 8px;
width: 60px;
height: 60px;
font-size: 1.4rem;
background-color: var(--vn-accent-color);
&.active {
background-color: $primary;
}
}
}
.filter-value {
display: flex;
align-items: center;
}
</style>
<i18n>
en:
params:
supplier: Supplier
from: From
to: To
active: Is active
visible: Is visible
floramondo: Is floramondo
salesPersonFk: Buyer
categoryFk: Category
es:
params:
supplier: Proveedor
from: Desde
to: Hasta
active: Activo
visible: Visible
floramondo: Floramondo
salesPersonFk: Comprador
categoryFk: Categoría
</i18n>

View File

@ -0,0 +1,78 @@
<script setup>
import { onBeforeMount, computed } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize';
import VnSubToolbar from '../ui/VnSubToolbar.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import LeftMenu from 'components/LeftMenu.vue';
const props = defineProps({
dataKey: { type: String, required: true },
baseUrl: { type: String, default: undefined },
customUrl: { type: String, default: undefined },
filter: { type: Object, default: () => {} },
descriptor: { type: Object, required: true },
searchbarDataKey: { type: String, default: undefined },
searchbarUrl: { type: String, default: undefined },
searchbarLabel: { type: String, default: '' },
searchbarInfo: { type: String, default: '' },
});
const { t } = useI18n();
const stateStore = useStateStore();
const route = useRoute();
const url = computed(() => {
if (props.baseUrl) return `${props.baseUrl}/${route.params.id}`;
return props.customUrl;
});
const arrayData = useArrayData(props.dataKey, {
url: url.value,
filter: props.filter,
});
onBeforeMount(async () => {
if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id };
await arrayData.fetch({ append: false });
});
if (props.baseUrl) {
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
arrayData.store.url = `${props.baseUrl}/${route.params.id}`;
await arrayData.fetch({ append: false });
}
});
}
</script>
<template>
<Teleport
to="#searchbar"
v-if="stateStore.isHeaderMounted() && props.searchbarDataKey"
>
<VnSearchbar
:data-key="props.searchbarDataKey"
:url="props.searchbarUrl"
:label="t(props.searchbarLabel)"
:info="t(props.searchbarInfo)"
/>
</Teleport>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<component :is="descriptor" />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="[useCardSize(), $attrs.class]">
<RouterView />
</div>
</QPage>
</QPageContainer>
</template>

View File

@ -187,7 +187,7 @@ const columns = computed(() => [
downloadFile( downloadFile(
prop.row.id, prop.row.id,
$props.downloadModel, $props.downloadModel,
null, undefined,
prop.row.download prop.row.download
), ),
}, },

View File

@ -200,8 +200,8 @@ es:
pendingPayment: 'Su pedido está pendiente de pago. pendingPayment: 'Su pedido está pendiente de pago.
Por favor, entre en la página web y efectue el pago con tarjeta. Muchas gracias.' Por favor, entre en la página web y efectue el pago con tarjeta. Muchas gracias.'
minAmount: 'Es necesario un importe mínimo de 50 (Sin IVA) en su pedido minAmount: 'Es necesario un importe mínimo de 50 (Sin IVA) en su pedido
{ orderId } del día { shipped } para recibirlo sin portes adicionales.' { orderId } con llegada { landing } para recibirlo sin portes adicionales.'
orderChanges: 'Pedido {orderId} día { shipped }: { changes }' orderChanges: 'Pedido {orderId} con llegada estimada día { landing }: { changes }'
en: Inglés en: Inglés
es: Español es: Español
fr: Francés fr: Francés
@ -215,11 +215,12 @@ fr:
Message: Message Message: Message
messageTooltip: Les caractères spéciaux comme les accents comptent comme plusieurs messageTooltip: Les caractères spéciaux comme les accents comptent comme plusieurs
templates: templates:
pendingPayment: 'Votre commande est en attente de paiement. pendingPayment: 'Verdnatura : Commande en attente de règlement. Veuillez régler votre commande avant 9h.
Veuillez vous connecter sur le site web et effectuer le paiement par carte. Merci beaucoup.' Sinon elle sera décalée en fonction de vos jours de livraison . Merci'
minAmount: 'Un montant minimum de 50 (TVA non incluse) est requis pour votre commande minAmount: 'Verdnatura vous rappelle :
{ orderId } du { shipped } afin de la recevoir sans frais de port supplémentaires.' Montant minimum nécessaire de 50 euros pour recevoir la commande { orderId } livraison { landing }.
orderChanges: 'Commande { orderId } du { shipped }: { changes }' Merci.'
orderChanges: 'Commande {orderId} livraison {landing} indisponible/s. Désolés pour le dérangement.'
en: Anglais en: Anglais
es: Espagnol es: Espagnol
fr: Français fr: Français
@ -236,8 +237,8 @@ pt:
pendingPayment: 'Seu pedido está pendente de pagamento. pendingPayment: 'Seu pedido está pendente de pagamento.
Por favor, acesse o site e faça o pagamento com cartão. Muito obrigado.' Por favor, acesse o site e faça o pagamento com cartão. Muito obrigado.'
minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido minAmount: 'É necessário um valor mínimo de 50 (sem IVA) em seu pedido
{ orderId } do dia { shipped } para recebê-lo sem custos de envio adicionais.' { orderId } do dia { landing } para recebê-lo sem custos de envio adicionais.'
orderChanges: 'Pedido { orderId } dia { shipped }: { changes }' orderChanges: 'Pedido { orderId } com chegada dia { landing }: { changes }'
en: Inglês en: Inglês
es: Espanhol es: Espanhol
fr: Francês fr: Francês

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onBeforeMount, useSlots, watch, computed, ref } from 'vue'; import { onBeforeMount, watch, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue'; import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
@ -38,7 +38,6 @@ const $props = defineProps({
}); });
const state = useState(); const state = useState();
const slots = useSlots();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const arrayData = useArrayData($props.dataKey || $props.module, { const arrayData = useArrayData($props.dataKey || $props.module, {
@ -47,7 +46,7 @@ const arrayData = useArrayData($props.dataKey || $props.module, {
skip: 0, skip: 0,
}); });
const { store } = arrayData; const { store } = arrayData;
const entity = computed(() =>Array.isArray( store.data) ? store.data[0] : store.data); const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data));
const isLoading = ref(false); const isLoading = ref(false);
defineExpose({ defineExpose({
@ -55,14 +54,12 @@ defineExpose({
}); });
onBeforeMount(async () => { onBeforeMount(async () => {
await getData(); await getData();
watch( watch($props, async () => await getData());
() => $props.url,
async () => await getData()
);
}); });
async function getData() { async function getData() {
store.url = $props.url; store.url = $props.url;
store.filter = $props.filter ?? {};
isLoading.value = true; isLoading.value = true;
try { try {
const { data } = await arrayData.fetch({ append: false, updateRouter: false }); const { data } = await arrayData.fetch({ append: false, updateRouter: false });
@ -117,7 +114,7 @@ const emit = defineEmits(['onFetch']);
icon="more_vert" icon="more_vert"
round round
size="md" size="md"
:class="{ invisible: !slots.menu }" :class="{ invisible: !$slots.menu }"
> >
<QTooltip> <QTooltip>
{{ t('components.cardDescriptor.moreOptions') }} {{ t('components.cardDescriptor.moreOptions') }}

View File

@ -15,7 +15,7 @@ const props = defineProps({
default: null, default: null,
}, },
entityId: { entityId: {
type: Number, type: [Number, String],
default: null, default: null,
}, },
dataKey: { dataKey: {
@ -32,7 +32,7 @@ const arrayData = useArrayData(props.dataKey || route.meta.moduleName, {
skip: 0, skip: 0,
}); });
const { store } = arrayData; const { store } = arrayData;
const entity = computed(() => Array.isArray(store.data) ? store.data[0] : store.data); const entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data));
const isLoading = ref(false); const isLoading = ref(false);
defineExpose({ defineExpose({
@ -48,9 +48,10 @@ onBeforeMount(async () => {
async function fetch() { async function fetch() {
store.url = props.url; store.url = props.url;
store.filter = props.filter ?? {};
isLoading.value = true; isLoading.value = true;
const { data } = await arrayData.fetch({ append: false, updateRouter: false }); const { data } = await arrayData.fetch({ append: false, updateRouter: false });
emit('onFetch', data); emit('onFetch', Array.isArray(data) ? data[0] : data);
isLoading.value = false; isLoading.value = false;
} }
</script> </script>

View File

@ -12,12 +12,23 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
viewCustomization: {
type: String,
default: '',
},
}); });
const $q = useQuasar(); const $q = useQuasar();
// El objetivo de asignar las clases de personalización desde el wrapper es no tener conflictos entre vistas que usen el mismo componente
const viewCustomizationClasses = {
workerCalendar: 'worker-calendar-customizations',
};
const containerClasses = computed(() => { const containerClasses = computed(() => {
const classes = ['main-container-background']; const classes = ['main-container-background'];
if (viewCustomizationClasses[$props.viewCustomization])
classes.push(viewCustomizationClasses[$props.viewCustomization]);
if ($props.bordered) classes.push('--bordered'); if ($props.bordered) classes.push('--bordered');
if ($props.transparentBackground) classes.push('transparent-background'); if ($props.transparentBackground) classes.push('transparent-background');
else classes.push($q.dark.isActive ? '--dark' : '--light'); else classes.push($q.dark.isActive ? '--dark' : '--light');
@ -33,6 +44,47 @@ const containerClasses = computed(() => {
</template> </template>
<style lang="scss"> <style lang="scss">
@import '../../css/quasar.variables.scss';
:root {
// Cambia los colores del día actual del calendario por los de salix
--calendar-border-current-dark: #84d0e2 2px solid;
--calendar-border-current: #84d0e2 2px solid;
--calendar-current-color-dark: #84d0e2;
// Colores de fondo del calendario en dark mode
--calendar-outside-background-dark: #222;
--calendar-background-dark: #222;
}
// Clases para modificar el color de fecha seleccionada en componente QCalendarMonth
.q-dark div .q-calendar-mini .q-calendar-month__day.q-selected .q-calendar__button {
background-color: $primary !important;
color: white !important;
}
.q-calendar-mini .q-calendar-month__day.q-selected .q-calendar__button {
background-color: $primary !important;
color: white !important;
}
.q-calendar-month__head--weekday {
// Transforma los nombres de los días de la semana a mayúsculas
text-transform: capitalize;
}
.transparent-background {
--calendar-background-dark: transparent;
--calendar-background: transparent;
--calendar-outside-background-dark: transparent;
}
.q-calendar__button {
&:hover {
background-color: var(--vn-accent-color);
cursor: pointer;
}
}
.main-container-background { .main-container-background {
--calendar-current-background-dark: transparent; --calendar-current-background-dark: transparent;
@ -45,14 +97,64 @@ const containerClasses = computed(() => {
} }
&.--bordered { &.--bordered {
border: 1px solid black; border: 1px solid #222;
} }
} }
.transparent-background { .worker-calendar-customizations {
--calendar-background-dark: transparent; .q-calendar__button {
--calendar-background: transparent; width: 32px;
--calendar-outside-background-dark: transparent; height: 32px;
font-size: 13px;
&:hover {
background-color: var(--vn-accent-color);
cursor: pointer;
}
}
.q-calendar-month__week--days > div:nth-child(6),
.q-calendar-month__week--days > div:nth-child(7) {
// Cambia el color de los días sábado y domingo
color: #777777;
}
.q-calendar-month__week--wrapper {
margin-bottom: 4px;
}
.q-calendar-month__workweek {
height: 32px;
display: flex;
justify-content: center;
}
.q-calendar__button--bordered {
color: $info !important;
}
.q-calendar-month__day--content {
position: absolute;
top: 1;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
.q-outside .calendar-event {
display: none;
}
.q-calendar-month__workweek,
.q-calendar-month__head--workweek,
.q-calendar-month__head--weekday.q-calendar__center.q-calendar__ellipsis {
text-transform: capitalize;
color: #777;
font-weight: bold;
font-size: 0.8rem;
text-align: center;
}
} }
.nav-container { .nav-container {

View File

@ -32,7 +32,7 @@ async function insert() {
<template> <template>
<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 :descriptor="false" :worker-id="1" size="md" /> <VnAvatar :worker-id="currentUser.id" size="md" />
<div class="full-width row justify-between q-pa-xs"> <div class="full-width row justify-between q-pa-xs">
<VnUserLink :name="t('New note')" :worker-id="currentUser.id" /> <VnUserLink :name="t('New note')" :worker-id="currentUser.id" />
{{ t('globals.now') }} {{ t('globals.now') }}
@ -78,8 +78,8 @@ async function insert() {
<TransitionGroup name="list" tag="div" class="column items-center full-width"> <TransitionGroup name="list" tag="div" class="column items-center full-width">
<QCard <QCard
class="q-pa-xs q-mb-sm full-width" class="q-pa-xs q-mb-sm full-width"
v-for="note in rows" v-for="(note, index) in rows"
:key="note.id" :key="note.id ?? index"
> >
<QCardSection horizontal> <QCardSection horizontal>
<VnAvatar <VnAvatar

View File

@ -77,7 +77,6 @@ const arrayData = useArrayData(props.dataKey, {
userParams: props.userParams, userParams: props.userParams,
exprBuilder: props.exprBuilder, exprBuilder: props.exprBuilder,
}); });
const hasMoreData = ref();
const store = arrayData.store; const store = arrayData.store;
onMounted(() => { onMounted(() => {
@ -97,7 +96,7 @@ const addFilter = async (filter, params) => {
async function fetch() { async function fetch() {
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
if (!arrayData.hasMoreData.value) { if (!store.hasMoreData) {
isLoading.value = false; isLoading.value = false;
} }
emit('onFetch', store.data); emit('onFetch', store.data);
@ -110,8 +109,8 @@ async function paginate() {
isLoading.value = true; isLoading.value = true;
await arrayData.loadMore(); await arrayData.loadMore();
if (!arrayData.hasMoreData.value) { if (!store.hasMoreData) {
if (store.userParamsChanged) arrayData.hasMoreData.value = true; if (store.userParamsChanged) store.hasMoreData = true;
store.userParamsChanged = false; store.userParamsChanged = false;
endPagination(); endPagination();
return; return;
@ -132,9 +131,7 @@ function endPagination() {
emit('onPaginate'); emit('onPaginate');
} }
async function onLoad(index, done) { async function onLoad(index, done) {
if (!store.data) { if (!store.data) return done();
return done();
}
if (store.data.length === 0 || !props.url) return done(false); if (store.data.length === 0 || !props.url) return done(false);
@ -142,7 +139,7 @@ async function onLoad(index, done) {
await paginate(); await paginate();
let isDone = false; let isDone = false;
if (store.userParamsChanged) isDone = !arrayData.hasMoreData.value; if (store.userParamsChanged) isDone = !store.hasMoreData;
done(isDone); done(isDone);
} }
@ -182,13 +179,12 @@ defineExpose({ fetch, addFilter });
</QCard> </QCard>
</div> </div>
</div> </div>
<QInfiniteScroll <QInfiniteScroll
v-if="store.data" v-if="store.data"
@load="onLoad" @load="onLoad"
:offset="offset" :offset="offset"
:disable="disableInfiniteScroll || !arrayData.hasMoreData"
class="full-width" class="full-width"
:disable="disableInfiniteScroll || !store.hasMoreData"
v-bind="$attrs" v-bind="$attrs"
> >
<slot name="body" :rows="store.data"></slot> <slot name="body" :rows="store.data"></slot>
@ -196,7 +192,10 @@ defineExpose({ fetch, addFilter });
<QSpinner color="orange" size="md" /> <QSpinner color="orange" size="md" />
</div> </div>
</QInfiniteScroll> </QInfiniteScroll>
<div v-if="!isLoading && hasMoreData" class="w-full flex justify-center q-mt-md"> <div
v-if="!isLoading && store.hasMoreData"
class="w-full flex justify-center q-mt-md"
>
<QBtn color="primary" :label="t('Load more data')" @click="paginate()" /> <QBtn color="primary" :label="t('Load more data')" @click="paginate()" />
</div> </div>
</template> </template>

View File

@ -131,13 +131,6 @@ async function search() {
/> />
</template> </template>
<template #append> <template #append>
<QIcon
v-if="searchText !== ''"
name="close"
@click="searchText = ''"
class="cursor-pointer"
/>
<QIcon <QIcon
v-if="props.info && $q.screen.gt.xs" v-if="props.info && $q.screen.gt.xs"
name="info" name="info"

View File

@ -1,11 +1,27 @@
<script setup> <script setup>
import { onMounted, onUnmounted } from 'vue'; import { onMounted, onUnmounted, ref } from 'vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
const stateStore = useStateStore(); const stateStore = useStateStore();
const actions = ref(null);
const data = ref(null);
const opts = { subtree: true, childList: true, attributes: true };
const hasContent = ref(false);
onMounted(() => { onMounted(() => {
stateStore.toggleSubToolbar(); stateStore.toggleSubToolbar();
actions.value = document.querySelector('#st-actions');
data.value = document.querySelector('#st-data');
if (!actions.value && !data.value) return;
// Check if there's content to display
const observer = new MutationObserver(
() =>
(hasContent.value =
actions.value.childNodes.length + data.value.childNodes.length)
);
if (actions.value) observer.observe(actions.value, opts);
if (data.value) observer.observe(data.value, opts);
}); });
onUnmounted(() => { onUnmounted(() => {
@ -14,7 +30,10 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<QToolbar class="bg-vn-section-color justify-end sticky"> <QToolbar
class="justify-end sticky"
v-show="hasContent || $slots['st-actions'] || $slots['st-data']"
>
<slot name="st-data"> <slot name="st-data">
<div id="st-data"></div> <div id="st-data"></div>
</slot> </slot>

View File

@ -9,12 +9,9 @@ const arrayDataStore = useArrayDataStore();
export function useArrayData(key, userOptions) { export function useArrayData(key, userOptions) {
if (!key) throw new Error('ArrayData: A key is required to use this composable'); if (!key) throw new Error('ArrayData: A key is required to use this composable');
if (!arrayDataStore.get(key)) { if (!arrayDataStore.get(key)) arrayDataStore.set(key);
arrayDataStore.set(key);
}
const store = arrayDataStore.get(key); const store = arrayDataStore.get(key);
const hasMoreData = ref(false);
const route = useRoute(); const route = useRoute();
let canceller = null; let canceller = null;
@ -22,6 +19,7 @@ export function useArrayData(key, userOptions) {
onMounted(() => { onMounted(() => {
setOptions(); setOptions();
store.skip = 0;
const query = route.query; const query = route.query;
if (query.params) { if (query.params) {
@ -29,9 +27,7 @@ export function useArrayData(key, userOptions) {
} }
}); });
if (key && userOptions) { if (key && userOptions) setOptions();
setOptions();
}
function setOptions() { function setOptions() {
const allowedOptions = [ const allowedOptions = [
@ -96,8 +92,7 @@ export function useArrayData(key, userOptions) {
}); });
const { limit } = filter; const { limit } = filter;
hasMoreData.value = limit && response.data.length >= limit; store.hasMoreData = limit && response.data.length >= limit;
store.hasMoreData = hasMoreData.value;
if (append) { if (append) {
if (!store.data) store.data = []; if (!store.data) store.data = [];
@ -156,9 +151,10 @@ export function useArrayData(key, userOptions) {
delete store.userParams[param]; delete store.userParams[param];
delete params[param]; delete params[param];
if (store.filter?.where) { if (store.filter?.where) {
delete store.filter.where[ const key = Object.keys(
Object.keys(exprBuilder ? exprBuilder(param) : param)[0] exprBuilder && exprBuilder(param) ? exprBuilder(param) : param
]; );
if (key[0]) delete store.filter.where[key[0]];
if (Object.keys(store.filter.where).length === 0) { if (Object.keys(store.filter.where).length === 0) {
delete store.filter.where; delete store.filter.where;
} }
@ -169,7 +165,7 @@ export function useArrayData(key, userOptions) {
} }
async function loadMore() { async function loadMore() {
if (!hasMoreData.value && !store.hasMoreData) return; if (!store.hasMoreData) return;
store.skip = store.limit * page.value; store.skip = store.limit * page.value;
page.value += 1; page.value += 1;
@ -211,7 +207,6 @@ export function useArrayData(key, userOptions) {
destroy, destroy,
loadMore, loadMore,
store, store,
hasMoreData,
totalRows, totalRows,
updateStateParams, updateStateParams,
isLoading, isLoading,

View File

@ -119,6 +119,11 @@ select:-webkit-autofill {
font-variation-settings: 'FILL' 1; font-variation-settings: 'FILL' 1;
} }
.fill-icon-on-hover:hover {
font-variation-settings: 'FILL' 1;
cursor: pointer;
}
.vn-table-separation-row { .vn-table-separation-row {
height: 16px !important; height: 16px !important;
background-color: var(--vn-section-color) !important; background-color: var(--vn-section-color) !important;
@ -136,6 +141,19 @@ select:-webkit-autofill {
background-color: var(--vn-section-color); background-color: var(--vn-section-color);
} }
.q-checkbox {
& .q-checkbox__label {
color: var(--vn-text-color);
}
& .q-checkbox__inner {
color: var(--vn-label-color);
}
}
.tr-header {
color: var(--vn-label-color);
}
.q-chip, .q-chip,
.q-notification__message, .q-notification__message,
.q-notification__icon { .q-notification__icon {
@ -170,13 +188,6 @@ input::-webkit-inner-spin-button {
-moz-appearance: none; -moz-appearance: none;
} }
// Clases para modificar el color de fecha seleccionada en componente QCalendarMonth .q-scrollarea__content {
.q-dark div .q-calendar-mini .q-calendar-month__day.q-selected .q-calendar__button { max-width: 100%;
background-color: $primary !important;
color: white !important;
}
.q-calendar-mini .q-calendar-month__day.q-selected .q-calendar__button {
background-color: $primary !important;
color: white !important;
} }

View File

@ -91,3 +91,42 @@ export function toDateTimeFormat(date, showSeconds = false) {
second: showSeconds ? '2-digit' : undefined, second: showSeconds ? '2-digit' : undefined,
}); });
} }
/**
* Converts seconds to a formatted string representing hours and minutes (hh:mm).
* @param {number} seconds - The number of seconds to convert.
* @param {boolean} includeHSuffix - Optional parameter indicating whether to include "h." after the hour.
* @returns {string} A string representing the time in the format "hh:mm" with optional "h." suffix.
*/
export function secondsToHoursMinutes(seconds, includeHSuffix = true) {
if (!seconds) return includeHSuffix ? '00:00 h.' : '00:00';
const hours = Math.floor(seconds / 3600);
const remainingMinutes = seconds % 3600;
const minutes = Math.floor(remainingMinutes / 60);
const formattedHours = hours < 10 ? '0' + hours : hours;
const formattedMinutes = minutes < 10 ? '0' + minutes : minutes;
// Append "h." if includeHSuffix is true
const suffix = includeHSuffix ? ' h.' : '';
// Return formatted string
return formattedHours + ':' + formattedMinutes + suffix;
}
export function getTimeDifferenceWithToday(date) {
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
date = new Date(date);
date.setHours(0, 0, 0, 0);
return today - date;
}
export function isLower(date) {
return getTimeDifferenceWithToday(date) > 0;
}
export function isBigger(date) {
return getTimeDifferenceWithToday(date) < 0;
}

View File

@ -83,6 +83,7 @@ globals:
selectFile: Select a file selectFile: Select a file
copyClipboard: Copy on clipboard copyClipboard: Copy on clipboard
salesPerson: SalesPerson salesPerson: SalesPerson
send: Send
code: Code code: Code
pageTitles: pageTitles:
summary: Summary summary: Summary
@ -816,6 +817,7 @@ worker:
pbx: Private Branch Exchange pbx: Private Branch Exchange
log: Log log: Log
calendar: Calendar calendar: Calendar
timeControl: Time control
list: list:
name: Name name: Name
email: Email email: Email
@ -1122,6 +1124,7 @@ item:
list: List list: List
diary: Diary diary: Diary
tags: Tags tags: Tags
fixedPrice: Fixed prices
wasteBreakdown: Waste breakdown wasteBreakdown: Waste breakdown
itemCreate: New item itemCreate: New item
descriptor: descriptor:
@ -1151,6 +1154,22 @@ item:
stemMultiplier: Multiplier stemMultiplier: Multiplier
producer: Producer producer: Producer
landed: Landed landed: Landed
fixedPrice:
itemId: Item ID
groupingPrice: Grouping price
packingPrice: Packing price
hasMinPrice: Has min price
minPrice: Min price
started: Started
ended: Ended
warehouse: Warehouse
create:
name: Name
tag: Tag
priority: Priority
type: Type
intrastat: Intrastat
origin: Origin
agency: agency:
isOwn: Own isOwn: Own
isAnyVolumeAllowed: Any volume allowed isAnyVolumeAllowed: Any volume allowed
@ -1164,6 +1183,25 @@ agency:
label: Search agency... label: Search agency...
components: components:
topbar: {} topbar: {}
itemsFilterPanel:
typeFk: Type
tag: Tag
value: Value
# ItemFixedPriceFilter
buyerFk: Buyer
warehouseFk: Warehouse
started: From
ended: To
mine: For me
hasMinPrice: Minimum price
# LatestBuysFilter
salesPersonFk: Buyer
supplierFk: Supplier
from: From
to: To
active: Is active
visible: Is visible
floramondo: Is floramondo
userPanel: userPanel:
copyToken: Token copied to clipboard copyToken: Token copied to clipboard
settings: Settings settings: Settings

View File

@ -83,6 +83,7 @@ globals:
selectFile: Seleccione un fichero selectFile: Seleccione un fichero
copyClipboard: Copiar en portapapeles copyClipboard: Copiar en portapapeles
salesPerson: Comercial salesPerson: Comercial
send: Enviar
code: Código code: Código
pageTitles: pageTitles:
summary: Resumen summary: Resumen
@ -814,6 +815,7 @@ worker:
pbx: Centralita pbx: Centralita
log: Historial log: Historial
calendar: Calendario calendar: Calendario
timeControl: Control de horario
list: list:
name: Nombre name: Nombre
email: Email email: Email
@ -1121,6 +1123,7 @@ item:
list: Listado list: Listado
diary: Histórico diary: Histórico
tags: Etiquetas tags: Etiquetas
fixedPrice: Precios fijados
wasteBreakdown: Deglose de mermas wasteBreakdown: Deglose de mermas
itemCreate: Nuevo artículo itemCreate: Nuevo artículo
descriptor: descriptor:
@ -1150,6 +1153,15 @@ item:
stemMultiplier: Multiplicador stemMultiplier: Multiplicador
producer: Productor producer: Productor
landed: F. entrega landed: F. entrega
fixedPrice:
itemId: ID Artículo
groupingPrice: Precio grouping
packingPrice: Precio packing
hasMinPrice: Tiene precio mínimo
minPrice: Precio min
started: Inicio
ended: Fin
warehouse: Almacén
create: create:
name: Nombre name: Nombre
tag: Etiqueta tag: Etiqueta
@ -1167,6 +1179,25 @@ agency:
agency: Agencia agency: Agencia
components: components:
topbar: {} topbar: {}
itemsFilterPanel:
typeFk: Tipo
tag: Etiqueta
value: Valor
# ItemFixedPriceFilter
buyerFk: Comprador
warehouseFk: Almacén
started: Desde
ended: Hasta
mine: Para mi
hasMinPrice: Precio mínimo
# LatestBuysFilter
salesPersonFk: Comprador
supplierFk: Proveedor
from: Desde
to: Hasta
active: Activo
visible: Visible
floramondo: Floramondo
userPanel: userPanel:
copyToken: Token copiado al portapapeles copyToken: Token copiado al portapapeles
settings: Configuración settings: Configuración

View File

@ -1,46 +1,15 @@
<script setup> <script setup>
import LeftMenu from 'components/LeftMenu.vue'; import VnCard from 'components/common/VnCard.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore';
import { useI18n } from 'vue-i18n';
import ClaimDescriptor from './ClaimDescriptor.vue'; import ClaimDescriptor from './ClaimDescriptor.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const { t } = useI18n();
</script> </script>
<template> <template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> <VnCard
<VnSearchbar data-key="Claim"
data-key="ClaimList" base-url="Claims"
url="Claims/filter" :descriptor="ClaimDescriptor"
:label="t('Search claim')" searchbar-data-key="ClaimList"
:info="t('You can search by claim id or customer name')" searchbar-url="Claims/filter"
searchbar-label="Search claim"
searchbar-info="You can search by claim id or customer name"
/> />
</Teleport>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<ClaimDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template> </template>
<i18n>
es:
Search claim: Buscar reclamación
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
Details: Detalles
Notes: Notas
Action: Acción
</i18n>

View File

@ -16,7 +16,7 @@ const claimId = computed(() => $props.id || route.params.id);
const claimFilter = { const claimFilter = {
where: { claimFk: claimId.value }, where: { claimFk: claimId.value },
fields: ['created', 'workerFk', 'text'], fields: ['id', 'created', 'workerFk', 'text'],
include: { include: {
relation: 'worker', relation: 'worker',
scope: { scope: {

View File

@ -0,0 +1,2 @@
Search claim: Buscar reclamación
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente

View File

@ -1,43 +1,15 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import VnCard from 'components/common/VnCard.vue';
import { useStateStore } from 'stores/useStateStore';
import CustomerDescriptor from './CustomerDescriptor.vue'; import CustomerDescriptor from './CustomerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const { t } = useI18n();
</script> </script>
<template> <template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> <VnCard
<VnSearchbar data-key="Client"
data-key="CustomerList" base-url="Clients"
url="Clients/filter" :descriptor="CustomerDescriptor"
:label="t('Search customer')" searchbar-data-key="CustomerList"
:info="t('You can search by customer id or name')" searchbar-url="Clients/filter"
searchbar-label="Search customer"
searchbar-info="You can search by customer id or name"
/> />
</Teleport>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<CustomerDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template> </template>
<i18n>
es:
Search customer: Buscar cliente
You can search by customer id or name: Puedes buscar por id o nombre del cliente
</i18n>

View File

@ -1,3 +1,5 @@
Search customer: Buscar cliente
You can search by customer id or name: Puedes buscar por id o nombre del cliente
customerFilter: customerFilter:
filter: filter:
name: Nombre name: Nombre

View File

@ -1,26 +1,13 @@
<script setup> <script setup>
import { useStateStore } from 'stores/useStateStore'; import VnCard from 'components/common/VnCard.vue';
import DepartmentDescriptor from 'pages/Department/Card/DepartmentDescriptor.vue'; import DepartmentDescriptor from 'pages/Department/Card/DepartmentDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
const stateStore = useStateStore();
</script> </script>
<template> <template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256"> <VnCard
<QScrollArea class="fit"> class="q-pa-md column items-center"
<DepartmentDescriptor /> v-bind="{ ...$attrs }"
<QSeparator /> data-key="Department"
<LeftMenu source="card" /> base-url="Departments"
</QScrollArea> :descriptor="DepartmentDescriptor"
</QDrawer> />
<QPageContainer>
<QPage>
<VnSubToolbar />
<div class="q-pa-md column items-center">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template> </template>

View File

@ -27,6 +27,7 @@ onMounted(async () => {
<template> <template>
<CardSummary <CardSummary
data-key="DepartmentSummary"
ref="summary" ref="summary"
:url="`Departments/${entityId}`" :url="`Departments/${entityId}`"
class="full-width" class="full-width"

View File

@ -1,48 +1,15 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import VnCard from 'components/common/VnCard.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import EntryDescriptor from './EntryDescriptor.vue'; import EntryDescriptor from './EntryDescriptor.vue';
import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize';
const { t } = useI18n();
const stateStore = useStateStore();
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()"> <VnCard
<Teleport to="#searchbar"> data-key="Entry"
<VnSearchbar base-url="Entries"
data-key="EntryList" :descriptor="EntryDescriptor"
url="Entries/filter" searchbar-data-key="EntryList"
:label="t('Search entries')" searchbar-url="Entries/filter"
:info="t('You can search by entry reference')" searchbar-label="Search entries"
searchbar-info="You can search by entry reference"
/> />
</Teleport>
</template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<EntryDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template> </template>
<i18n>
es:
Search entries: Buscar entradas
You can search by entry reference: Puedes buscar por referencia de la entrada
</i18n>

View File

@ -650,7 +650,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</QToolbar> </QToolbar>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8"> <QScrollArea class="fit text-grey-8">
<EntryLatestBuysFilter data-key="EntryLatestBuys" :tags="tags" /> <EntryLatestBuysFilter data-key="EntryLatestBuys" />
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">

View File

@ -1,141 +1,26 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnInput from 'components/common/VnInput.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue'; import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import ItemsFilterPanel from 'src/components/ItemsFilterPanel.vue';
import axios from 'axios';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({
defineProps({
dataKey: { dataKey: {
type: String, type: String,
required: true, required: true,
}, },
}); });
const itemCategories = ref([]);
const selectedCategoryFk = ref(null);
const selectedTypeFk = ref(null);
const itemTypesOptions = ref([]);
const itemTypeWorkersOptions = ref([]); const itemTypeWorkersOptions = ref([]);
const suppliersOptions = ref([]); const suppliersOptions = ref([]);
const tagOptions = ref([]);
const tagValues = ref([]);
const categoryList = computed(() => {
return (itemCategories.value || [])
.filter((category) => category.display)
.map((category) => ({
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
});
const selectedCategory = computed(() =>
(itemCategories.value || []).find(
(category) => category?.id === selectedCategoryFk.value
)
);
const selectedType = computed(() => {
return (itemTypesOptions.value || []).find(
(type) => type?.id === selectedTypeFk.value
);
});
const selectCategory = async (params, categoryId, search) => {
if (params.categoryFk === categoryId) {
resetCategory(params);
search();
return;
}
selectedCategoryFk.value = categoryId;
params.categoryFk = categoryId;
await fetchItemTypes(categoryId);
search();
};
const resetCategory = (params) => {
selectedCategoryFk.value = null;
itemTypesOptions.value = null;
if (params) {
params.categoryFk = null;
params.typeFk = null;
}
};
const applyTags = (params, search) => {
params.tags = tagValues.value
.filter((tag) => tag.selectedTag && tag.value)
.map((tag) => ({
tagFk: tag.selectedTag.id,
tagName: tag.selectedTag.name,
value: tag.value,
}));
search();
};
const fetchItemTypes = async (id) => {
try {
const filter = {
fields: ['id', 'name', 'categoryFk'],
where: { categoryFk: id },
include: 'category',
order: 'name ASC',
};
const { data } = await axios.get('ItemTypes', {
params: { filter: JSON.stringify(filter) },
});
itemTypesOptions.value = data;
} catch (err) {
console.error('Error fetching item types', err);
}
};
const getCategoryClass = (category, params) => {
if (category.id === params?.categoryFk) {
return 'active';
}
};
const getSelectedTagValues = async (tag) => {
try {
tag.value = null;
const filter = {
fields: ['value'],
order: 'value ASC',
limit: 30,
};
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get(`Tags/${tag.selectedTag.id}/filterValue`, {
params,
});
tag.valueOptions = data;
} catch (err) {
console.error('Error getting selected tag values');
}
};
const removeTag = (index, params, search) => {
(tagValues.value || []).splice(index, 1);
applyTags(params, search);
};
</script> </script>
<template> <template>
<FetchData
url="ItemCategories"
limit="30"
auto-load
@on-fetch="(data) => (itemCategories = data)"
/>
<FetchData <FetchData
url="TicketRequests/getItemTypeWorker" url="TicketRequests/getItemTypeWorker"
limit="30" limit="30"
@ -150,102 +35,12 @@ const removeTag = (index, params, search) => {
:filter="{ fields: ['id', 'name', 'nickname'], order: 'name ASC', limit: 30 }" :filter="{ fields: ['id', 'name', 'nickname'], order: 'name ASC', limit: 30 }"
@on-fetch="(data) => (suppliersOptions = data)" @on-fetch="(data) => (suppliersOptions = data)"
/> />
<FetchData <ItemsFilterPanel :data-key="dataKey" :custom-tags="['tags']">
url="Tags"
:filter="{ fields: ['id', 'name', 'isFree'] }"
auto-load
limit="30"
@on-fetch="(data) => (tagOptions = data)"
/>
<VnFilterPanel
:data-key="props.dataKey"
:expr-builder="exprBuilder"
:custom-tags="['tags']"
@init="onFilterInit"
@remove="clearFilter"
>
<template #tags="{ tag, formatFn }">
<strong v-if="tag.label === 'categoryFk'">
{{ t(selectedCategory?.name || '') }}
</strong>
<strong v-else-if="tag.label === 'typeFk'">
{{ t(selectedType?.name || '') }}
</strong>
<div v-else class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #customTags="{ tags: customTags, params }">
<template v-for="tag in customTags" :key="tag.label">
<VnFilterPanelChip
v-for="chip in tag.value"
:key="chip"
removable
@remove="removeTagChip(chip, params, searchFn)"
>
<div class="q-gutter-x-xs">
<strong>{{ chip.tagName }}: </strong>
<span>"{{ chip.value }}"</span>
</div>
</VnFilterPanelChip>
</template>
</template>
<template #body="{ params, searchFn }"> <template #body="{ params, searchFn }">
<QItem class="category-filter q-mt-md">
<QBtn
dense
flat
round
v-for="category in categoryList"
:key="category.name"
:class="['category', getCategoryClass(category, params)]"
:icon="category.icon"
@click="selectCategory(params, category.id, searchFn)"
>
<QTooltip>
{{ t(category.name) }}
</QTooltip>
</QBtn>
</QItem>
<QItem class="q-my-md"> <QItem class="q-my-md">
<QItemSection> <QItemSection>
<VnSelectFilter <VnSelectFilter
:label="t('params.typeFk')" :label="t('components.itemsFilterPanel.salesPersonFk')"
v-model="params.typeFk"
:options="itemTypesOptions"
option-value="id"
option-label="name"
dense
outlined
rounded
use-input
:disable="!selectedCategoryFk"
@update:model-value="
(value) => {
selectedTypeFk = value;
searchFn();
}
"
>
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption>
{{ opt.categoryName }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QItemSection>
</QItem>
<QSeparator />
<QItem class="q-my-md">
<QItemSection>
<VnSelectFilter
:label="t('params.salesPersonFk')"
v-model="params.salesPersonFk" v-model="params.salesPersonFk"
:options="itemTypeWorkersOptions" :options="itemTypeWorkersOptions"
option-value="id" option-value="id"
@ -261,7 +56,7 @@ const removeTag = (index, params, search) => {
<QItem class="q-my-md"> <QItem class="q-my-md">
<QItemSection> <QItemSection>
<VnSelectFilter <VnSelectFilter
:label="t('params.supplier')" :label="t('components.itemsFilterPanel.supplierFk')"
v-model="params.supplierFk" v-model="params.supplierFk"
:options="suppliersOptions" :options="suppliersOptions"
option-value="id" option-value="id"
@ -288,7 +83,7 @@ const removeTag = (index, params, search) => {
<QItem class="q-my-md"> <QItem class="q-my-md">
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
:label="t('params.from')" :label="t('components.itemsFilterPanel.from')"
v-model="params.from" v-model="params.from"
is-outlined is-outlined
@update:model-value="searchFn()" @update:model-value="searchFn()"
@ -298,7 +93,7 @@ const removeTag = (index, params, search) => {
<QItem class="q-my-md"> <QItem class="q-my-md">
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
:label="t('params.to')" :label="t('components.itemsFilterPanel.to')"
v-model="params.to" v-model="params.to"
is-outlined is-outlined
@update:model-value="searchFn()" @update:model-value="searchFn()"
@ -308,7 +103,7 @@ const removeTag = (index, params, search) => {
<QItem> <QItem>
<QItemSection> <QItemSection>
<QCheckbox <QCheckbox
:label="t('params.active')" :label="t('components.itemsFilterPanel.active')"
v-model="params.active" v-model="params.active"
toggle-indeterminate toggle-indeterminate
@update:model-value="searchFn()" @update:model-value="searchFn()"
@ -316,7 +111,7 @@ const removeTag = (index, params, search) => {
</QItemSection> </QItemSection>
<QItemSection> <QItemSection>
<QCheckbox <QCheckbox
:label="t('params.visible')" :label="t('components.itemsFilterPanel.visible')"
v-model="params.visible" v-model="params.visible"
toggle-indeterminate toggle-indeterminate
@update:model-value="searchFn()" @update:model-value="searchFn()"
@ -326,149 +121,13 @@ const removeTag = (index, params, search) => {
<QItem> <QItem>
<QItemSection> <QItemSection>
<QCheckbox <QCheckbox
:label="t('params.floramondo')" :label="t('components.itemsFilterPanel.floramondo')"
v-model="params.floramondo" v-model="params.floramondo"
toggle-indeterminate toggle-indeterminate
@update:model-value="searchFn()" @update:model-value="searchFn()"
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem
v-for="(value, index) in tagValues"
:key="value"
class="q-mt-md filter-value"
>
<QItemSection class="col">
<VnSelectFilter
:label="t('params.tag')"
v-model="value.selectedTag"
:options="tagOptions"
option-label="name"
dense
outlined
rounded
:emit-value="false"
use-input
:is-clearable="false"
@update:model-value="getSelectedTagValues(value)"
/>
</QItemSection>
<QItemSection class="col">
<VnSelectFilter
v-if="!value?.selectedTag?.isFree && value.valueOptions"
:label="t('params.value')"
v-model="value.value"
:options="value.valueOptions || []"
option-value="value"
option-label="value"
dense
outlined
rounded
emit-value
use-input
:disable="!value"
:is-clearable="false"
class="filter-input"
@update:model-value="applyTags(params, searchFn)"
/>
<VnInput
v-else
v-model="value.value"
:label="t('params.value')"
:disable="!value"
is-outlined
class="filter-input"
:is-clearable="false"
@keyup.enter="applyTags(params, searchFn)"
/>
</QItemSection>
<QIcon
name="delete"
class="filter-icon"
@click="removeTag(index, params, searchFn)"
/>
</QItem>
<QItem class="q-mt-lg">
<QIcon
name="add_circle"
class="filter-icon"
@click="tagValues.push({})"
/>
</QItem>
</template> </template>
</VnFilterPanel> </ItemsFilterPanel>
</template> </template>
<style lang="scss" scoped>
.category-filter {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
.category {
padding: 8px;
width: 60px;
height: 60px;
font-size: 1.4rem;
background-color: var(--vn-accent-color);
&.active {
background-color: $primary;
}
}
}
.filter-icon {
font-size: 24px;
color: $primary;
padding: 0 4px;
cursor: pointer;
}
.filter-input {
flex-shrink: 1;
min-width: 0;
}
.filter-value {
display: flex;
align-items: center;
}
</style>
<i18n>
en:
params:
supplier: Supplier
from: From
to: To
active: Is active
visible: Is visible
floramondo: Is floramondo
salesPersonFk: Buyer
categoryFk: Category
typeFk: Type
tag: Tag
value: Value
es:
params:
supplier: Proveedor
from: Desde
to: Hasta
active: Activo
visible: Visible
floramondo: Floramondo
salesPersonFk: Comprador
categoryFk: Categoría
typeFk: Tipo
tag: Etiqueta
value: Valor
Plant: Planta
Flower: Flor
Handmade: Confección
Green: Verde
Accessories: Complemento
Fruit: Fruta
</i18n>

View File

@ -1,3 +1,5 @@
Search entries: Buscar entradas
You can search by entry reference: Puedes buscar por referencia de la entrada
entryList: entryList:
list: list:
inventoryEntry: Es inventario inventoryEntry: Es inventario

View File

@ -1,18 +1,6 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import VnCard from 'components/common/VnCard.vue';
import { useStateStore } from 'stores/useStateStore';
import InvoiceInDescriptor from './InvoiceInDescriptor.vue'; import InvoiceInDescriptor from './InvoiceInDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useArrayData } from 'src/composables/useArrayData';
import { onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const { t } = useI18n();
const route = useRoute();
const filter = { const filter = {
include: [ include: [
@ -21,69 +9,25 @@ const filter = {
scope: { scope: {
include: { include: {
relation: 'contacts', relation: 'contacts',
scope: { scope: { where: { email: { neq: null } } },
where: {
email: { neq: null },
}, },
}, },
}, },
}, { relation: 'invoiceInDueDay' },
}, { relation: 'company' },
{ { relation: 'currency' },
relation: 'invoiceInDueDay',
},
{
relation: 'company',
},
{
relation: 'currency',
},
], ],
}; };
const arrayData = useArrayData('InvoiceIn', {
url: `InvoiceIns/${route.params.id}`,
filter,
});
onMounted(async () => await arrayData.fetch({ append: false }));
watch(
() => route.params.id,
async (newId, oldId) => {
if (newId) {
arrayData.store.url = `InvoiceIns/${newId}`;
await arrayData.fetch({ append: false });
}
}
);
</script> </script>
<template> <template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> <VnCard
<VnSearchbar data-key="InvoiceIn"
data-key="InvoiceInList" base-url="InvoiceIns"
url="InvoiceIns/filter" :filter="filter"
:label="t('Search invoice')" :descriptor="InvoiceInDescriptor"
:info="t('You can search by invoice reference')" searchbar-data-key="InvoiceInList"
searchbar-url="InvoiceIns/filter"
searchbar-label="Search invoice"
searchbar-info="You can search by invoice reference"
/> />
</Teleport>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<InvoiceInDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template> </template>
<i18n>
es:
Search invoice: Buscar factura recibida
You can search by invoice reference: Puedes buscar por referencia de la factura
</i18n>

View File

@ -7,7 +7,6 @@ import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import { useCapitalize } from 'src/composables/useCapitalize';
import VnCurrency from 'src/components/common/VnCurrency.vue'; import VnCurrency from 'src/components/common/VnCurrency.vue';
const { t } = useI18n(); const { t } = useI18n();
@ -71,11 +70,7 @@ const suppliersRef = ref();
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate :label="t('From')" v-model="params.from" is-outlined />
:label="t('From')"
v-model="params.from"
is-outlined
/>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem> <QItem>

View File

@ -0,0 +1,2 @@
Search invoice: Buscar factura recibida
You can search by invoice reference: Puedes buscar por referencia de la factura

View File

@ -1,43 +1,15 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import InvoiceOutDescriptor from './InvoiceOutDescriptor.vue'; import InvoiceOutDescriptor from './InvoiceOutDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue'; import VnCard from 'components/common/VnCard.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const { t } = useI18n();
</script> </script>
<template> <template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> <VnCard
<VnSearchbar data-key="InvoiceOut"
data-key="InvoiceOutList" base-url="InvoiceOuts"
url="InvoiceOuts/filter" :descriptor="InvoiceOutDescriptor"
:label="t('Search invoice')" searchbar-data-key="InvoiceOutList"
:info="t('You can search by invoice reference')" searchbar-url="InvoiceOuts/filter"
searchbar-label="Search invoice"
searchbar-info="You can search by invoice reference"
/> />
</Teleport>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<InvoiceOutDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template> </template>
<i18n>
es:
Search invoice: Buscar factura emitida
You can search by invoice reference: Puedes buscar por referencia de la factura
</i18n>

View File

@ -138,7 +138,7 @@ const ticketsColumns = ref([
<VnTitle :text="t('invoiceOut.summary.taxBreakdown')" /> <VnTitle :text="t('invoiceOut.summary.taxBreakdown')" />
<QTable :columns="taxColumns" :rows="invoiceOut.taxesBreakdown" flat> <QTable :columns="taxColumns" :rows="invoiceOut.taxesBreakdown" flat>
<template #header="props"> <template #header="props">
<QTr :props="props"> <QTr class="tr-header" :props="props">
<QTh v-for="col in props.cols" :key="col.name" :props="props"> <QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }} {{ t(col.label) }}
</QTh> </QTh>
@ -149,6 +149,18 @@ const ticketsColumns = ref([
<QCard class="vn-three"> <QCard class="vn-three">
<VnTitle :text="t('invoiceOut.summary.tickets')" /> <VnTitle :text="t('invoiceOut.summary.tickets')" />
<QTable v-if="tickets" :columns="ticketsColumns" :rows="tickets" flat> <QTable v-if="tickets" :columns="ticketsColumns" :rows="tickets" flat>
<template #header="props">
<QTr :props="props">
<QTh
class="tr-header"
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ t(col.label) }}
</QTh>
</QTr>
</template>
<template #body-cell-item="{ value }"> <template #body-cell-item="{ value }">
<QTd> <QTd>
<QBtn flat color="primary"> <QBtn flat color="primary">
@ -159,7 +171,7 @@ const ticketsColumns = ref([
</template> </template>
<template #body-cell-quantity="{ value, row }"> <template #body-cell-quantity="{ value, row }">
<QTd> <QTd>
<QBtn flat color="primary" dense> <QBtn class="no-uppercase" flat color="primary" dense>
{{ value }} {{ value }}
<CustomerDescriptorProxy :id="row.id" /> <CustomerDescriptorProxy :id="row.id" />
</QBtn> </QBtn>
@ -170,3 +182,8 @@ const ticketsColumns = ref([
</template> </template>
</CardSummary> </CardSummary>
</template> </template>
<style lang="scss" scoped>
.no-uppercase {
text-transform: none;
}
</style>

View File

@ -119,8 +119,8 @@ const columns = computed(() => [
}, },
{ {
label: t('invoiceOut.negativeBases.comercial'), label: t('invoiceOut.negativeBases.comercial'),
field: 'workerSocialName', field: 'workerName',
name: 'comercial', name: 'worker',
align: 'left', align: 'left',
}, },
]); ]);
@ -181,9 +181,11 @@ const downloadCSV = async () => {
<TicketDescriptorProxy :id="row.ticketFk" /> <TicketDescriptorProxy :id="row.ticketFk" />
</QTd> </QTd>
</template> </template>
<template #body-cell-comercial="{ row }"> <template #body-cell-worker="{ row }">
<QTd> <QTd>
<QBtn flat dense color="blue">{{ row.comercialName }}</QBtn> <QBtn class="no-uppercase" flat dense color="blue">{{
row.workerName
}}</QBtn>
<WorkerDescriptorProxy :id="row.comercialId" /> <WorkerDescriptorProxy :id="row.comercialId" />
</QTd> </QTd>
</template> </template>
@ -211,6 +213,9 @@ const downloadCSV = async () => {
border-radius: 4px; border-radius: 4px;
padding: 6px; padding: 6px;
} }
.no-uppercase {
text-transform: none;
}
</style> </style>
<i18n> <i18n>

View File

@ -0,0 +1,2 @@
Search invoice: Buscar factura emitida
You can search by invoice reference: Puedes buscar por referencia de la factura

View File

@ -1,28 +1,7 @@
<script setup> <script setup>
import LeftMenu from 'components/LeftMenu.vue'; import VnCard from 'components/common/VnCard.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import ItemDescriptor from './ItemDescriptor.vue'; import ItemDescriptor from './ItemDescriptor.vue';
import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
</script> </script>
<template> <template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256"> <VnCard data-key="Item" base-url="Items" :descriptor="ItemDescriptor" />
<QScrollArea class="fit">
<ItemDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template> </template>

View File

@ -0,0 +1,605 @@
<script setup>
import { onMounted, ref, reactive, computed, onUnmounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import EditTableCellValueForm from 'src/components/EditTableCellValueForm.vue';
import ItemFixedPriceFilter from './ItemFixedPriceFilter.vue';
import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue';
import { useStateStore } from 'stores/useStateStore';
import { dashIfEmpty } from 'src/filters';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useState } from 'src/composables/useState';
import { toCurrency } from 'filters/index';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
import { useArrayData } from 'composables/useArrayData';
import { isLower, isBigger } from 'src/filters/date.js';
const stateStore = useStateStore();
const { t } = useI18n();
const { openConfirmationModal } = useVnConfirm();
const state = useState();
const { notify } = useNotify();
const editTableCellDialogRef = ref(null);
const user = state.getUser();
const fixedPrices = ref([]);
const fixedPricesOriginalData = ref([]);
const warehousesOptions = ref([]);
const itemsWithNameOptions = ref([]);
const rowsSelected = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'name':
return { 'i.name': { like: `%${value}%` } };
case 'itemFk':
case 'warehouseFk':
case 'rate2':
case 'rate3':
param = `fp.${param}`;
return { [param]: value };
case 'minPrice':
param = `i.${param}`;
return { [param]: value };
}
};
const params = reactive({});
const arrayData = useArrayData('ItemFixedPrices', {
url: 'FixedPrices/filter',
userParams: params,
order: ['itemFk'],
exprBuilder: exprBuilder,
});
const store = arrayData.store;
const fetchFixedPrices = async () => {
await arrayData.fetch({ append: false });
};
const onFixedPricesFetched = (data) => {
fixedPrices.value = data;
// el objetivo de guardar una copia de las rows es evitar guardar cambios si la data no cambió al disparar los eventos
fixedPricesOriginalData.value = JSON.parse(JSON.stringify(data));
};
watch(
() => store.data,
(data) => onFixedPricesFetched(data)
);
const applyColumnFilter = async (col) => {
try {
const paramKey = col.columnFilter?.filterParamKey || col.field;
params[paramKey] = col.columnFilter.filterValue;
await arrayData.addFilter({ params });
} catch (err) {
console.error('Error applying column filter', err);
}
};
const getColumnInputEvents = (col) => {
return col.columnFilter.type === 'select'
? { 'update:modelValue': () => applyColumnFilter(col) }
: {
'keyup.enter': () => applyColumnFilter(col),
};
};
const defaultColumnFilter = {
component: VnInput,
type: 'text',
filterValue: null,
event: getColumnInputEvents,
attrs: {
dense: true,
},
};
const defaultColumnAttrs = {
align: 'left',
sortable: true,
};
const columns = computed(() => [
{
label: t('item.fixedPrice.itemId'),
name: 'itemId',
field: 'itemFk',
...defaultColumnAttrs,
columnFilter: {
...defaultColumnFilter,
},
},
{
label: t('globals.description'),
field: 'name',
name: 'description',
...defaultColumnAttrs,
columnFilter: {
...defaultColumnFilter,
},
},
{
label: t('item.fixedPrice.groupingPrice'),
field: 'rate2',
name: 'groupingPrice',
...defaultColumnAttrs,
columnFilter: {
...defaultColumnFilter,
},
format: (val) => toCurrency(val),
},
{
label: t('item.fixedPrice.packingPrice'),
field: 'rate3',
name: 'packingPrice',
...defaultColumnAttrs,
columnFilter: {
...defaultColumnFilter,
},
format: (val) => dashIfEmpty(val),
},
{
label: t('item.fixedPrice.minPrice'),
field: 'minPrice',
name: 'minPrice',
...defaultColumnAttrs,
columnFilter: {
...defaultColumnFilter,
},
},
{
label: t('item.fixedPrice.started'),
field: 'started',
name: 'started',
...defaultColumnAttrs,
columnFilter: null,
},
{
label: t('item.fixedPrice.ended'),
field: 'ended',
name: 'ended',
...defaultColumnAttrs,
columnFilter: null,
},
{
label: t('item.fixedPrice.warehouse'),
field: 'warehouseFk',
name: 'warehouse',
...defaultColumnAttrs,
columnFilter: {
component: VnSelectFilter,
type: 'select',
filterValue: null,
event: getColumnInputEvents,
attrs: {
options: warehousesOptions.value,
'option-value': 'id',
'option-label': 'name',
dense: true,
},
},
},
{ name: 'deleteAction', align: 'center' },
]);
const editTableFieldsOptions = [
{
field: 'rate2',
label: t('item.fixedPrice.groupingPrice'),
component: 'input',
attrs: {
type: 'number',
},
},
{
field: 'rate3',
label: t('item.fixedPrice.packingPrice'),
component: 'input',
attrs: {
type: 'number',
},
},
{
field: 'minPrice',
label: t('item.fixedPrice.minPrice'),
component: 'input',
attrs: {
type: 'number',
},
},
{
field: 'hasMinPrice',
label: t('item.fixedPrice.hasMinPrice'),
component: 'checkbox',
attrs: {
'false-value': 0,
'true-value': 1,
},
},
{
field: 'started',
label: t('item.fixedPrice.started'),
component: 'date',
},
{
field: 'ended',
label: t('item.fixedPrice.ended'),
component: 'date',
},
{
field: 'warehouseFk',
label: t('item.fixedPrice.warehouse'),
component: 'select',
attrs: {
options: [],
'option-label': 'name',
'option-value': 'id',
},
},
];
const getRowUpdateInputEvents = (props, resetMinPrice, inputType = 'text') => {
return inputType === 'text'
? {
'keyup.enter': () => upsertPrice(props, resetMinPrice),
blur: () => upsertPrice(props, resetMinPrice),
}
: { 'update:modelValue': () => upsertPrice(props, resetMinPrice) };
};
const validations = (row, rowIndex, col) => {
const isNew = !row.id;
// Si la row no tiene id significa que fue agregada con addRow y no se ha guardado en la base de datos
// Si isNew es falso no se checkea si el valor es igual a la original
if (!isNew)
if (fixedPricesOriginalData.value[rowIndex][col.field] == row[col.field])
return false;
const requiredFields = ['itemFk', 'started', 'ended', 'rate2', 'rate3'];
return requiredFields.every(
(field) => row[field] !== null && row[field] !== undefined
);
};
const upsertPrice = async ({ row, col, rowIndex }, resetMinPrice = false) => {
if (!validations(row, rowIndex, col)) return;
try {
if (resetMinPrice) row.hasMinPrice = 0;
const { data } = await axios.patch('FixedPrices/upsertFixedPrice', row);
row = data;
fixedPricesOriginalData.value[rowIndex][col.field] = row[col.field];
} catch (err) {
console.error('Error editing price', err);
}
};
const addRow = () => {
if (!fixedPrices.value || fixedPrices.value.length === 0) {
fixedPrices.value = [];
const today = Date.vnNew();
const millisecsInDay = 86400000;
const daysInWeek = 7;
const nextWeek = new Date(today.getTime() + daysInWeek * millisecsInDay);
const newPrice = {
started: today,
ended: nextWeek,
hasMinPrice: 0,
};
fixedPricesOriginalData.value.push({ ...newPrice });
fixedPrices.value.push({ ...newPrice });
return;
}
const lastItemCopy = JSON.parse(
JSON.stringify(fixedPrices.value[fixedPrices.value.length - 1])
);
const { id, ...restOfItem } = lastItemCopy;
fixedPricesOriginalData.value.push(restOfItem);
fixedPrices.value.push(restOfItem);
};
const openEditTableCellDialog = () => {
editTableCellDialogRef.value.show();
};
const onEditCellDataSaved = async () => {
rowsSelected.value = [];
await fetchFixedPrices();
};
const onWarehousesFetched = (data) => {
warehousesOptions.value = data;
// Actualiza las 'options' del elemento con field 'warehouseFk' en 'editTableFieldsOptions'.
const warehouseField = editTableFieldsOptions.find(
(field) => field.field === 'warehouseFk'
);
warehouseField.attrs.options = data;
};
const removePrice = async (id, rowIndex) => {
try {
await axios.delete(`FixedPrices/${id}`);
fixedPrices.value.splice(rowIndex, 1);
fixedPricesOriginalData.value.splice(rowIndex, 1);
notify(t('globals.dataSaved'), 'positive');
} catch (err) {
console.error('Error removing price', err);
}
};
const updateMinPrice = async (value, props) => {
// El checkbox hasMinPrice se encuentra en la misma columna que el input hasMinPrice
// Por lo tanto le mandamos otro objeto con las mismas propiedades pero con el campo 'field' cambiado
props.row.hasMinPrice = value;
await upsertPrice({
row: props.row,
col: { field: 'hasMinPrice' },
rowIndex: props.rowIndex,
});
};
onMounted(async () => {
stateStore.rightDrawer = true;
params.warehouseFk = user.value.warehouseFk;
await fetchFixedPrices();
});
onUnmounted(() => (stateStore.rightDrawer = false));
</script>
<template>
<FetchData
url="Warehouses"
:filter="{ order: ['name'] }"
auto-load
@on-fetch="(data) => onWarehousesFetched(data)"
/>
<FetchData
url="Items/withName"
:filter="{ fields: ['id', 'name'], order: 'id DESC' }"
auto-load
@on-fetch="(data) => (itemsWithNameOptions = data)"
/>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<ItemFixedPriceFilter
data-key="ItemFixedPrices"
:warehouses-options="warehousesOptions"
/>
</QScrollArea>
</QDrawer>
<QPage class="column items-center q-pa-md">
<QTable
:rows="fixedPrices"
:columns="columns"
row-key="id"
selection="multiple"
v-model:selected="rowsSelected"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
>
<template #top-row="{ cols }">
<QTr>
<QTd />
<QTd
v-for="(col, index) in cols"
:key="index"
style="max-width: 100px"
>
<component
:is="col.columnFilter.component"
v-if="col.columnFilter"
v-model="col.columnFilter.filterValue"
v-bind="col.columnFilter.attrs"
v-on="col.columnFilter.event(col)"
dense
/>
</QTd>
</QTr>
</template>
<template #body-cell-itemId="props">
<QTd>
<VnSelectFilter
:options="itemsWithNameOptions"
hide-selected
option-label="id"
option-value="id"
v-model="props.row.itemFk"
v-on="getRowUpdateInputEvents(props, true, 'select')"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
<QItemLabel caption>{{ scope.opt?.name }}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QTd>
</template>
<template #body-cell-description="{ row }">
<QTd class="col">
<span class="link">
{{ row.name }}
</span>
<ItemDescriptorProxy :id="row.itemFk" />
<fetched-tags :item="row" :max-length="6" />
</QTd>
</template>
<template #body-cell-groupingPrice="props">
<QTd class="col">
<VnInput
v-model.number="props.row.rate2"
v-on="getRowUpdateInputEvents(props)"
>
<template #append></template>
</VnInput>
</QTd>
</template>
<template #body-cell-packingPrice="props">
<QTd class="col">
<VnInput
v-model.number="props.row.rate3"
v-on="getRowUpdateInputEvents(props)"
>
<template #append></template>
</VnInput>
</QTd>
</template>
<template #body-cell-minPrice="props">
<QTd class="col">
<div class="row">
<QCheckbox
class="col"
:model-value="props.row.hasMinPrice"
@update:model-value="updateMinPrice($event, props)"
:false-value="0"
:true-value="1"
:toggle-indeterminate="false"
/>
<VnInput
class="col"
:disable="!props.row.hasMinPrice"
v-model.number="props.row.minPrice"
v-on="getRowUpdateInputEvents(props)"
type="number"
/>
</div>
</QTd>
</template>
<template #body-cell-started="props">
<QTd class="col" style="min-width: 160px">
<VnInputDate
v-model="props.row.started"
v-on="getRowUpdateInputEvents(props, false, 'date')"
v-bind="
isBigger(props.row.started)
? { 'bg-color': 'warning', 'is-outlined': true }
: {}
"
/>
</QTd>
</template>
<template #body-cell-ended="props">
<QTd class="col" style="min-width: 150px">
<VnInputDate
v-model="props.row.ended"
v-on="getRowUpdateInputEvents(props, false, 'date')"
v-bind="
isLower(props.row.ended)
? { 'bg-color': 'warning', 'is-outlined': true }
: {}
"
/>
</QTd>
</template>
<template #body-cell-warehouse="props">
<QTd class="col">
<VnSelectFilter
:options="warehousesOptions"
hide-selected
option-label="name"
option-value="id"
v-model="props.row.warehouseFk"
v-on="getRowUpdateInputEvents(props, false, 'select')"
/>
</QTd>
</template>
<template #body-cell-deleteAction="{ row, rowIndex }">
<QTd class="col">
<QIcon
name="delete"
size="sm"
class="cursor-pointer fill-icon-on-hover"
color="primary"
@click.stop="
openConfirmationModal(
t('This row will be removed'),
t('Do you want to clone this item?'),
() => removePrice(row.id, rowIndex)
)
"
>
<QTooltip class="text-no-wrap">
{{ t('Delete') }}
</QTooltip>
</QIcon>
</QTd>
</template>
<template #bottom-row>
<QTd align="center">
<QIcon
@click.stop="addRow()"
class="fill-icon-on-hover"
color="primary"
name="add_circle"
size="sm"
>
<QTooltip>
{{ t('Add fixed price') }}
</QTooltip>
</QIcon>
</QTd>
</template>
</QTable>
<QPageSticky v-if="rowsSelected.length > 0" :offset="[20, 20]">
<QBtn @click="openEditTableCellDialog()" color="primary" fab icon="edit" />
<QTooltip>
{{ t('Edit fixed price(s)') }}
</QTooltip>
</QPageSticky>
<QDialog ref="editTableCellDialogRef">
<EditTableCellValueForm
edit-url="FixedPrices/editFixedPrice"
:rows="rowsSelected"
:fields-options="editTableFieldsOptions"
@on-data-saved="onEditCellDataSaved()"
/>
</QDialog>
</QPage>
</template>
<i18n>
es:
Add fixed price: Añadir precio fijado
Edit fixed price(s): Editar precio(s) fijado(s)
This row will be removed: Esta linea se eliminará
Are you sure you want to continue?: ¿Seguro que quieres continuar?
Delete: Eliminar
</i18n>

View File

@ -0,0 +1,108 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import ItemsFilterPanel from 'src/components/ItemsFilterPanel.vue';
const { t } = useI18n();
defineProps({
dataKey: {
type: String,
required: true,
},
warehousesOptions: {
type: Array,
default: () => [],
},
});
const itemTypeWorkersOptions = ref([]);
</script>
<template>
<FetchData
url="TicketRequests/getItemTypeWorker"
limit="30"
auto-load
:filter="{ fields: ['id', 'nickname'], order: 'nickname ASC', limit: 30 }"
@on-fetch="(data) => (itemTypeWorkersOptions = data)"
/>
<ItemsFilterPanel :data-key="dataKey" :custom-tags="['tags']">
<template #body="{ params, searchFn }">
<QItem class="q-my-md">
<QItemSection>
<VnSelectFilter
:label="t('components.itemsFilterPanel.buyerFk')"
v-model="params.buyerFk"
:options="itemTypeWorkersOptions"
option-value="id"
option-label="nickname"
dense
outlined
rounded
use-input
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem class="q-my-md">
<QItemSection>
<VnSelectFilter
:label="t('components.itemsFilterPanel.warehouseFk')"
v-model="params.warehouseFk"
:options="warehousesOptions"
option-label="name"
option-value="id"
dense
outlined
rounded
use-input
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem class="q-my-md">
<QItemSection>
<VnInputDate
:label="t('components.itemsFilterPanel.started')"
v-model="params.started"
is-outlined
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem class="q-my-md">
<QItemSection>
<VnInputDate
:label="t('components.itemsFilterPanel.ended')"
v-model="params.ended"
is-outlined
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
:label="t('components.itemsFilterPanel.mine')"
v-model="params.mine"
toggle-indeterminate
@update:model-value="searchFn()"
/>
</QItemSection>
<QItemSection>
<QCheckbox
:label="t('components.itemsFilterPanel.hasMinPrice')"
v-model="params.hasMinPrice"
toggle-indeterminate
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
</template>
</ItemsFilterPanel>
</template>

View File

@ -1,23 +1,7 @@
<script setup> <script setup>
import LeftMenu from 'components/LeftMenu.vue'; import VnCard from 'components/common/VnCard.vue';
import { useStateStore } from 'stores/useStateStore';
import OrderDescriptor from 'pages/Order/Card/OrderDescriptor.vue'; import OrderDescriptor from 'pages/Order/Card/OrderDescriptor.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
const stateStore = useStateStore();
</script> </script>
<template> <template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256"> <VnCard data-key="Order" base-url="Orders" :descriptor="OrderDescriptor" />
<QScrollArea class="fit">
<OrderDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<VnSubToolbar />
<RouterView></RouterView>
</QPage>
</QPageContainer>
</template> </template>

View File

@ -4,7 +4,6 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue'; import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
@ -28,7 +27,6 @@ const filter = {
@on-fetch="(data) => (sectors = data)" @on-fetch="(data) => (sectors = data)"
auto-load auto-load
/> />
<VnSubToolbar />
<FormModel :url="`Parkings/${parkingId}`" model="parking" :filter="filter" auto-load> <FormModel :url="`Parkings/${parkingId}`" model="parking" :filter="filter" auto-load>
<template #form="{ data }"> <template #form="{ data }">
<VnRow> <VnRow>

View File

@ -1,57 +1,21 @@
<script setup> <script setup>
import { onMounted, watch } from 'vue'; import VnCard from 'components/common/VnCard.vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useArrayData } from 'src/composables/useArrayData';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import ParkingDescriptor from 'pages/Parking/Card/ParkingDescriptor.vue'; import ParkingDescriptor from 'pages/Parking/Card/ParkingDescriptor.vue';
const { t } = useI18n();
const route = useRoute();
const stateStore = useStateStore();
const filter = { const filter = {
fields: ['id', 'sectorFk', 'code', 'pickingOrder', 'row', 'column'], fields: ['id', 'sectorFk', 'code', 'pickingOrder', 'row', 'column'],
include: [{ relation: 'sector', scope: { fields: ['id', 'description'] } }], include: [{ relation: 'sector', scope: { fields: ['id', 'description'] } }],
}; };
const arrayData = useArrayData('Parking', {
url: `Parkings/${route.params.id}`,
filter,
});
const { store } = arrayData;
onMounted(async () => await arrayData.fetch({ append: false }));
watch(
() => route.params.id,
async (newId) => {
if (newId) {
store.url = `Parkings/${newId}`;
store.filter = filter;
await arrayData.fetch({ append: false });
}
}
);
</script> </script>
<template> <template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> <VnCard
<VnSearchbar
:info="t('parking.searchBar.info')"
:label="t('parking.searchBar.label')"
data-key="Parking" data-key="Parking"
base-url="Parkings"
:filter="filter"
:descriptor="ParkingDescriptor"
searchbar-data-key="ParkingList"
searchbar-url="Parkings"
searchbar-label="parking.searchBar.label"
searchbar-info="parking.searchBar.info"
/> />
</Teleport>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<ParkingDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<RouterView></RouterView>
</QPage>
</QPageContainer>
</template> </template>

View File

@ -11,9 +11,9 @@ const $props = defineProps({
default: 0, default: 0,
}, },
}); });
const { params } = useRoute(); const router = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const entityId = computed(() => $props.id || params.id); const entityId = computed(() => $props.id || router.params.id);
const filter = { const filter = {
fields: ['id', 'sectorFk', 'code', 'pickingOrder', 'row', 'column'], fields: ['id', 'sectorFk', 'code', 'pickingOrder', 'row', 'column'],

View File

@ -1,22 +1,7 @@
<script setup> <script setup>
import { useStateStore } from 'stores/useStateStore'; import VnCard from 'components/common/VnCard.vue';
import LeftMenu from 'components/LeftMenu.vue';
import RouteDescriptor from 'pages/Route/Card/RouteDescriptor.vue'; import RouteDescriptor from 'pages/Route/Card/RouteDescriptor.vue';
// import ShelvingDescriptor from 'pages/Shelving/Card/ShelvingDescriptor.vue';
const stateStore = useStateStore();
</script> </script>
<template> <template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256"> <VnCard data-key="Route" base-url="Routes" :descriptor="RouteDescriptor" />
<QScrollArea class="fit">
<RouteDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<RouterView></RouterView>
</QPage>
</QPageContainer>
</template> </template>

View File

@ -1,21 +1,7 @@
<script setup> <script setup>
import { useStateStore } from 'stores/useStateStore'; import VnCard from 'components/common/VnCard.vue';
import LeftMenu from 'components/LeftMenu.vue';
import ShelvingDescriptor from 'pages/Shelving/Card/ShelvingDescriptor.vue'; import ShelvingDescriptor from 'pages/Shelving/Card/ShelvingDescriptor.vue';
const stateStore = useStateStore();
</script> </script>
<template> <template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256"> <VnCard data-key="Shelving" base-url="Shelvings" :descriptor="ShelvingDescriptor" />
<QScrollArea class="fit">
<ShelvingDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<RouterView></RouterView>
</QPage>
</QPageContainer>
</template> </template>

View File

@ -1,73 +1,14 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import VnCard from 'components/common/VnCard.vue';
import { useStateStore } from 'stores/useStateStore';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import LeftMenu from 'components/LeftMenu.vue';
import SupplierDescriptor from './SupplierDescriptor.vue'; import SupplierDescriptor from './SupplierDescriptor.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const { t } = useI18n();
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()"> <VnCard
<Teleport to="#searchbar"> data-key="Supplier"
<VnSearchbar base-url="Suppliers"
data-key="SuppliersList" :descriptor="SupplierDescriptor"
url="Suppliers/filter" searchbar-data-key="SupplierList"
:limit="20" searchbar-url="Suppliers/filter"
:label="t('Search suppliers')" searchbar-label="Search suppliers"
/> />
</Teleport>
</template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<SupplierDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template> </template>
<style lang="scss">
.q-scrollarea__content {
max-width: 100%;
}
</style>
<style lang="scss" scoped>
.descriptor {
max-width: 256px;
h5 {
margin: 0 15px;
}
.header {
display: flex;
justify-content: space-between;
}
.q-card__actions {
justify-content: center;
}
#descriptor-skeleton .q-card__actions {
justify-content: space-between;
}
}
</style>
<i18n>
es:
Search suppliers: Buscar proveedores
</i18n>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { useRoute } from 'vue-router';
import { computed, onMounted } from 'vue'; import { computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
@ -33,20 +33,8 @@ const arrayData = useArrayData('SupplierConsumption', {
const store = arrayData.store; const store = arrayData.store;
const dateRanges = computed(() => { const dateRanges = computed(() => {
const ranges = { const { from, to } = arrayData.store?.userParams || {};
from: null, return { from, to };
to: null,
};
if (route.query && route.query.params) {
const params = JSON.parse(route.query.params);
if (params.from && params.to) {
ranges.from = params.from;
ranges.to = params.to;
}
}
return ranges;
}); });
const reportParams = computed(() => ({ const reportParams = computed(() => ({
@ -117,7 +105,7 @@ onMounted(async () => {
<template> <template>
<Teleport to="#st-actions" v-if="stateStore.isSubToolbarShown()"> <Teleport to="#st-actions" v-if="stateStore.isSubToolbarShown()">
<QBtn <QBtn
:disabled="!dateRanges.from && !dateRanges.to" :disabled="!dateRanges.from || !dateRanges.to || !rows.length"
color="primary" color="primary"
icon-right="picture_as_pdf" icon-right="picture_as_pdf"
no-caps no-caps
@ -129,7 +117,7 @@ onMounted(async () => {
</QTooltip> </QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
:disabled="!dateRanges.from && !dateRanges.to" :disabled="!dateRanges.from || !dateRanges.to || !rows.length"
color="primary" color="primary"
icon-right="email" icon-right="email"
no-caps no-caps
@ -140,7 +128,6 @@ onMounted(async () => {
</QTooltip> </QTooltip>
</QBtn> </QBtn>
</Teleport> </Teleport>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8"> <QScrollArea class="fit text-grey-8">

View File

@ -0,0 +1 @@
Search suppliers: Buscar proveedores

View File

@ -1,73 +1,15 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import VnCard from 'components/common/VnCard.vue';
import { useStateStore } from 'stores/useStateStore';
import TicketDescriptor from './TicketDescriptor.vue'; import TicketDescriptor from './TicketDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const { t } = useI18n();
</script> </script>
<template> <template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> <VnCard
<VnSearchbar data-key="Ticket"
data-key="TicketList" base-url="Tickets"
url="Tickets/filter" :descriptor="TicketDescriptor"
:label="t('Search ticket')" searchbar-data-key="TicketList"
:info="t('You can search by ticket id or alias')" searchbar-url="Tickets/filter"
searchbar-label="Search ticket"
searchbar-info="You can search by ticket id or alias"
/> />
</Teleport>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<TicketDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template> </template>
<style lang="scss">
.q-scrollarea__content {
max-width: 100%;
}
</style>
<style lang="scss" scoped>
.descriptor {
max-width: 256px;
h5 {
margin: 0 15px;
}
.header {
display: flex;
justify-content: space-between;
}
.q-card__actions {
justify-content: center;
}
#descriptor-skeleton .q-card__actions {
justify-content: space-between;
}
}
</style>
<i18n>
es:
Search ticket: Buscar ticket
You can search by ticket id or alias: Puedes buscar por id o alias del ticket
</i18n>

View File

@ -91,8 +91,8 @@ async function changeState(value) {
> >
<template #header="{ entity }"> <template #header="{ entity }">
<div> <div>
Ticket #{{ entity.id }} - {{ entity.client.name }} ({{ Ticket #{{ entity.id }} - {{ entity.client?.name }} ({{
entity.client.id entity.client?.id
}}) - }}) -
{{ entity.nickname }} {{ entity.nickname }}
</div> </div>

View File

@ -0,0 +1,2 @@
Search ticket: Buscar ticket
You can search by ticket id or alias: Puedes buscar por id o alias del ticket

View File

@ -1,55 +1,7 @@
<script setup> <script setup>
import { useStateStore } from 'stores/useStateStore'; import VnCard from 'components/common/VnCard.vue';
import TravelDescriptor from './TravelDescriptor.vue'; import TravelDescriptor from './TravelDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
</script> </script>
<template> <template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256"> <VnCard data-key="Travel" base-url="Travels" :descriptor="TravelDescriptor" />
<QScrollArea class="fit">
<TravelDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template> </template>
<style lang="scss">
.q-scrollarea__content {
max-width: 100%;
}
</style>
<style lang="scss" scoped>
.descriptor {
max-width: 256px;
h5 {
margin: 0 15px;
}
.header {
display: flex;
justify-content: space-between;
}
.q-card__actions {
justify-content: center;
}
#descriptor-skeleton .q-card__actions {
justify-content: space-between;
}
}
</style>

View File

@ -1,32 +1,6 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import VnCard from 'components/common/VnCard.vue';
import { useStateStore } from 'stores/useStateStore';
import { useRoute } from 'vue-router';
import LeftMenu from 'components/LeftMenu.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore();
const route = useRoute();
const { t } = useI18n();
</script> </script>
<template> <template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256"> <VnCard data-key="Wagon" base-url="Wagons" />
<QScrollArea class="fit">
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template> </template>
<i18n>
es:
Search customer: Buscar cliente
You can search by customer id or name: Puedes buscar por id o nombre del cliente
</i18n>

View File

@ -178,6 +178,7 @@ watch(_year, (newValue) => {
class="outline" class="outline"
bordered bordered
transparent-background transparent-background
view-customization="workerCalendar"
> >
<template #header> <template #header>
<span class="full-width text-center text-body1 q-py-sm">{{ <span class="full-width text-center text-body1 q-py-sm">{{
@ -222,58 +223,6 @@ watch(_year, (newValue) => {
</template> </template>
<style lang="scss"> <style lang="scss">
@import '../../../css/quasar.variables.scss';
:root {
// Cambia los colores del día actual del calendario por los de salix
--calendar-border-current-dark: #84d0e2 2px solid;
--calendar-border-current: #84d0e2 2px solid;
--calendar-current-color-dark: #ec8916;
}
.q-calendar__button {
width: 32px;
height: 32px;
font-size: 13px;
&:hover {
background-color: var(--vn-accent-color);
cursor: pointer;
}
}
.q-calendar-month__week--days > div:nth-child(6),
.q-calendar-month__week--days > div:nth-child(7) {
// Cambia el color de los días sábado y domingo
color: #777777;
}
.q-calendar-month__week--wrapper {
margin-bottom: 4px;
}
.q-calendar-month__workweek {
height: 32px;
display: flex;
justify-content: center;
}
.q-calendar__button--bordered {
color: $info !important;
}
.q-calendar-month__day--content {
position: absolute;
top: 1;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
.q-outside .calendar-event {
display: none;
}
.calendar-event { .calendar-event {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -296,15 +245,6 @@ watch(_year, (newValue) => {
opacity: 0.8; opacity: 0.8;
} }
} }
.q-calendar-month__workweek,
.q-calendar-month__head--workweek,
.q-calendar-month__head--weekday.q-calendar__center.q-calendar__ellipsis {
text-transform: capitalize;
color: #777;
font-weight: bold;
font-size: 0.8rem;
text-align: center;
}
</style> </style>
<i18n> <i18n>

View File

@ -1,44 +1,18 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import VnCard from 'components/common/VnCard.vue';
import { useStateStore } from 'stores/useStateStore';
import WorkerDescriptor from './WorkerDescriptor.vue'; import WorkerDescriptor from './WorkerDescriptor.vue';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import useCardSize from 'src/composables/useCardSize';
const stateStore = useStateStore(); const filter = { where: {} };
const { t } = useI18n();
</script> </script>
<template> <template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> <VnCard
<VnSearchbar data-key="Worker"
data-key="WorkerList" custom-url="Workers/Summary"
url="Workers/filter" :descriptor="WorkerDescriptor"
:label="t('Search worker')" :filter="filter"
:info="t('You can search by worker id or name')" searchbar-data-key="WorkerList"
searchbar-url="Workers/filter"
searchbar-label="Search worker"
searchbar-info="You can search by worker id or name"
/> />
</Teleport>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<WorkerDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<VnSubToolbar />
<div :class="useCardSize()">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template> </template>
<i18n>
es:
Search worker: Buscar trabajador
You can search by worker id or name: Puedes buscar por id o nombre del trabajador
</i18n>

View File

@ -0,0 +1,33 @@
<script setup>
defineProps({
color: {
type: String,
default: null,
},
avatarClass: {
type: String,
default: null,
},
selected: {
type: Boolean,
default: undefined,
},
});
defineEmits(['update:selected']);
</script>
<template>
<QChip
class="text-white q-ma-none"
:selected="selected"
:style="{ backgroundColor: selected ? color : 'black' }"
@update:selected="$emit('update:selected', $event)"
>
<QAvatar
:color="color"
:class="avatarClass"
:style="{ backgroundColor: color }"
/>
<slot />
</QChip>
</template>

View File

@ -31,7 +31,9 @@ const entityId = computed(() => {
}); });
const worker = ref(); const worker = ref();
const filter = { where: { id: route.params.id}}; const filter = computed(() => {
return { where: { id: entityId.value } };
});
const sip = ref(null); const sip = ref(null);
@ -60,7 +62,7 @@ const setData = (entity) => {
<CardDescriptor <CardDescriptor
module="Worker" module="Worker"
data-key="workerData" data-key="workerData"
:url="`Workers/summary`" url="Workers/summary"
:filter="filter" :filter="filter"
:title="data.title" :title="data.title"
:subtitle="data.subtitle" :subtitle="data.subtitle"

View File

@ -10,7 +10,7 @@ import CardSummary from 'components/ui/CardSummary.vue';
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';
const { params } = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const $props = defineProps({ const $props = defineProps({
@ -20,18 +20,25 @@ const $props = defineProps({
}, },
}); });
const entityId = computed(() => $props.id || params.id); const entityId = computed(() => $props.id || route.params.id);
const workerUrl = ref(); const workerUrl = ref();
onMounted(async () => { onMounted(async () => {
workerUrl.value = (await getUrl('')) + `worker/${entityId.value}/`; workerUrl.value = (await getUrl('')) + `worker/${entityId.value}/`;
}); });
const filter = { where: { id: entityId.value } }; const filter = computed(() => {
return { where: { id: entityId.value } };
});
</script> </script>
<template> <template>
<CardSummary ref="summary" :url="`Workers/summary`" :filter="filter"> <CardSummary
data-key="workerData"
ref="summary"
:url="`Workers/summary`"
:filter="filter"
>
<template #header="{ entity }"> <template #header="{ entity }">
<div>{{ entity.id }} - {{ entity.firstName }} {{ entity.lastName }}</div> <div>{{ entity.id }} - {{ entity.firstName }} {{ entity.lastName }}</div>
</template> </template>
@ -41,7 +48,7 @@ const filter = { where: { id: entityId.value } };
:url="workerUrl + `basic-data`" :url="workerUrl + `basic-data`"
:text="t('worker.summary.basicData')" :text="t('worker.summary.basicData')"
/> />
<VnLv :label="t('worker.card.name')" :value="worker.user.nickname" /> <VnLv :label="t('worker.card.name')" :value="worker.user?.nickname" />
<VnLv <VnLv
:label="t('worker.list.department')" :label="t('worker.list.department')"
:value="worker.department?.department?.name" :value="worker.department?.department?.name"

View File

@ -0,0 +1,659 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { onMounted, ref, computed, onBeforeMount, nextTick, reactive } from 'vue';
import FetchData from 'components/FetchData.vue';
import WorkerTimeHourChip from 'pages/Worker/Card/WorkerTimeHourChip.vue';
import WorkerTimeForm from 'pages/Worker/Card/WorkerTimeForm.vue';
import WorkerTimeReasonForm from 'pages/Worker/Card/WorkerTimeReasonForm.vue';
import WorkerDateLabel from './WorkerDateLabel.vue';
import WorkerTimeControlCalendar from 'pages/Worker/Card/WorkerTimeControlCalendar.vue';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
import { useRole } from 'src/composables/useRole';
import { useWeekdayStore } from 'src/stores/useWeekdayStore';
import { useStateStore } from 'stores/useStateStore';
import { useState } from 'src/composables/useState';
import { dashIfEmpty } from 'src/filters';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useArrayData } from 'composables/useArrayData';
import { toTimeFormat, secondsToHoursMinutes } from 'filters/date.js';
import toDateString from 'filters/toDateString.js';
import { date } from 'quasar';
const route = useRoute();
const { t, locale } = useI18n();
const { notify } = useNotify();
const { hasAny } = useRole();
const _state = useState();
const user = _state.getUser();
const stateStore = useStateStore();
const weekdayStore = useWeekdayStore();
const weekDays = ref([]);
const { openConfirmationModal } = useVnConfirm();
const { getWeekOfYear } = date;
const workerTimeFormDialogRef = ref(null);
const workerTimeReasonFormDialogRef = ref(null);
const workerHoursRef = ref(null);
const selectedDate = ref(null);
const startOfWeek = ref(null);
const endOfWeek = ref(null);
const selectedWeekNumber = ref(null);
const state = ref(null);
const reason = ref(null);
const canResend = ref(null);
const weekTotalHours = ref(null);
const workerTimeControlMails = ref([]);
const workerTimeFormProps = reactive({
dated: null,
entryId: null,
entryCode: null,
});
// Array utilizado por QCalendar para seleccionar un rango de fechas
const selectedCalendarDates = ref([]);
// Date formateada para bindear al componente QDate
const selectedDateFormatted = ref(toDateString(Date.vnNew()));
const arrayData = useArrayData('workerData');
const worker = computed(() => arrayData.store?.data);
const isHr = computed(() => hasAny(['hr']));
const isHimSelf = computed(() => user.value.id === Number(route.params.id));
const columns = computed(() => {
return weekdayStore.getLocales?.map((day, index) => {
const obj = {
label: day.locale,
formattedDate: getHeaderFormattedDate(weekDays.value[index]?.dated),
name: day.name,
align: 'center',
colIndex: index,
dayData: weekDays.value[index],
};
return obj;
});
});
const getHeaderFormattedDate = (date) => {
const newDate = new Date(date);
const day = String(newDate.getDate()).padStart(2, '0');
const monthName = newDate.toLocaleString(locale.value, { month: 'long' });
return `${day} ${monthName}`;
};
const formattedWeekTotalHours = computed(() =>
secondsToHoursMinutes(weekTotalHours.value)
);
const onInputChange = async (date) => {
if (!date) return;
const { year, month, day } = date.scope.timestamp;
const _date = new Date(year, month - 1, day);
setDate(_date);
};
const setDate = async (date) => {
if (!date) return;
selectedDate.value = date;
selectedDate.value.setHours(0, 0, 0, 0);
const newStartOfWeek = getStartOfWeek(selectedDate.value); // Obtener el día de inicio de la semana a partir de la fecha seleccionada
const newEndOfWeek = getEndOfWeek(newStartOfWeek); // Obtener el fin de la semana a partir de la fecha de inicio de la semana
startOfWeek.value = newStartOfWeek;
endOfWeek.value = newEndOfWeek;
selectedWeekNumber.value = getWeekOfYear(newStartOfWeek); // Asignar el número de la semana del año
getWeekDates(newStartOfWeek, newEndOfWeek);
await nextTick(); // Esperar actualización del DOM y luego fetchear data necesaria
await fetchHours();
await fetchWeekData();
};
// Función para obtener el inicio de la semana a partir de una fecha
const getStartOfWeek = (selectedDate) => {
const dayOfWeek = selectedDate.getDay(); // Obtener el día de la semana de la fecha seleccionada
const startOfWeek = new Date(selectedDate);
startOfWeek.setDate(selectedDate.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1)); // Calcular el inicio de la semana
return startOfWeek;
};
// Función para obtener el fin de la semana a partir del inicio de la semana
const getEndOfWeek = (startOfWeek) => {
const endOfWeek = new Date(startOfWeek);
endOfWeek.setHours(23, 59, 59, 59);
endOfWeek.setDate(startOfWeek.getDate() + 6); // Calcular el fin de la semana sumando 6 días al inicio de la semana
return endOfWeek;
};
// Función para obtener las fechas de la semana seleccionada
const getWeekDates = (startOfWeek, endOfWeek) => {
selectedCalendarDates.value = [];
weekDays.value = []; // Limpiar la información de las fechas seleccionadas previamente
let currentDate = new Date(startOfWeek);
while (currentDate <= endOfWeek) {
// Iterar sobre los días de la semana
selectedCalendarDates.value.push(toDateString(currentDate)); // Agregar fecha formateada para el array de fechas bindeado al componente QCalendar
weekDays.value.push({ dated: new Date(currentDate.getTime()) }); // Agregar el día de la semana al array información de días de la semana
currentDate = new Date(currentDate.setDate(currentDate.getDate() + 1)); // Avanzar al siguiente día
}
};
const workerHoursFilter = computed(() => ({
where: {
and: [{ timed: { gte: startOfWeek.value } }, { timed: { lte: endOfWeek.value } }],
},
}));
const getWorkedHours = async (from, to) => {
weekTotalHours.value = null;
let _weekTotalHours = 0;
let params = {
from: from,
id: route.params.id,
to: to,
};
const { data } = await axios.get(`Workers/${route.params.id}/getWorkedHours`, {
params,
});
const workDays = data;
const map = new Map();
for (const workDay of workDays) {
workDay.dated = new Date(workDay.dated);
map.set(workDay.dated, workDay);
_weekTotalHours += workDay.workedHours;
}
for (const weekDay of weekDays.value) {
const workDay = workDays.find((day) => {
let from = new Date(day.dated);
from.setHours(0, 0, 0, 0);
let to = new Date(day.dated);
to.setHours(23, 59, 59, 59);
return weekDay.dated >= from && weekDay.dated <= to;
});
if (workDay) {
weekDay.expectedHours = workDay.expectedHours;
weekDay.workedHours = workDay.workedHours;
}
}
weekTotalHours.value = _weekTotalHours;
};
const getAbsences = async () => {
const params = {
workerFk: route.params.id,
businessFk: null,
year: startOfWeek.value.getFullYear(),
};
const { data } = await axios.get('Calendars/absences', { params });
if (data) addEvents(data);
};
const addEvents = (data) => {
const events = {};
const addEvent = (day, event) => {
events[new Date(day).getTime()] = event;
};
if (data.holidays) {
data.holidays.forEach((holiday) => {
const holidayDetail = holiday.detail && holiday.detail.description;
const holidayType = holiday.type && holiday.type.name;
const holidayName = holidayDetail || holidayType;
addEvent(holiday.dated, {
name: holidayName,
color: '#ff0',
});
});
}
if (data.absences) {
data.absences.forEach((absence) => {
const type = absence.absenceType;
addEvent(absence.dated, {
name: type.name,
color: type.rgb,
});
});
}
weekDays.value.forEach((day) => {
const timestamp = day.dated.getTime();
if (events[timestamp]) day.event = events[timestamp];
});
};
const fetchHours = async () => {
try {
await workerHoursRef.value.fetch();
await getWorkedHours(startOfWeek.value, endOfWeek.value);
await getAbsences();
} catch (err) {
console.error('Error fetching worker hours');
}
};
const fetchWorkerTimeControlMails = async (filter) => {
try {
const { data } = await axios.get('WorkerTimeControlMails', {
params: { filter: JSON.stringify(filter) },
});
return data;
} catch (err) {
console.error('Error fetching worker time control mails');
}
};
const fetchWeekData = async () => {
try {
const filter = {
where: {
workerFk: route.params.id,
year: selectedDate.value ? selectedDate.value?.getFullYear() : null,
week: selectedWeekNumber.value,
},
};
const data = await fetchWorkerTimeControlMails(filter);
if (!data.length) {
state.value = null;
} else {
const [mail] = data;
state.value = mail.state;
reason.value = mail.reason;
}
await canBeResend();
} catch (err) {
console.error('Error fetching week data');
}
};
const canBeResend = async () => {
canResend.value = false;
const filter = {
where: {
year: selectedDate.value.getFullYear(),
week: selectedWeekNumber.value,
},
limit: 1,
};
const data = await fetchWorkerTimeControlMails(filter);
if (data.length) canResend.value = true;
};
const setHours = (data) => {
for (const weekDay of weekDays.value) {
if (data) {
let day = weekDay.dated.getDay();
weekDay.hours = data
.filter((hour) => new Date(hour.timed).getDay() == day)
.sort((a, b) => new Date(a.timed) - new Date(b.timed));
} else weekDay.hours = null;
}
};
const getFinishTime = () => {
if (!weekDays.value || weekDays.value.length === 0) return;
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
let todayInWeek = weekDays.value.find(
(day) => day.dated.getTime() === today.getTime()
);
if (todayInWeek && todayInWeek.hours && todayInWeek.hours.length) {
const remainingTime = todayInWeek.workedHours
? (todayInWeek.expectedHours - todayInWeek.workedHours) * 1000
: null;
const lastKnownEntry = todayInWeek.hours[todayInWeek.hours.length - 1];
const lastKnownTime = new Date(lastKnownEntry.timed).getTime();
const finishTimeStamp =
lastKnownTime && remainingTime ? lastKnownTime + remainingTime : null;
if (finishTimeStamp) return toTimeFormat(finishTimeStamp) + ' h.';
}
};
const updateData = async () => {
await fetchHours();
await getMailStates(selectedDate.value);
};
const getMailStates = async (date) => {
const params = {
month: date.getMonth() + 1,
year: date.getFullYear(),
};
const { data } = await axios.get(
`WorkerTimeControls/${route.params.id}/getMailStates`,
{ params }
);
workerTimeControlMails.value = data;
};
const showWorkerTimeForm = (propValue, formType) => {
const isEditForm = formType === 'edit';
workerTimeFormProps.entryId = isEditForm ? propValue.id : null;
workerTimeFormProps.entryCode = isEditForm ? propValue.entryCode : null;
workerTimeFormProps.dated = isEditForm ? null : propValue;
workerTimeFormDialogRef.value.show();
};
const showReasonForm = () => {
workerTimeReasonFormDialogRef.value.show();
};
const updateWorkerTimeControlMail = async (state, reason) => {
try {
const params = {
workerId: Number(route.params.id),
year: selectedDate.value.getFullYear(),
week: selectedWeekNumber.value,
state,
};
if (reason) params.reason = reason;
await axios.post('WorkerTimeControls/updateWorkerTimeControlMail', params);
await getMailStates(selectedDate.value);
await fetchWeekData();
notify(t('globals.dataSaved'), 'positive');
} catch (err) {
console.error('Error updating worker time control mail');
}
};
const isSatisfied = async () => {
await updateWorkerTimeControlMail('CONFIRMED');
};
const isUnsatisfied = async (reason) => {
if (!reason) {
notify(t('You must indicate a reason', 'negative'));
return;
}
updateWorkerTimeControlMail('REVISE', reason);
};
const resendEmail = async () => {
try {
const params = {
recipient: worker.value?.user?.email,
week: selectedWeekNumber.value,
year: selectedDate.value.getFullYear(),
workerId: Number(route.params.id),
state: 'SENDED',
};
await axios.post('WorkerTimeControls/weekly-hour-hecord-email', params);
await getMailStates(selectedDate.value);
notify(t('Email sended'), 'positive');
} catch (err) {
console.error('Error sending email');
}
};
onBeforeMount(() => {
weekdayStore.initStore();
});
onMounted(async () => {
await setDate(Date.vnNew());
await getMailStates(selectedDate.value);
stateStore.rightDrawer = true;
});
</script>
<template>
<FetchData
ref="workerHoursRef"
url="WorkerTimeControls/filter"
:filter="workerHoursFilter"
:params="{
workerFk: route.params.id,
}"
@on-fetch="(data) => setHours(data)"
/>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<div>
<QBtnGroup push class="q-gutter-x-sm" flat>
<QBtn
v-if="isHimSelf && state"
:label="t('Satisfied')"
color="primary"
type="submit"
:disabled="state == 'CONFIRMED'"
@click="isSatisfied()"
/>
<QBtn
v-if="isHimSelf && state"
:label="t('Not satisfied')"
color="primary"
type="submit"
:disabled="state == 'REVISE'"
@click="showReasonForm()"
style="margin-left: 1px"
/>
</QBtnGroup>
<QBtnGroup push class="q-gutter-x-sm" flat style="margin-left: 0px">
<QBtn
v-if="reason && state && (isHimSelf || isHr)"
:label="t('Reason')"
color="primary"
type="submit"
@click="showReasonForm()"
/>
<QBtn
v-if="isHr && state !== 'CONFIRMED' && canResend"
:label="state ? t('Resend') : t('globals.send')"
color="primary"
type="submit"
@click="
openConfirmationModal(
t('Send time control email'),
t('Are you sure you want to send it?'),
resendEmail
)
"
>
<QTooltip>
{{ state ? t('Resend') : t('globals.send') }}
{{ t('email of this week to the user') }}
</QTooltip>
</QBtn>
</QBtnGroup>
</div>
</Teleport>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="260" class="q-pa-md">
<div class="q-pa-md q-mb-md" style="border: 2px solid #222">
<QCardSection horizontal>
<span class="text-weight-bold text-subtitle1 text-center full-width">
{{ t('Hours') }}
</span>
</QCardSection>
<QCardSection class="column items-center" horizontal>
<div>
<span class="details-label">{{ t('Total semana') }} </span>
<span>: {{ formattedWeekTotalHours }}</span>
</div>
<div>
<span class="details-label">{{ t('Termina a las') }}: </span>
<span>{{ dashIfEmpty(getFinishTime()) }}</span>
</div>
</QCardSection>
</div>
<WorkerTimeControlCalendar
v-model:model-value="selectedDateFormatted"
:selected-dates="selectedCalendarDates"
:active-date="false"
:worker-time-control-mails="workerTimeControlMails"
@click-date="onInputChange"
@on-moved="getMailStates"
/>
</QDrawer>
<QPage class="column items-center">
<QTable :columns="columns" :rows="['']" hide-bottom class="full-width">
<template #header="props">
<QTr :props="props" no-hover>
<QTh
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
style="vertical-align: top"
>
<div class="column-title-container">
<span class="text-primary">{{ t(col.label) }}</span>
<span class="q-mb-xs">{{ col.formattedDate }}</span>
<WorkerDateLabel
v-if="col.dayData?.event"
:color="col.dayData.event.color"
>
<span>
{{ col.dayData.event.name }}
</span>
</WorkerDateLabel>
</div>
</QTh>
</QTr>
</template>
<template #body="props">
<QTr no-hover>
<QTd
v-for="(day, index) in props.cols"
:key="index"
style="padding: 20px 16px !important"
>
<div class="full-height full-width column items-center">
<WorkerTimeHourChip
v-for="(hour, ind) in day.dayData?.hours"
:key="ind"
:hour="hour.timed"
:manual="hour.manual"
:direction="hour.direction"
:id="hour.id"
@on-hour-entry-deleted="updateData()"
@show-worker-time-form="
showWorkerTimeForm(
{ id: hour.id, entryCode: hour.direction },
'edit'
)
"
class="hour-chip"
/>
</div>
</QTd>
</QTr>
<QTr no-hover>
<QTd v-for="(day, index) in props.cols" :key="index">
<div class="column items-center justify-center">
<span class="q-mb-md text-sm text-body1">
{{ secondsToHoursMinutes(day.dayData?.workedHours) }}
</span>
<QIcon
name="add_circle"
color="primary"
class="fill-icon cursor-pointer"
size="sm"
@click="showWorkerTimeForm(day.dayData?.dated, 'create')"
>
<QTooltip>{{ t('Add time') }}</QTooltip>
</QIcon>
</div>
</QTd>
</QTr>
</template>
</QTable>
<QDialog ref="workerTimeFormDialogRef">
<WorkerTimeForm v-bind="workerTimeFormProps" @on-data-saved="updateData()" />
</QDialog>
<QDialog ref="workerTimeReasonFormDialogRef">
<WorkerTimeReasonForm
@on-submit="isUnsatisfied($event)"
:reason="reason"
:is-him-self="isHimSelf"
/>
</QDialog>
</QPage>
</template>
<style scoped lang="scss">
.column-title-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 14px;
}
.details-label {
color: var(--vn-label);
}
.hour-chip {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0px;
}
}
</style>
<i18n>
es:
Hours: Horas
Total semana: Total semana
Termina a las: Termina a las
Add time: Añadir hora
Reason: Motivo
Not satisfied: No conforme
Satisfied: Conforme
Resend: Reenviar
email of this week to the user: email de esta semana al usuario
Email sended: Email enviado
Send time control email: Enviar email control horario
Are you sure you want to send it?: ¿Seguro que quieres enviarlo?
You must indicate a reason: Debes indicar un motivo
</i18n>

View File

@ -0,0 +1,171 @@
<script setup>
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { QCalendarMonth } from '@quasar/quasar-ui-qcalendar/src/index.js';
import QCalendarMonthWrapper from 'src/components/ui/QCalendarMonthWrapper.vue';
const $props = defineProps({
modelValue: {
type: String,
default: '',
},
selectedDates: {
type: Array,
default: () => [],
},
showNavigation: {
type: Boolean,
default: true,
},
activeDate: {
type: Boolean,
default: true,
},
workerTimeControlMails: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['update:modelValue', 'clickDate', 'onMoved']);
const { locale } = useI18n();
const calendarRef = ref(null);
const stateClasses = {
CONFIRMED: {
className: 'confirmed',
title: 'Conforme',
},
REVISE: {
className: 'revise',
title: 'No conforme',
},
SENDED: {
className: 'sended',
title: 'Pendiente',
},
};
const value = computed({
get: () => $props.modelValue,
set: (val) => emit('update:modelValue', val),
});
const formattedNavigationLabel = computed(() => {
const [year, month, day] = $props.modelValue.split('-');
const date = new Date(year, month - 1, day);
const _month = date.toLocaleString(locale.value, { month: 'long' });
return `${_month.charAt(0).toUpperCase() + _month.slice(1)} ${year}`;
});
const workerTimeControlMailsMap = computed(() => {
if (!$props.workerTimeControlMails || !$props.workerTimeControlMails.length)
return new Map();
const map = new Map();
$props.workerTimeControlMails.forEach((mail) => map.set(mail.week, mail.state));
return map;
});
const onPrev = () => {
calendarRef.value.prev();
};
const onNext = () => {
calendarRef.value.next();
};
const clickDate = (ev) => {
emit('clickDate', ev);
};
const onMoved = (ev) => {
emit('onMoved', new Date(ev.year, ev.month - 1, ev.day));
};
const workWeeksElements = ref([]);
watch(
() => $props.workerTimeControlMails,
() => {
getWorkWeekElements();
paintWorkWeeks();
}
);
const getWorkWeekElements = () => {
workWeeksElements.value = document.getElementsByClassName(
'q-calendar-month__workweek'
);
};
const paintWorkWeeks = async () => {
for (var i = 0; i < workWeeksElements.value.length; i++) {
const element = workWeeksElements.value[i];
const week = Number(element.innerHTML);
const weekState = workerTimeControlMailsMap.value.get(week);
const { className, title } = stateClasses[weekState] || {};
element.classList.remove('confirmed', 'revise', 'sended');
if (className) {
element.classList.add(className);
element.setAttribute('title', title);
} else {
element.removeAttribute('title');
}
}
};
</script>
<template>
<QCalendarMonthWrapper>
<template #header>
<div class="row items-center full-width">
<QIcon name="arrow_back_ios" class="nav-arrow col" @click="onPrev" />
<span class="col-6 text-no-wrap text-center text-subtitle1">{{
formattedNavigationLabel
}}</span>
<QIcon name="arrow_forward_ios" class="nav-arrow col" @click="onNext" />
</div>
</template>
<template #calendar>
<QCalendarMonth
ref="calendarRef"
v-model="value"
show-work-weeks
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
:selected-dates="selectedDates"
:locale="locale"
mini-mode
enable-outside-days
class="q-py-sm"
no-active-date
@click-date="clickDate"
@moved="onMoved"
/>
</template>
</QCalendarMonthWrapper>
</template>
<style lang="scss">
.confirmed {
color: #97b92f;
}
.revise {
color: #f61e1e;
}
.sended {
color: #d19b25;
}
.nav-arrow {
cursor: pointer;
user-select: none;
}
</style>

View File

@ -0,0 +1,109 @@
<script setup>
import { reactive, ref, computed, onBeforeMount } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import FormModelPopup from 'components/FormModelPopup.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
const $props = defineProps({
entryId: {
type: Number,
default: null,
},
entryCode: {
type: String,
default: null,
},
dated: {
type: Date,
default: true,
},
});
const emit = defineEmits(['onDataSaved']);
const route = useRoute();
const { t } = useI18n();
let workerHourEntry = reactive({});
const entryDirections = [
{ code: 'in', description: t('Entrada') },
{ code: 'middle', description: t('Intermedio') },
{ code: 'out', description: t('Salida') },
];
const closeButton = ref(null);
const onDataSaved = () => {
emit('onDataSaved');
closeForm();
};
const closeForm = () => {
if (closeButton.value) closeButton.value.click();
};
const isEditMode = computed(() => !!$props.entryId);
const title = computed(() => (isEditMode.value ? t('Edit entry') : t('Add time')));
const urlCreate = computed(() =>
isEditMode.value
? `WorkerTimeControls/${$props.entryId}/updateTimeEntry`
: `WorkerTimeControls/${route.params.id}/addTimeEntry`
);
onBeforeMount(() => {
workerHourEntry = isEditMode.value
? { direction: $props.entryCode }
: {
timed: new Date($props.dated),
workerFk: route.params.id,
};
});
</script>
<template>
<FormModelPopup
:form-initial-data="workerHourEntry"
:observe-form-changes="false"
:default-actions="false"
:title="title"
:url-create="urlCreate"
@on-data-saved="onDataSaved()"
>
<template #form-inputs="{ data }">
<VnInputTime
v-if="!isEditMode"
:label="t('Hour')"
v-model="data.timed"
autofocus
:required="true"
:is-clearable="false"
/>
<VnSelectFilter
:label="t('Type')"
v-model="data.direction"
:options="entryDirections"
option-value="code"
option-label="description"
hide-selected
:required="true"
/>
</template>
</FormModelPopup>
</template>
<i18n>
es:
Add time: Añadir hora
Edit entry: Editar entrada
Hour: Hora
Type: Tipo
Entrada: Entrada
Intermedio: Intermedio
Salida: Salida
</i18n>

View File

@ -0,0 +1,143 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { computed } from 'vue';
import useNotify from 'src/composables/useNotify.js';
import { useVnConfirm } from 'composables/useVnConfirm';
import { toTimeFormat } from 'filters/date.js';
import axios from 'axios';
const $props = defineProps({
id: {
type: Number,
required: true,
},
manual: {
type: Boolean,
required: true,
},
hour: {
type: String,
required: true,
},
direction: {
type: String,
required: true,
default: null,
},
});
const emit = defineEmits(['onHourEntryDeleted', 'showWorkerTimeForm']);
const { t } = useI18n();
const { notify } = useNotify();
const { openConfirmationModal } = useVnConfirm();
const directionIconTooltip = computed(() => {
const tooltipDictionary = {
in: t('Entrada'),
out: t('Salida'),
};
return tooltipDictionary[$props.direction] || null;
});
const directionIconName = computed(() => {
const tooltipDictionary = {
in: 'arrow_forward',
out: 'arrow_back',
};
return tooltipDictionary[$props.direction] || null;
});
const deleteHourEntry = async () => {
try {
const { data } = await axios.post(
`WorkerTimeControls/${$props.id}/deleteTimeEntry`
);
if (!data) return;
emit('onHourEntryDeleted');
notify('Entry removed', 'positive');
} catch (err) {
console.error('Error deleting hour entry');
}
};
const showWorkerTimeForm = () => emit('showWorkerTimeForm');
</script>
<template>
<div class="row items-center no-wrap">
<QIcon class="direction-icon" :name="directionIconName" size="sm">
<QTooltip>{{ directionIconTooltip }}</QTooltip>
</QIcon>
<QBadge
rounded
class="chip"
:class="{ '--manual': manual }"
@click="showWorkerTimeForm()"
>
<QIcon name="edit" size="sm" class="fill-icon">
<QTooltip>{{ t('Edit') }}</QTooltip></QIcon
>
<span class="q-px-sm text-subtitle2 text-weight-regular">{{
toTimeFormat(hour)
}}</span>
<QIcon
v-if="manual"
name="cancel"
class="remove-icon"
size="sm"
@click.stop="
openConfirmationModal(
t('This time entry will be deleted'),
t('Are you sure you want to delete this entry?'),
deleteHourEntry
)
"
/>
</QBadge>
</div>
</template>
<style scoped lang="scss">
.chip {
height: 28px;
cursor: pointer;
opacity: 0.8;
color: #eee;
background-color: #222;
&.--manual {
color: var(--vn-accent-color);
background-color: $primary;
}
&:hover {
opacity: 1;
}
}
.direction-icon {
color: var(--vn-label-color);
margin-right: 6px;
}
.remove-icon {
cursor: pointer;
&:hover {
font-variation-settings: 'FILL' 1;
}
}
</style>
<i18n>
es:
Entrada: Entrada
Salida: Salida
Edit: Editar
This time entry will be deleted: Se eliminará la hora fichada
Are you sure you want to delete this entry?: ¿Seguro que quieres eliminarla?
Entry removed: Fichada borrada
</i18n>

View File

@ -0,0 +1,52 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FormPopup from 'components/FormPopup.vue';
const $props = defineProps({
reason: {
type: String,
default: '',
},
isHimSelf: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['onSubmit']);
const { t } = useI18n();
const closeButton = ref(null);
const reasonFormData = ref($props.reason);
const onSubmit = () => {
emit('onSubmit', reasonFormData.value);
closeForm();
};
const closeForm = () => {
if (closeButton.value) closeButton.value.click();
};
</script>
<template>
<FormPopup @on-submit="onSubmit()">
<template #form-inputs>
<QInput
:label="t('Reason')"
v-model="reasonFormData"
type="textarea"
autogrow
:disable="!isHimSelf"
/>
</template>
</FormPopup>
</template>
<i18n>
es:
Reason: Motivo
</i18n>

View File

@ -0,0 +1,2 @@
Search worker: Buscar trabajador
You can search by worker id or name: Puedes buscar por id o nombre del trabajador

View File

@ -10,7 +10,7 @@ export default {
component: RouterView, component: RouterView,
redirect: { name: 'ItemMain' }, redirect: { name: 'ItemMain' },
menus: { menus: {
main: ['ItemList', 'WasteBreakdown'], main: ['ItemList', 'WasteBreakdown', 'ItemFixedPrice'],
card: ['ItemBasicData'], card: ['ItemBasicData'],
}, },
children: [ children: [
@ -29,11 +29,20 @@ export default {
}, },
component: () => import('src/pages/Item/ItemList.vue'), component: () => import('src/pages/Item/ItemList.vue'),
}, },
{
path: 'fixed-price',
name: 'ItemFixedPrice',
meta: {
title: 'fixedPrice',
icon: 'vn:fixedPrice',
},
component: () => import('src/pages/Item/ItemFixedPrice.vue'),
},
{ {
path: 'create', path: 'create',
name: 'ItemCreate', name: 'ItemCreate',
meta: { meta: {
title: 'create', title: 'itemCreate',
}, },
component: () => import('src/pages/Item/ItemCreate.vue'), component: () => import('src/pages/Item/ItemCreate.vue'),
}, },

View File

@ -21,6 +21,7 @@ export default {
'WorkerLog', 'WorkerLog',
'WorkerCalendar', 'WorkerCalendar',
'WorkerDms', 'WorkerDms',
'WorkerTimeControl',
], ],
departmentCard: ['BasicData'], departmentCard: ['BasicData'],
}, },
@ -156,6 +157,16 @@ export default {
}, },
component: () => import('src/pages/Worker/Card/WorkerCalendar.vue'), component: () => import('src/pages/Worker/Card/WorkerCalendar.vue'),
}, },
{
name: 'WorkerTimeControl',
path: 'time-control',
meta: {
title: 'timeControl',
icon: 'access_time',
},
component: () =>
import('src/pages/Worker/Card/WorkerTimeControl.vue'),
},
], ],
}, },
], ],

View File

@ -60,11 +60,13 @@ export const useWeekdayStore = defineStore('weekdayStore', () => {
// El día de mañana esto permitirá ordenar los weekdays en base a el locale si se lo desea reemplazando localeOrder.es por localeOrder[locale] // El día de mañana esto permitirá ordenar los weekdays en base a el locale si se lo desea reemplazando localeOrder.es por localeOrder[locale]
const locales = []; const locales = [];
for (let code of localeOrder.es) { for (let code of localeOrder.es) {
const weekDay = weekdaysMap[code];
const locale = t(`weekdays.${weekdaysMap[code].code}`);
const obj = { const obj = {
...weekdaysMap[code], ...weekDay,
locale: t(`weekdays.${weekdaysMap[code].code}`), locale,
localeChar: t(`weekdays.${weekdaysMap[code].code}`).substr(0, 1), localeChar: locale.substr(0, 1),
localeAbr: t(`weekdays.${weekdaysMap[code].code}`).substr(0, 3), localeAbr: locale.substr(0, 3),
}; };
locales.push(obj); locales.push(obj);
} }

View File

@ -1,5 +1,6 @@
const locationOptions = '[role="listbox"] > div.q-virtual-scroll__content > .q-item'; const locationOptions = '[role="listbox"] > div.q-virtual-scroll__content > .q-item';
describe('VnLocation', () => { describe('VnLocation', () => {
const dialogInputs = '.q-dialog label input';
describe('Create', () => { describe('Create', () => {
const inputLocation = const inputLocation =
'.q-form .q-card> :nth-child(3) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control'; '.q-form .q-card> :nth-child(3) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control';
@ -39,13 +40,9 @@ describe('VnLocation', () => {
cy.get( cy.get(
':nth-child(6) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control > :nth-child(3) > .q-icon' ':nth-child(6) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control > :nth-child(3) > .q-icon'
).click(); ).click();
cy.get(' .q-card > h1').should('have.text', 'New postcode'); cy.get('.q-card > h1').should('have.text', 'New postcode');
cy.get( cy.get(dialogInputs).eq(0).clear('12');
'.q-card > :nth-child(4) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control > :nth-child(1) > input' cy.get(dialogInputs).eq(0).type('1234453');
).clear('12');
cy.get(
'.q-card > :nth-child(4) > :nth-child(1) > .q-field > .q-field__inner > .q-field__control > :nth-child(1) > input'
).type('1234453');
cy.selectOption( cy.selectOption(
'.q-dialog__inner > .column > #formModel > .q-card > :nth-child(4) > :nth-child(2) > .q-field > .q-field__inner > .q-field__control ', '.q-dialog__inner > .column > #formModel > .q-card > :nth-child(4) > :nth-child(2) > .q-field > .q-field__inner > .q-field__control ',
'Valencia' 'Valencia'

View File

@ -21,7 +21,8 @@ describe('ClaimPhoto', () => {
cy.get('.q-notification__message').should('have.text', 'Data saved'); cy.get('.q-notification__message').should('have.text', 'Data saved');
}); });
it('should open first image dialog change to second and close', () => { /* it.skip('should open first image dialog change to second and close', () => {
skiped fix on https://redmine.verdnatura.es/issues/7113
cy.get( cy.get(
':nth-child(1) > .q-card > .q-img > .q-img__container > .q-img__image' ':nth-child(1) > .q-card > .q-img > .q-img__container > .q-img__image'
).click(); ).click();
@ -37,7 +38,7 @@ describe('ClaimPhoto', () => {
cy.get('.q-carousel__slide > .q-img > .q-img__container > .q-img__image').should( cy.get('.q-carousel__slide > .q-img > .q-img__container > .q-img__image').should(
'not.be.visible' 'not.be.visible'
); );
}); }); */
it('should remove third and fourth file', () => { it('should remove third and fourth file', () => {
cy.get( cy.get(

View File

@ -1,7 +1,6 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe('InvoiceInDueDay', () => { describe('InvoiceInDueDay', () => {
const inputs = 'label input'; const inputs = 'label input';
const inputBtns = 'label button';
const addBtn = '.q-page-sticky > div > .q-btn > .q-btn__content'; const addBtn = '.q-page-sticky > div > .q-btn > .q-btn__content';
beforeEach(() => { beforeEach(() => {
@ -10,7 +9,6 @@ describe('InvoiceInDueDay', () => {
}); });
it('should update the amount', () => { it('should update the amount', () => {
cy.get(inputBtns).eq(1).click();
cy.get(inputs).eq(3).type(23); cy.get(inputs).eq(3).type(23);
cy.saveCard(); cy.saveCard();
cy.get('.q-notification__message').should('have.text', 'Data saved'); cy.get('.q-notification__message').should('have.text', 'Data saved');

View File

@ -1,11 +1,11 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe('InvoiceInVat', () => { describe('InvoiceInVat', () => {
const inputs = 'label input';
const inputBtns = 'label button';
const thirdRow = 'tbody > :nth-child(3)'; const thirdRow = 'tbody > :nth-child(3)';
const firstLineVat = 'tbody > :nth-child(1) > :nth-child(4)'; const firstLineVat = 'tbody > :nth-child(1) > :nth-child(4)';
const dialogInputs = '.q-dialog label input'; const dialogInputs = '.q-dialog label input';
const dialogBtns = '.q-dialog button'; const dialogBtns = '.q-dialog button';
const acrossInput =
':nth-child(1) > .q-td.q-table--col-auto-width > .q-field > .q-field__inner > .q-field__control > :nth-child(2) > .default-icon';
const randomInt = Math.floor(Math.random() * 100); const randomInt = Math.floor(Math.random() * 100);
beforeEach(() => { beforeEach(() => {
@ -13,11 +13,8 @@ describe('InvoiceInVat', () => {
cy.visit(`/#/invoice-in/1/vat`); cy.visit(`/#/invoice-in/1/vat`);
}); });
it('should edit the first line', () => { it('should edit the sage iva', () => {
cy.get(inputBtns).eq(1).click();
cy.get(inputs).eq(2).type(23);
cy.selectOption(firstLineVat, 'H.P. IVA 21% CEE'); cy.selectOption(firstLineVat, 'H.P. IVA 21% CEE');
cy.saveCard(); cy.saveCard();
cy.visit(`/#/invoice-in/1/vat`); cy.visit(`/#/invoice-in/1/vat`);
@ -36,16 +33,13 @@ describe('InvoiceInVat', () => {
}); });
it('should throw an error if there are fields undefined', () => { it('should throw an error if there are fields undefined', () => {
cy.get(inputBtns).eq(0).click(); cy.get(acrossInput).click();
cy.get(':nth-child(1) > .q-td.q-table--col-auto-width > .q-field > .q-field__inner > .q-field__control > :nth-child(2) > .default-icon').click();
cy.get(dialogBtns).eq(2).click(); cy.get(dialogBtns).eq(2).click();
cy.get('.q-notification__message').should('have.text', "The code can't be empty"); cy.get('.q-notification__message').should('have.text', "The code can't be empty");
}); });
it('should correctly handle expense addition', () => { it('should correctly handle expense addition', () => {
cy.get(inputBtns).eq(0).click(); cy.get(acrossInput).click();
cy.get(':nth-child(1) > .q-td.q-table--col-auto-width > .q-field > .q-field__inner > .q-field__control > :nth-child(2) > .default-icon').click();
cy.get(dialogInputs).eq(0).type(randomInt); cy.get(dialogInputs).eq(0).type(randomInt);
cy.get(dialogInputs).eq(1).click(); cy.get(dialogInputs).eq(1).click();
cy.get(dialogInputs).eq(1).type('This is a dummy expense'); cy.get(dialogInputs).eq(1).type('This is a dummy expense');

View File

@ -48,7 +48,7 @@ describe('VnPaginate', () => {
{ id: 3, name: 'Bruce Wayne' }, { id: 3, name: 'Bruce Wayne' },
], ],
}); });
vm.arrayData.hasMoreData.value = true; vm.store.hasMoreData = true;
await vm.$nextTick(); await vm.$nextTick();
vm.store.data = [ vm.store.data = [