Merge branch 'dev' into 7248-tablesObservation
gitea/salix-front/pipeline/pr-dev Build queued... Details

This commit is contained in:
Alex Moreno 2025-05-07 07:49:56 +00:00
commit cb47858f65
48 changed files with 1072 additions and 500 deletions

View File

@ -347,8 +347,8 @@ watch(formUrl, async () => {
<QBtnDropdown <QBtnDropdown
v-if="$props.goTo && $props.defaultSave" v-if="$props.goTo && $props.defaultSave"
@click="onSubmitAndGo" @click="onSubmitAndGo"
:label="tMobile('globals.saveAndContinue')" :label="tMobile('globals.saveAndContinue') + ' ' + t('globals.' + $props.goTo.split('/').pop())"
:title="t('globals.saveAndContinue')" :title="t('globals.saveAndContinue') + ' ' + t('globals.' + $props.goTo.split('/').pop())"
:disable="!hasChanges" :disable="!hasChanges"
color="primary" color="primary"
icon="save" icon="save"
@ -397,4 +397,4 @@ watch(formUrl, async () => {
:label="t && t('globals.pleaseWait')" :label="t && t('globals.pleaseWait')"
color="primary" color="primary"
/> />
</template> </template>

View File

@ -380,8 +380,8 @@ defineExpose({
data-cy="saveAndContinueDefaultBtn" data-cy="saveAndContinueDefaultBtn"
v-if="$props.goTo" v-if="$props.goTo"
@click="saveAndGo" @click="saveAndGo"
:label="tMobile('globals.saveAndContinue')" :label="tMobile('globals.saveAndContinue') + ' ' + t('globals.' + $props.goTo.split('/').pop())"
:title="t('globals.saveAndContinue')" :title="t('globals.saveAndContinue') + ' ' + t('globals.' + $props.goTo.split('/').pop())"
:disable="!hasChanges" :disable="!hasChanges"
color="primary" color="primary"
icon="save" icon="save"

View File

@ -35,6 +35,7 @@ import { getColAlign } from 'src/composables/getColAlign';
import RightMenu from '../common/RightMenu.vue'; import RightMenu from '../common/RightMenu.vue';
import VnScroll from '../common/VnScroll.vue'; import VnScroll from '../common/VnScroll.vue';
import VnCheckboxMenu from '../common/VnCheckboxMenu.vue'; import VnCheckboxMenu from '../common/VnCheckboxMenu.vue';
import VnCheckbox from '../common/VnCheckbox.vue';
const arrayData = useArrayData(useAttrs()['data-key']); const arrayData = useArrayData(useAttrs()['data-key']);
const $props = defineProps({ const $props = defineProps({
@ -332,6 +333,7 @@ function stopEventPropagation(event) {
function reload(params) { function reload(params) {
selected.value = []; selected.value = [];
selectAll.value = false;
CrudModelRef.value.reload(params); CrudModelRef.value.reload(params);
} }
@ -645,7 +647,7 @@ const rowCtrlClickFunction = computed(() => {
return () => {}; return () => {};
}); });
const handleHeaderSelection = (evt, data) => { const handleHeaderSelection = (evt, data) => {
if (evt === 'selected' && data) { if (evt === 'updateSelected' && selectAll.value) {
selected.value = tableRef.value.rows; selected.value = tableRef.value.rows;
} else if (evt === 'selectAll') { } else if (evt === 'selectAll') {
selected.value = data; selected.value = data;
@ -680,7 +682,13 @@ const handleHeaderSelection = (evt, data) => {
:class="$attrs['class'] ?? 'q-px-md'" :class="$attrs['class'] ?? 'q-px-md'"
:limit="$attrs['limit'] ?? 100" :limit="$attrs['limit'] ?? 100"
ref="CrudModelRef" ref="CrudModelRef"
@on-fetch="(...args) => emit('onFetch', ...args)" @on-fetch="
(...args) => {
selectAll = false;
selected = [];
emit('onFetch', ...args);
}
"
:search-url="searchUrl" :search-url="searchUrl"
:disable-infinite-scroll="isTableMode" :disable-infinite-scroll="isTableMode"
:before-save-fn="removeTextValue" :before-save-fn="removeTextValue"
@ -718,14 +726,23 @@ const handleHeaderSelection = (evt, data) => {
:data-cy :data-cy
> >
<template #header-selection> <template #header-selection>
<VnCheckboxMenu <div class="flex items-center no-wrap" style="display: flex">
:searchUrl="searchUrl" <VnCheckbox
:expand="$props.multiCheck.expand" v-model="selectAll"
v-model="selectAll" @click="handleHeaderSelection('updateSelected', $event)"
:url="$attrs['url']" />
@update:selected="handleHeaderSelection('selected', $event)"
@select:all="handleHeaderSelection('selectAll', $event)" <VnCheckboxMenu
/> v-if="selectAll && $props.multiCheck.expand"
:searchUrl="searchUrl"
v-model="selectAll"
:url="$attrs['url']"
@update:selected="
handleHeaderSelection('updateSelected', $event)
"
@select:all="handleHeaderSelection('selectAll', $event)"
/>
</div>
</template> </template>
<template #top-left v-if="!$props.withoutHeader"> <template #top-left v-if="!$props.withoutHeader">

View File

@ -9,10 +9,6 @@ const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const model = defineModel({ type: [Boolean] }); const model = defineModel({ type: [Boolean] });
const props = defineProps({ const props = defineProps({
expand: {
type: Boolean,
default: false,
},
url: { url: {
type: String, type: String,
required: true, required: true,
@ -22,29 +18,26 @@ const props = defineProps({
default: 'table', default: 'table',
}, },
}); });
const value = ref(false);
const menuRef = ref(null); const menuRef = ref(null);
const errorMessage = ref(null); const errorMessage = ref(null);
const rows = ref(0); const rows = ref(0);
const onClick = async () => { const onClick = async () => {
errorMessage.value = null; errorMessage.value = null;
if (value.value) { const { filter } = JSON.parse(route.query[props.searchUrl]);
const { filter } = JSON.parse(route.query[props.searchUrl]); filter.limit = 0;
filter.limit = 0; const params = {
const params = { params: { filter: JSON.stringify(filter) },
params: { filter: JSON.stringify(filter) }, };
}; try {
try { const { data } = axios.get(props.url, params);
const { data } = await axios.get(props.url, params); rows.value = data;
rows.value = data; } catch (error) {
} catch (error) { const response = error.response;
const response = error.response; if (response.data.error.name === 'UserError') {
if (response.data.error.name === 'UserError') { errorMessage.value = t('tooManyResults');
errorMessage.value = t('tooManyResults'); } else {
} else { errorMessage.value = response.data.error.message;
errorMessage.value = response.data.error.message;
}
} }
} }
}; };
@ -52,57 +45,53 @@ defineEmits(['update:selected', 'select:all']);
</script> </script>
<template> <template>
<div class="flex items-center no-wrap" style="display: flex"> <QIcon
<VnCheckbox v-model="value" @click="$emit('update:selected', value)" /> style="margin-left: -10px"
<QIcon data-cy="btnMultiCheck"
style="margin-left: -10px" name="expand_more"
data-cy="btnMultiCheck" @click="onClick"
v-if="value && $props.expand" class="cursor-pointer"
name="expand_more" color="primary"
@click="onClick" size="xs"
class="cursor-pointer" >
color="primary" <QMenu
size="xs" fit
anchor="bottom start"
self="top left"
ref="menuRef"
data-cy="menuMultiCheck"
> >
<QMenu <QList separator>
fit <QItem
anchor="bottom start" data-cy="selectAll"
self="top left" v-ripple
ref="menuRef" clickable
data-cy="menuMultiCheck" @click="
> $refs.menuRef.hide();
<QList separator> $emit('select:all', toRaw(rows));
<QItem "
data-cy="selectAll" >
v-ripple <QItemSection>
clickable <QItemLabel>
@click=" <span v-text="t('Select all')" />
$refs.menuRef.hide(); </QItemLabel>
$emit('select:all', toRaw(rows)); <QItemLabel overline caption>
" <span
> v-if="errorMessage"
<QItemSection> class="text-negative"
<QItemLabel> v-text="errorMessage"
<span v-text="t('Select all')" /> />
</QItemLabel> <span
<QItemLabel overline caption> v-else
<span v-text="t('records', { rows: rows.length ?? 0 })"
v-if="errorMessage" />
class="text-negative" </QItemLabel>
v-text="errorMessage" </QItemSection>
/> </QItem>
<span <slot name="more-options"></slot>
v-else </QList>
v-text="t('records', { rows: rows.length })" </QMenu>
/> </QIcon>
</QItemLabel>
</QItemSection>
</QItem>
<slot name="more-options"></slot>
</QList>
</QMenu>
</QIcon>
</div>
</template> </template>
<i18n lang="yml"> <i18n lang="yml">
en: en:

View File

@ -132,8 +132,7 @@ const card = toRef(props, 'item');
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
white-space: nowrap;
width: 192px;
p { p {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@ -1,10 +1,11 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { ref, reactive, useAttrs, computed } from 'vue'; import { ref, reactive, useAttrs, computed, onMounted, nextTick, } from 'vue';
import { onBeforeRouteLeave } from 'vue-router'; import { onBeforeRouteLeave , useRouter, useRoute} from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { tMobile } from 'src/composables/tMobile';
import { toDateHourMin } from 'src/filters'; import { toDateHourMin } from 'src/filters';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
@ -33,10 +34,15 @@ const $props = defineProps({
addNote: { type: Boolean, default: false }, addNote: { type: Boolean, default: false },
selectType: { type: Boolean, default: false }, selectType: { type: Boolean, default: false },
justInput: { type: Boolean, default: false }, justInput: { type: Boolean, default: false },
goTo: { type: String, default: '', },
}); });
const { t } = useI18n(); const { t } = useI18n();
const quasar = useQuasar(); const quasar = useQuasar();
const stateStore = useStateStore();
const router = useRouter();
const route = useRoute();
const componentIsRendered = ref(false);
const newNote = reactive({ text: null, observationTypeFk: null }); const newNote = reactive({ text: null, observationTypeFk: null });
const observationTypes = ref([]); const observationTypes = ref([]);
const vnPaginateRef = ref(); const vnPaginateRef = ref();
@ -45,6 +51,7 @@ const defaultObservationType = computed(() =>
observationTypes.value.find(ot => ot.code === 'salesPerson')?.id observationTypes.value.find(ot => ot.code === 'salesPerson')?.id
); );
let savedNote = false;
let originalText; let originalText;
function handleClick(e) { function handleClick(e) {
@ -68,6 +75,7 @@ async function insert() {
}; };
await axios.post($props.url, newBody); await axios.post($props.url, newBody);
await vnPaginateRef.value.fetch(); await vnPaginateRef.value.fetch();
savedNote = true;
} }
function confirmAndUpdate() { function confirmAndUpdate() {
@ -129,8 +137,29 @@ const handleObservationTypes = (data) => {
} }
}; };
onMounted(() => {
nextTick(() => (componentIsRendered.value = true));
});
async function saveAndGo() {
savedNote = false;
await insert();
await savedNote;
router.push({ path: $props.goTo });
}
</script> </script>
<template> <template>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && componentIsRendered && $props.goTo && !route.path.includes('summary')">
<QBtn
:label="tMobile('globals.saveAndContinue') + ' ' + t('globals.' + $props.goTo.split('/').pop())"
:title="t('globals.saveAndContinue') + ' ' + t('globals.' + $props.goTo.split('/').pop())"
color="primary"
icon="save"
@click="saveAndGo"
data-cy="saveContinueNoteButton"
/>
</Teleport>
<FetchData <FetchData
v-if="selectType" v-if="selectType"
url="ObservationTypes" url="ObservationTypes"
@ -176,7 +205,7 @@ const handleObservationTypes = (data) => {
:required="'required' in originalAttrs" :required="'required' in originalAttrs"
clearable clearable
> >
<template #append> <template #append v-if="!$props.goTo">
<QBtn <QBtn
:title="t('Save (Enter)')" :title="t('Save (Enter)')"
icon="save" icon="save"

View File

@ -6,6 +6,7 @@ import toDateHourMinSec from './toDateHourMinSec';
import toRelativeDate from './toRelativeDate'; import toRelativeDate from './toRelativeDate';
import toCurrency from './toCurrency'; import toCurrency from './toCurrency';
import toPercentage from './toPercentage'; import toPercentage from './toPercentage';
import toNumber from './toNumber';
import toLowerCamel from './toLowerCamel'; import toLowerCamel from './toLowerCamel';
import dashIfEmpty from './dashIfEmpty'; import dashIfEmpty from './dashIfEmpty';
import dateRange from './dateRange'; import dateRange from './dateRange';
@ -34,6 +35,7 @@ export {
toRelativeDate, toRelativeDate,
toCurrency, toCurrency,
toPercentage, toPercentage,
toNumber,
dashIfEmpty, dashIfEmpty,
dateRange, dateRange,
getParamWhere, getParamWhere,

8
src/filters/toNumber.js Normal file
View File

@ -0,0 +1,8 @@
export default function (value, fractionSize = 2) {
if (isNaN(value)) return value;
return new Intl.NumberFormat('es-ES', {
style: 'decimal',
minimumFractionDigits: 0,
maximumFractionDigits: fractionSize,
}).format(value);
}

View File

@ -24,13 +24,14 @@ globals:
dataDeleted: Data deleted dataDeleted: Data deleted
delete: Delete delete: Delete
search: Search search: Search
lines: Lines
changes: Changes changes: Changes
dataCreated: Data created dataCreated: Data created
add: Add add: Add
create: Create create: Create
edit: Edit edit: Edit
save: Save save: Save
saveAndContinue: Save and continue saveAndContinue: Save and go to
remove: Remove remove: Remove
reset: Reset reset: Reset
close: Close close: Close
@ -107,6 +108,8 @@ globals:
from: From from: From
to: To to: To
notes: Notes notes: Notes
photos: Photos
due-day: Due day
refresh: Refresh refresh: Refresh
item: Item item: Item
ticket: Ticket ticket: Ticket
@ -122,6 +125,7 @@ globals:
producer: Producer producer: Producer
origin: Origin origin: Origin
state: State state: State
total: Total
subtotal: Subtotal subtotal: Subtotal
visible: Visible visible: Visible
price: Price price: Price
@ -347,6 +351,7 @@ globals:
vehicleList: Vehicles vehicleList: Vehicles
vehicle: Vehicle vehicle: Vehicle
entryPreAccount: Pre-account entryPreAccount: Pre-account
management: Worker management
unsavedPopup: unsavedPopup:
title: Unsaved changes will be lost title: Unsaved changes will be lost
subtitle: Are you sure exit without saving? subtitle: Are you sure exit without saving?
@ -395,6 +400,7 @@ errors:
updateUserConfig: Error updating user config updateUserConfig: Error updating user config
tokenConfig: Error fetching token config tokenConfig: Error fetching token config
writeRequest: The requested operation could not be completed writeRequest: The requested operation could not be completed
claimBeginningQuantity: Cannot import a line with a claimed quantity of 0
login: login:
title: Login title: Login
username: Username username: Username

View File

@ -25,12 +25,13 @@ globals:
openDetail: Ver detalle openDetail: Ver detalle
delete: Eliminar delete: Eliminar
search: Buscar search: Buscar
lines: Lineas
changes: Cambios changes: Cambios
add: Añadir add: Añadir
create: Crear create: Crear
edit: Modificar edit: Modificar
save: Guardar save: Guardar
saveAndContinue: Guardar y continuar saveAndContinue: Guardar e ir a
remove: Eliminar remove: Eliminar
reset: Restaurar reset: Restaurar
close: Cerrar close: Cerrar
@ -111,6 +112,8 @@ globals:
from: Desde from: Desde
to: Hasta to: Hasta
notes: Notas notes: Notas
photos: Fotos
due-day: Vencimiento
refresh: Actualizar refresh: Actualizar
item: Artículo item: Artículo
ticket: Ticket ticket: Ticket
@ -126,6 +129,7 @@ globals:
producer: Productor producer: Productor
origin: Origen origin: Origen
state: Estado state: Estado
total: Total
subtotal: Subtotal subtotal: Subtotal
visible: Visible visible: Visible
price: Precio price: Precio
@ -350,6 +354,7 @@ globals:
vehicleList: Vehículos vehicleList: Vehículos
vehicle: Vehículo vehicle: Vehículo
entryPreAccount: Precontabilizar entryPreAccount: Precontabilizar
management: Gestión de trabajadores
unsavedPopup: unsavedPopup:
title: Los cambios que no haya guardado se perderán title: Los cambios que no haya guardado se perderán
subtitle: ¿Seguro que quiere salir sin guardar? subtitle: ¿Seguro que quiere salir sin guardar?
@ -391,6 +396,7 @@ errors:
updateUserConfig: Error al actualizar la configuración de usuario updateUserConfig: Error al actualizar la configuración de usuario
tokenConfig: Error al obtener configuración de token tokenConfig: Error al obtener configuración de token
writeRequest: No se pudo completar la operación solicitada writeRequest: No se pudo completar la operación solicitada
claimBeginningQuantity: No se puede importar una linea sin una cantidad reclamada
login: login:
title: Inicio de sesión title: Inicio de sesión
username: Nombre de usuario username: Nombre de usuario

View File

@ -13,8 +13,10 @@ import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue'; import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import useNotify from 'src/composables/useNotify.js';
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify();
const quasar = useQuasar(); const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const claim = ref(null); const claim = ref(null);
@ -176,12 +178,17 @@ async function save(data) {
} }
async function importToNewRefundTicket() { async function importToNewRefundTicket() {
await post(`ClaimBeginnings/${claimId}/importToNewRefundTicket`); try{
await claimActionsForm.value.reload(); await post(`ClaimBeginnings/${claimId}/importToNewRefundTicket`);
quasar.notify({ await claimActionsForm.value.reload();
message: t('globals.dataSaved'), quasar.notify({
type: 'positive', message: t('globals.dataSaved'),
}); type: 'positive',
});
} catch (error) {
const errorMessage = error.response?.data?.error?.message;
notify( t(errorMessage), 'negative' );
}
} }
async function post(query, params) { async function post(query, params) {

View File

@ -33,6 +33,7 @@ function onBeforeSave(formData, originalData) {
<FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load /> <FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load />
<FormModel <FormModel
model="Claim" model="Claim"
:go-to="`/claim/${route.params.id}/notes`"
:url-update="`Claims/updateClaim/${route.params.id}`" :url-update="`Claims/updateClaim/${route.params.id}`"
:mapper="onBeforeSave" :mapper="onBeforeSave"
auto-load auto-load

View File

@ -78,6 +78,8 @@ const columns = computed(() => [
label: t('Quantity'), label: t('Quantity'),
field: ({ sale }) => sale.quantity, field: ({ sale }) => sale.quantity,
sortable: true, sortable: true,
style: 'padding-right: 2%;',
headerStyle: 'padding-right: 2%;'
}, },
{ {
name: 'claimed', name: 'claimed',
@ -110,6 +112,8 @@ const columns = computed(() => [
field: ({ sale }) => totalRow(sale), field: ({ sale }) => totalRow(sale),
format: (value) => toCurrency(value), format: (value) => toCurrency(value),
sortable: true, sortable: true,
style: 'padding-right: 2%;',
headerStyle: 'padding-right: 2%;'
}, },
]); ]);
@ -152,12 +156,33 @@ function showImportDialog() {
.onOk(() => claimLinesForm.value.reload()); .onOk(() => claimLinesForm.value.reload());
} }
async function saveWhenHasChanges() { function fillClaimedQuantities() {
if (claimLinesForm.value.getChanges().updates) { const formData = claimLinesForm.value.formData;
await claimLinesForm.value.onSubmit(); let hasChanges = false;
onFetch(claimLinesForm.value.formData);
const selectedRows = formData.filter(row => selected.value.includes(row));
for (const row of selectedRows) {
if (row.quantity === 0 || row.quantity === null) {
row.quantity = row.sale.quantity;
hasChanges = true;
}
}
if (hasChanges) {
quasar.notify({
message: t('Quantities filled automatically'),
type: 'positive',
});
} else {
quasar.notify({
message: t('No quantities to fill'),
type: 'info',
});
} }
} }
</script> </script>
<template> <template>
<Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()"> <Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()">
@ -185,15 +210,16 @@ async function saveWhenHasChanges() {
auto-load auto-load
/> />
<div class="q-pa-md"> <div class="q-pa-md">
<CrudModel <CrudModel
data-key="ClaimLines" data-key="claimLines"
ref="claimLinesForm" ref="claimLinesForm"
:go-to="`photos`"
:url="`Claims/${route.params.id}/lines`" :url="`Claims/${route.params.id}/lines`"
save-url="ClaimBeginnings/crud" save-url="ClaimBeginnings/crud"
:user-filter="linesFilter" :user-filter="linesFilter"
@on-fetch="onFetch" @on-fetch="onFetch"
v-model:selected="selected" v-model:selected="selected"
:default-save="false"
:default-reset="false" :default-reset="false"
auto-load auto-load
:limit="0" :limit="0"
@ -214,8 +240,7 @@ async function saveWhenHasChanges() {
v-model.number="row.quantity" v-model.number="row.quantity"
type="number" type="number"
dense dense
@keyup.enter="saveWhenHasChanges()"
@blur="saveWhenHasChanges()"
/> />
</QTd> </QTd>
</template> </template>
@ -272,10 +297,7 @@ async function saveWhenHasChanges() {
type="number" type="number"
dense dense
autofocus autofocus
@keyup.enter="
saveWhenHasChanges()
"
@blur="saveWhenHasChanges()"
/> />
</QItemLabel> </QItemLabel>
</template> </template>
@ -313,6 +335,18 @@ async function saveWhenHasChanges() {
</template> </template>
</QTable> </QTable>
</template> </template>
<template #moreBeforeActions>
<QBtn
color="primary"
text-color="white"
:unelevated="true"
:label="t('Rellenar cantidades')"
:title="t('Rellenar cantidades')"
icon="auto_fix_high"
:disabled="!selected.length"
@click="fillClaimedQuantities"
/>
</template>
</CrudModel> </CrudModel>
</div> </div>
@ -358,6 +392,8 @@ es:
Delete claimed sales: Eliminar ventas reclamadas Delete claimed sales: Eliminar ventas reclamadas
Discount updated: Descuento actualizado Discount updated: Descuento actualizado
Claimed quantity: Cantidad reclamada Claimed quantity: Cantidad reclamada
Quantities filled automatically: Cantidades rellenadas automáticamente
No quantities to fill: No hay cantidades para rellenar
You are about to remove {count} rows: ' You are about to remove {count} rows: '
Vas a eliminar <strong>{count}</strong> línea | Vas a eliminar <strong>{count}</strong> línea |
Vas a eliminar <strong>{count}</strong> líneas' Vas a eliminar <strong>{count}</strong> líneas'

View File

@ -21,9 +21,11 @@ const claimFilter = {
}, },
}; };
</script> </script>
<template> <template>
<VnNotes <VnNotes
url="claimObservations" url="claimObservations"
:go-to="`/claim/${route.params.id}/lines`"
:add-note="$props.addNote" :add-note="$props.addNote"
:user-filter="claimFilter" :user-filter="claimFilter"
:filter="{ where: { claimFk: claimId } }" :filter="{ where: { claimFk: claimId } }"

View File

@ -54,7 +54,7 @@ const detailsColumns = ref([
{ {
name: 'item', name: 'item',
label: 'claim.item', label: 'claim.item',
field: (row) => row.sale.itemFk, field: (row) => dashIfEmpty(row.sale.itemFk),
sortable: true, sortable: true,
}, },
{ {
@ -67,13 +67,13 @@ const detailsColumns = ref([
{ {
name: 'quantity', name: 'quantity',
label: 'claim.quantity', label: 'claim.quantity',
field: (row) => row.sale.quantity, field: (row) => dashIfEmpty(row.sale.quantity),
sortable: true, sortable: true,
}, },
{ {
name: 'claimed', name: 'claimed',
label: 'claim.claimed', label: 'claim.claimed',
field: (row) => row.quantity, field: (row) => dashIfEmpty(row.quantity),
sortable: true, sortable: true,
}, },
{ {
@ -84,7 +84,7 @@ const detailsColumns = ref([
{ {
name: 'price', name: 'price',
label: 'claim.price', label: 'claim.price',
field: (row) => row.sale.price, field: (row) => dashIfEmpty(row.sale.price),
sortable: true, sortable: true,
}, },
{ {
@ -337,23 +337,16 @@ function claimUrl(section) {
</QTh> </QTh>
</QTr> </QTr>
</template> </template>
<template #body="props"> <template #body-cell-description="props">
<QTr :props="props"> <QTd :props="props">
<QTd v-for="col in props.cols" :key="col.name" :props="props"> <span class="link">
<template v-if="col.name === 'description'"> {{ props.value }}
<span class="link">{{ </span>
dashIfEmpty(col.field(props.row)) <ItemDescriptorProxy
}}</span> :id="props.row.sale.itemFk"
<ItemDescriptorProxy :sale-fk="props.row.saleFk"
:id="props.row.sale.itemFk" />
:sale-fk="props.row.saleFk" </QTd>
/>
</template>
<template v-else>
{{ dashIfEmpty(col.field(props.row)) }}
</template>
</QTd>
</QTr>
</template> </template>
</QTable> </QTable>
</QCard> </QCard>

View File

@ -2,12 +2,13 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { dashIfEmpty, toCurrency, toDate } from 'src/filters';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnPaginate from 'src/components/ui/VnPaginate.vue';
import ModalCloseContract from 'src/pages/Customer/components/ModalCloseContract.vue'; import ModalCloseContract from 'src/pages/Customer/components/ModalCloseContract.vue';
import { toDate } from 'src/filters'; import CustomerCreditContractsCreate from '../components/CustomerCreditContractsCreate.vue';
import VnLv from 'src/components/ui/VnLv.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -16,6 +17,7 @@ const quasar = useQuasar();
const vnPaginateRef = ref(null); const vnPaginateRef = ref(null);
const showQPageSticky = ref(true); const showQPageSticky = ref(true);
const showForm = ref();
const filter = { const filter = {
order: 'finished ASC, started DESC', order: 'finished ASC, started DESC',
@ -36,25 +38,21 @@ const fetch = (data) => {
data.forEach((element) => { data.forEach((element) => {
if (!element.finished) { if (!element.finished) {
showQPageSticky.value = false; showQPageSticky.value = false;
return;
} }
}); });
}; };
const toCustomerCreditContractsCreate = () => {
router.push({ name: 'CustomerCreditContractsCreate' });
};
const openDialog = (item) => { const openDialog = (item) => {
quasar.dialog({ quasar.dialog({
component: ModalCloseContract, component: ModalCloseContract,
componentProps: { componentProps: {
id: item.id, id: item.id,
promise: updateData, promise: async () => {
await updateData();
showQPageSticky.value = true;
},
}, },
}); });
updateData();
showQPageSticky.value = true;
}; };
const openViewCredit = (credit) => { const openViewCredit = (credit) => {
@ -66,14 +64,14 @@ const openViewCredit = (credit) => {
}); });
}; };
const updateData = () => { const updateData = async () => {
vnPaginateRef.value?.fetch(); await vnPaginateRef.value?.fetch();
}; };
</script> </script>
<template> <template>
<div class="full-width flex justify-center"> <section class="row justify-center">
<QCard class="card-width q-pa-lg"> <QCard class="q-pa-lg" style="width: 70%">
<VnPaginate <VnPaginate
:user-filter="filter" :user-filter="filter"
@on-fetch="fetch" @on-fetch="fetch"
@ -84,100 +82,84 @@ const updateData = () => {
url="CreditClassifications" url="CreditClassifications"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<div v-if="rows.length"> <div v-if="rows.length" class="q-gutter-y-md">
<QCard <QCard
v-for="(item, index) in rows" v-for="(item, index) in rows"
:key="index" :key="index"
:class="{ :class="{ disabled: item.finished }"
'customer-card': true,
'q-mb-md': index < rows.length - 1,
'is-active': !item.finished,
}"
> >
<QCardSection <QCardSection
class="full-width flex justify-between q-py-none" class="full-width"
:class="{ 'row justify-between': $q.screen.gt.md }"
> >
<div class="width-state flex"> <div class="width-state row no-wrap">
<div <QIcon
class="flex items-center cursor-pointer q-mr-md" :style="{
v-if="!item.finished" visibility: item.finished
? 'hidden'
: 'visible',
}"
@click.stop="openDialog(item)"
color="primary"
name="lock"
data-cy="closeBtn"
size="md"
class="fill-icon q-px-md"
> >
<QIcon <QTooltip>{{ t('Close contract') }}</QTooltip>
@click.stop="openDialog(item)" </QIcon>
color="primary"
name="lock"
size="md"
class="fill-icon"
>
<QTooltip>{{ t('Close contract') }}</QTooltip>
</QIcon>
</div>
<div> <div class="column">
<div class="flex q-mb-xs"> <VnLv
<div class="q-mr-sm color-vn-label"> :label="t('Since')"
{{ t('Since') }}: :value="toDate(item.started)"
</div> />
<div class="text-weight-bold"> <VnLv
{{ toDate(item.started) }} :label="t('To')"
</div> :value="toDate(item.finished)"
</div> />
<div class="flex">
<div class="q-mr-sm color-vn-label">
{{ t('To') }}:
</div>
<div class="text-weight-bold">
{{ toDate(item.finished) }}
</div>
</div>
</div> </div>
</div> </div>
<QSeparator vertical /> <QSeparator vertical />
<div class="width-data flex"> <div class="column width-data">
<div <div
class="full-width flex justify-between items-center" class="column"
v-if="item?.insurances.length" v-if="item?.insurances.length"
v-for="insurance in item.insurances"
:key="insurance.id"
> >
<div class="flex"> <div
<div class="color-vn-label q-mr-xs"> :class="{
{{ t('Credit') }}: 'row q-gutter-x-md': $q.screen.gt.sm,
</div> }"
<div class="text-weight-bold"> class="q-mb-sm"
{{ item.insurances[0].credit }} >
</div> <VnLv
</div> :label="t('Credit')"
<div class="flex"> :value="toCurrency(insurance.credit)"
<div class="color-vn-label q-mr-xs"> />
{{ t('Grade') }}: <VnLv
</div> :label="t('Grade')"
<div class="text-weight-bold"> :value="dashIfEmpty(insurance.grade)"
{{ item.insurances[0].grade || '-' }} />
</div> <VnLv
</div> :label="t('Date')"
<div class="flex"> :value="toDate(insurance.created)"
<div class="color-vn-label q-mr-xs"> />
{{ t('Date') }}:
</div>
<div class="text-weight-bold">
{{ toDate(item.insurances[0].created) }}
</div>
</div>
<div class="flex items-center cursor-pointer">
<QIcon
@click.stop="openViewCredit(item)"
color="primary"
name="preview"
size="md"
>
<QTooltip>{{
t('View credits')
}}</QTooltip>
</QIcon>
</div> </div>
</div> </div>
</div> </div>
<QBtn
@click.stop="openViewCredit(item)"
icon="preview"
size="md"
:title="t('View credits')"
data-cy="viewBtn"
color="primary"
flat
/>
</QCardSection> </QCardSection>
</QCard> </QCard>
</div> </div>
@ -187,11 +169,12 @@ const updateData = () => {
</template> </template>
</VnPaginate> </VnPaginate>
</QCard> </QCard>
</div> </section>
<QPageSticky :offset="[18, 18]" v-if="showQPageSticky"> <QPageSticky :offset="[18, 18]" v-if="showQPageSticky">
<QBtn <QBtn
@click.stop="toCustomerCreditContractsCreate()" data-cy="createBtn"
@click.stop="showForm = !showForm"
color="primary" color="primary"
fab fab
icon="add" icon="add"
@ -201,24 +184,25 @@ const updateData = () => {
{{ t('New contract') }} {{ t('New contract') }}
</QTooltip> </QTooltip>
</QPageSticky> </QPageSticky>
<QDialog v-model="showForm">
<CustomerCreditContractsCreate @on-data-saved="updateData()" />
</QDialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.customer-card {
border: 2px solid var(--vn-light-gray);
border-radius: 10px;
padding: 10px;
display: flex;
justify-content: space-between;
}
.is-active {
background-color: var(--vn-light-gray);
}
.width-state { .width-state {
width: 30%; width: 30%;
} }
.width-data { .width-data {
width: 65%; width: 50%;
}
::v-deep(.label) {
margin-right: 5px;
}
::v-deep(.label)::after {
content: ':';
color: var(--vn-label-color);
} }
</style> </style>

View File

@ -0,0 +1,93 @@
<script setup>
import { computed, onBeforeMount, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { toCurrency, toDate } from 'src/filters';
import VnTable from 'src/components/VnTable/VnTable.vue';
import axios from 'axios';
const { t } = useI18n();
const route = useRoute();
const create = ref(null);
const tableRef = ref();
const columns = computed(() => [
{
align: 'left',
field: 'created',
format: ({ created }) => toDate(created),
label: t('Created'),
name: 'created',
create: true,
columnCreate: {
component: 'date',
},
},
{
align: 'left',
field: 'grade',
label: t('Grade'),
name: 'grade',
create: true,
},
{
align: 'left',
format: ({ credit }) => toCurrency(credit),
label: t('Credit'),
name: 'credit',
create: true,
},
]);
onBeforeMount(async () => {
const query = `CreditClassifications/findOne?filter=${encodeURIComponent(
JSON.stringify({
fields: ['finished'],
where: { id: route.params.creditId },
}),
)}`;
const { data } = await axios(query);
create.value = data.finished
? false
: {
urlCreate: 'CreditInsurances',
title: t('Create Insurance'),
onDataSaved: () => tableRef.value.reload(),
formInitialData: {
created: Date.vnNew(),
creditClassificationFk: route.params.creditId,
},
};
});
</script>
<template>
<VnTable
v-if="create != null"
url="CreditInsurances"
ref="tableRef"
data-key="creditInsurances"
:filter="{
where: {
creditClassificationFk: `${route.params.creditId}`,
},
order: 'created DESC',
}"
:columns="columns"
:right-search="false"
:is-editable="false"
:use-model="true"
:column-search="false"
:disable-option="{ card: true }"
:create
auto-load
/>
</template>
<i18n>
es:
Created: Fecha creación
Grade: Grade
Credit: Crédito
</i18n>

View File

@ -99,7 +99,13 @@ async function acceptPropagate({ isEqualizated }) {
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnInput :label="t('Street')" clearable v-model="data.street" required /> <VnInput
:label="t('Street')"
clearable
v-model="data.street"
:uppercase="true"
required
/>
</VnRow> </VnRow>
<VnRow> <VnRow>

View File

@ -26,6 +26,7 @@ const columns = computed(() => [
url: 'Clients', url: 'Clients',
fields: ['id', 'socialName'], fields: ['id', 'socialName'],
optionLabel: 'socialName', optionLabel: 'socialName',
optionValue: 'socialName',
}, },
}, },
columnClass: 'expand', columnClass: 'expand',
@ -37,8 +38,11 @@ const columns = computed(() => [
name: 'city', name: 'city',
columnFilter: { columnFilter: {
component: 'select', component: 'select',
inWhere: true,
attrs: { attrs: {
url: 'Towns', url: 'Towns',
optionValue: 'name',
optionLabel: 'name',
}, },
}, },
cardVisible: true, cardVisible: true,
@ -94,7 +98,7 @@ const columns = computed(() => [
</VnSubToolbar> </VnSubToolbar>
<VnTable <VnTable
:data-key="dataKey" :data-key="dataKey"
url="Clients/filter" url="Clients/extendedListFilter"
:table="{ :table="{
'row-key': 'id', 'row-key': 'id',
selection: 'multiple', selection: 'multiple',

View File

@ -3,44 +3,29 @@ import { reactive, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.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 VnInputDate from 'src/components/common/VnInputDate.vue';
import FormModelPopup from 'src/components/FormModelPopup.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const routeId = computed(() => route.params.id); const routeId = computed(() => route.params.id);
const router = useRouter();
const initialData = reactive({ const initialData = reactive({
started: Date.vnNew(), started: Date.vnNew(),
clientFk: routeId.value, clientFk: routeId.value,
}); });
const toCustomerCreditContracts = () => {
router.push({ name: 'CustomerCreditContracts' });
};
</script> </script>
<template> <template>
<FormModel <FormModelPopup
v-on="$attrs"
:form-initial-data="initialData" :form-initial-data="initialData"
:observe-form-changes="false" :observe-form-changes="false"
url-create="creditClassifications/createWithInsurance" url-create="creditClassifications/createWithInsurance"
@on-data-saved="toCustomerCreditContracts()"
> >
<template #moreActions> <template #form-inputs="{ data }">
<QBtn
:label="t('globals.cancel')"
@click="toCustomerCreditContracts"
color="primary"
flat
icon="close"
/>
</template>
<template #form="{ data }">
<VnRow> <VnRow>
<div class="col"> <div class="col">
<VnInput <VnInput
@ -63,7 +48,7 @@ const toCustomerCreditContracts = () => {
</div> </div>
</VnRow> </VnRow>
</template> </template>
</FormModel> </FormModelPopup>
</template> </template>
<i18n> <i18n>

View File

@ -1,63 +0,0 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { toCurrency, toDate } from 'src/filters';
import VnTable from 'src/components/VnTable/VnTable.vue';
const { t } = useI18n();
const route = useRoute();
const filter = {
where: {
creditClassificationFk: `${route.params.creditId}`,
},
limit: 20,
};
const columns = computed(() => [
{
align: 'left',
field: 'created',
format: ({ created }) => toDate(created),
label: t('Created'),
name: 'created',
},
{
align: 'left',
field: 'grade',
label: t('Grade'),
name: 'grade',
},
{
align: 'left',
format: ({ credit }) => toCurrency(credit),
label: t('Credit'),
name: 'credit',
},
]);
</script>
<template>
<VnTable
url="CreditInsurances"
ref="tableRef"
data-key="creditInsurances"
:filter="filter"
:columns="columns"
:right-search="false"
:is-editable="false"
:use-model="true"
:column-search="false"
:disable-option="{ card: true }"
auto-load
></VnTable>
</template>
<i18n>
es:
Created: Fecha creación
Grade: Grade
Credit: Crédito
</i18n>

View File

@ -15,7 +15,7 @@ import InvoiceOutDescriptorProxy from 'pages/InvoiceOut/Card/InvoiceOutDescripto
import RouteDescriptorProxy from 'src/pages/Route/Card/RouteDescriptorProxy.vue'; import RouteDescriptorProxy from 'src/pages/Route/Card/RouteDescriptorProxy.vue';
import VnTable from 'src/components/VnTable/VnTable.vue'; import VnTable from 'src/components/VnTable/VnTable.vue';
import CustomerDescriptorProxy from '../Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from '../Card/CustomerDescriptorProxy.vue';
import { getItemPackagingType } from '../composables/getItemPackagingType.js';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -161,23 +161,6 @@ const setShippedColor = (date) => {
}; };
const rowClick = ({ id }) => const rowClick = ({ id }) =>
window.open(router.resolve({ params: { id }, name: 'TicketSummary' }).href, '_blank'); window.open(router.resolve({ params: { id }, name: 'TicketSummary' }).href, '_blank');
const getItemPackagingType = (ticketSales) => {
if (!ticketSales?.length) return '-';
const packagingTypes = ticketSales.reduce((types, sale) => {
const { itemPackingTypeFk } = sale.item;
if (
!types.includes(itemPackingTypeFk) &&
(itemPackingTypeFk === 'H' || itemPackingTypeFk === 'V')
) {
types.push(itemPackingTypeFk);
}
return types;
}, []);
return dashIfEmpty(packagingTypes.join(', ') || '-');
};
</script> </script>
<template> <template>

View File

@ -0,0 +1,36 @@
import { describe, it, expect } from 'vitest';
import { getItemPackagingType } from '../getItemPackagingType';
describe('getItemPackagingType', () => {
it('should return "-" if ticketSales is null or undefined', () => {
expect(getItemPackagingType(null)).toBe('-');
expect(getItemPackagingType(undefined)).toBe('-');
});
it('should return "-" if ticketSales does not have a length property', () => {
const ticketSales = { someKey: 'someValue' }; // No tiene propiedad length
expect(getItemPackagingType(ticketSales)).toBe('-');
});
it('should return unique packaging types as a comma-separated string', () => {
const ticketSales = [
{ item: { itemPackingTypeFk: 'H' } },
{ item: { itemPackingTypeFk: 'V' } },
{ item: { itemPackingTypeFk: 'H' } },
];
expect(getItemPackagingType(ticketSales)).toBe('H, V');
});
it('should return unique packaging types as a comma-separated string', () => {
const ticketSales = [
{ item: { itemPackingTypeFk: 'H' } },
{ item: { itemPackingTypeFk: 'V' } },
{ item: { itemPackingTypeFk: 'H' } },
{ item: { itemPackingTypeFk: 'A' } },
];
expect(getItemPackagingType(ticketSales)).toBe('H, V, A');
});
it('should return "-" if ticketSales is an empty array', () => {
expect(getItemPackagingType([])).toBe('-');
});
});

View File

@ -0,0 +1,11 @@
import { dashIfEmpty } from 'src/filters';
export function getItemPackagingType(ticketSales) {
if (!ticketSales?.length) return '-';
const packagingTypes = Array.from(
new Set(ticketSales.map(({ item: { itemPackingTypeFk } }) => itemPackingTypeFk)),
);
return dashIfEmpty(packagingTypes.join(', '));
}

View File

@ -1,22 +1,20 @@
<script setup> <script setup>
import { useRoute } from 'vue-router';
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import FetchedTags from 'components/ui/FetchedTags.vue';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import SupplierConsumptionFilter from './SupplierConsumptionFilter.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import { dateRange, toDate } from 'src/filters';
import { dashIfEmpty } from 'src/filters';
import { usePrintService } from 'composables/usePrintService';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios'; import axios from 'axios';
import { dateRange, toCurrency, toNumber, toDateHourMin } from 'src/filters';
import { usePrintService } from 'composables/usePrintService';
import useNotify from 'src/composables/useNotify';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import SupplierConsumptionFilter from './SupplierConsumptionFilter.vue';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
const state = useState(); const state = useState();
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -31,7 +29,86 @@ const arrayData = useArrayData('SupplierConsumption', {
order: ['itemTypeFk', 'itemName', 'itemSize'], order: ['itemTypeFk', 'itemName', 'itemSize'],
userFilter: { where: { supplierFk: route.params.id } }, userFilter: { where: { supplierFk: route.params.id } },
}); });
const headerColumns = computed(() => [
{
name: 'id',
label: t('globals.entry'),
align: 'left',
field: 'id',
sortable: true,
},
{
name: 'invoiceNumber',
label: t('globals.params.supplierRef'),
align: 'left',
field: 'invoiceNumber',
sortable: true,
},
{
name: 'shipped',
label: t('globals.shipped'),
align: 'center',
field: 'shipped',
format: toDateHourMin,
sortable: true,
},
{
name: 'quantity',
label: t('item.list.stems'),
align: 'center',
field: 'quantity',
format: (value) => toNumber(value),
sortable: true,
},
{
name: 'total',
label: t('globals.total'),
align: 'center',
field: 'total',
format: (value) => toCurrency(value),
sortable: true,
},
]);
const columns = computed(() => [
{
name: 'itemName',
label: t('globals.item'),
align: 'left',
field: 'itemName',
sortable: true,
},
{
name: 'subName',
align: 'left',
field: 'subName',
sortable: true,
},
{
name: 'quantity',
label: t('globals.quantity'),
align: 'right',
field: 'quantity',
format: (value) => toNumber(value),
sortable: true,
},
{
name: 'price',
label: t('globals.price'),
align: 'right',
field: 'price',
format: (value) => toCurrency(value),
sortable: true,
},
{
name: 'total',
label: t('globals.total'),
align: 'right',
field: 'total',
format: (value) => toCurrency(value),
sortable: true,
},
]);
const store = arrayData.store; const store = arrayData.store;
onUnmounted(() => state.unset('SupplierConsumption')); onUnmounted(() => state.unset('SupplierConsumption'));
@ -100,33 +177,34 @@ const sendCampaignMetricsEmail = ({ address }) => {
}; };
const totalEntryPrice = (rows) => { const totalEntryPrice = (rows) => {
let totalPrice = 0; if (!rows) return [];
let totalQuantity = 0; totalRows.value = rows.reduce(
if (!rows) return totalPrice; (acc, row) => {
for (const row of rows) { if (Array.isArray(row.buys)) {
let total = 0; const { total, quantity } = row.buys.reduce(
let quantity = 0; (buyAcc, buy) => {
buyAcc.total += buy.total || 0;
if (row.buys) { buyAcc.quantity += buy.quantity || 0;
for (const buy of row.buys) { return buyAcc;
total = total + buy.total; },
quantity = quantity + buy.quantity; { total: 0, quantity: 0 },
);
row.total = total;
row.quantity = quantity;
acc.totalPrice += total;
acc.totalQuantity += quantity;
} }
} return acc;
},
row.total = total; { totalPrice: 0, totalQuantity: 0 },
row.quantity = quantity; );
totalPrice = totalPrice + total;
totalQuantity = totalQuantity + quantity;
}
totalRows.value = { totalPrice, totalQuantity };
return rows; return rows;
}; };
onMounted(async () => { onMounted(async () => {
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
await getSupplierConsumptionData(); await getSupplierConsumptionData();
}); });
const expanded = ref([]);
</script> </script>
<template> <template>
@ -160,14 +238,14 @@ onMounted(async () => {
<div> <div>
{{ t('Total entries') }}: {{ t('Total entries') }}:
<QChip :dense="$q.screen.lt.sm" text-color="white"> <QChip :dense="$q.screen.lt.sm" text-color="white">
{{ totalRows.totalPrice }} {{ toCurrency(totalRows.totalPrice) }}
</QChip> </QChip>
</div> </div>
<QSeparator dark vertical /> <QSeparator dark vertical />
<div> <div>
{{ t('Total stems entries') }}: {{ t('Total stems entries') }}:
<QChip :dense="$q.screen.lt.sm" text-color="white"> <QChip :dense="$q.screen.lt.sm" text-color="white">
{{ totalRows.totalQuantity }} {{ toNumber(totalRows.totalQuantity) }}
</QChip> </QChip>
</div> </div>
</div> </div>
@ -177,59 +255,111 @@ onMounted(async () => {
<SupplierConsumptionFilter data-key="SupplierConsumption" /> <SupplierConsumptionFilter data-key="SupplierConsumption" />
</template> </template>
</RightMenu> </RightMenu>
<QTable <QCard class="full-width q-pa-md">
:rows="rows" <QTable
row-key="id" flat
hide-header bordered
class="full-width q-mt-md" :rows="rows"
:no-data-label="t('No results')" :columns="headerColumns"
> row-key="id"
<template #body="{ row }"> v-model:expanded="expanded"
<QTr> :grid="$q.screen.lt.md"
<QTd no-hover> >
<span class="label">{{ t('supplier.consumption.entry') }}: </span> <template #header="props">
<span>{{ row.id }}</span> <QTr :props="props">
</QTd> <QTh auto-width />
<QTd no-hover>
<span class="label">{{ t('globals.date') }}: </span> <QTh v-for="col in props.cols" :key="col.name" :props="props">
<span>{{ toDate(row.shipped) }}</span></QTd <span v-text="col.label" class="tr-header" />
</QTh>
</QTr>
</template>
<template #body="props">
<QTr
:props="props"
:key="`movement_${props.row.id}`"
class="bg-vn-page cursor-pointer"
@click="props.expand = !props.expand"
> >
<QTd colspan="6" no-hover> <QTd auto-width>
<span class="label">{{ t('globals.reference') }}: </span> <QIcon
<span>{{ row.invoiceNumber }}</span> :class="props.expand ? '' : 'rotate-270'"
</QTd> name="expand_circle_down"
</QTr> size="md"
<QTr v-for="(buy, index) in row.buys" :key="index"> :color="props.expand ? 'primary' : 'white'"
<QTd no-hover> />
<QBtn flat class="link" dense no-caps>{{ buy.itemName }}</QBtn> </QTd>
<ItemDescriptorProxy :id="buy.itemFk" />
</QTd>
<QTd no-hover> <QTd v-for="col in props.cols" :key="col.name" :props="props">
<span>{{ buy.subName }}</span> <span @click.stop class="link" v-if="col.name === 'id'">
<FetchedTags :item="buy" /> {{ col.value }}
</QTd> <EntryDescriptorProxy :id="col.value" />
<QTd no-hover> {{ dashIfEmpty(buy.quantity) }}</QTd> </span>
<QTd no-hover> {{ dashIfEmpty(buy.price) }}</QTd>
<QTd colspan="2" no-hover> {{ dashIfEmpty(buy.total) }}</QTd> <span v-else v-text="col.value" />
</QTr> </QTd>
<QTr> </QTr>
<QTd colspan="5" no-hover>
<span class="label">{{ t('Total entry') }}: </span> <QTr
<span>{{ row.total }} </span> v-show="props.expand"
</QTd> :props="props"
<QTd no-hover> :key="`expedition_${props.row.id}`"
<span class="label">{{ t('Total stems') }}: </span> >
<span>{{ row.quantity }}</span> <QTd colspan="12" style="padding: 1px 0">
</QTd> <QTable
</QTr> color="secondary"
</template> card-class="bg-vn-page text-white "
</QTable> style-class="height: 30px"
table-header-class="text-white"
:rows="props.row.buys"
:columns="columns"
row-key="id"
virtual-scroll
v-model:expanded="expanded"
>
<template #header="props">
<QTr :props="props">
<QTh
v-for="col in props.cols"
:key="col.name"
:props="props"
>
<span v-text="col.label" class="tr-header" />
</QTh>
</QTr>
</template>
<template #body="props">
<QTr :props="props" :key="`m_${props.row.id}`">
<QTd
v-for="col in props.cols"
:key="col.name"
:title="col.label"
:props="props"
>
<span
@click.stop
class="link"
v-if="col.name === 'itemName'"
>
{{ col.value }}
<ItemDescriptorProxy :id="props.row.itemFk" />
</span>
<span v-else v-text="col.value" />
</QTd>
</QTr>
</template>
</QTable>
</QTd>
</QTr>
</template>
</QTable>
</QCard>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.label { .q-table thead tr,
color: var(--vn-label-color); .q-table tbody td {
height: 30px;
} }
</style> </style>

View File

@ -8,7 +8,6 @@ import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.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 VnInputDate from 'src/components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
@ -59,11 +58,25 @@ const zoneWhere = computed(() => {
}); });
async function getLanded(params) { async function getLanded(params) {
getDate(`Agencies/getLanded`, params); const data = await getDate(`Agencies/getLanded`, params);
formData.value.landed = data.landed;
const shippedDate = new Date(params.shipped);
const landedDate = new Date(data.hour);
shippedDate.setHours(
landedDate.getHours(),
landedDate.getMinutes(),
landedDate.getSeconds(),
);
formData.value.shipped = shippedDate.toISOString();
} }
async function getShipped(params) { async function getShipped(params) {
getDate(`Agencies/getShipped`, params); const data = await getDate(`Agencies/getShipped`, params);
formData.value.landed = params.landed;
const [hours, minutes, seconds] = data.hour.split(':').map(Number);
let shippedDate = new Date(data.shipped);
shippedDate.setHours(hours, minutes, seconds);
formData.value.shipped = shippedDate.toISOString();
} }
async function getDate(query, params) { async function getDate(query, params) {
@ -75,15 +88,8 @@ async function getDate(query, params) {
if (!data) return notify(t('basicData.noDeliveryZoneAvailable'), 'negative'); if (!data) return notify(t('basicData.noDeliveryZoneAvailable'), 'negative');
formData.value.zoneFk = data.zoneFk; formData.value.zoneFk = data.zoneFk;
formData.value.landed = data.landed;
const shippedDate = new Date(params.shipped); return data;
const landedDate = new Date(data.hour);
shippedDate.setHours(
landedDate.getHours(),
landedDate.getMinutes(),
landedDate.getSeconds(),
);
formData.value.shipped = shippedDate.toISOString();
} }
const onChangeZone = async (zoneId) => { const onChangeZone = async (zoneId) => {
@ -273,8 +279,6 @@ async function getZone(options) {
<VnSelect <VnSelect
:label="t('ticketList.client')" :label="t('ticketList.client')"
v-model="clientId" v-model="clientId"
option-value="id"
option-label="name"
url="Clients" url="Clients"
:fields="['id', 'name']" :fields="['id', 'name']"
sort-by="id" sort-by="id"
@ -309,16 +313,31 @@ async function getZone(options) {
<VnSelect <VnSelect
:label="t('basicData.address')" :label="t('basicData.address')"
v-model="addressId" v-model="addressId"
option-value="id"
option-label="nickname" option-label="nickname"
:options="addresses" :options="addresses"
hide-selected hide-selected
map-options map-options
:required="true" :required="true"
:sort-by="['isActive DESC']"
:rules="validate('basicData.address')" :rules="validate('basicData.address')"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem
v-bind="scope.itemProps"
:class="{ disabled: !scope.opt.isActive }"
>
<QItemSection style="min-width: min-content" avatar>
<QIcon
v-if="
scope.opt.isActive &&
formData.client.defaultAddressFk === scope.opt.id
"
size="sm"
color="grey"
name="star"
class="fill-icon"
/>
</QItemSection>
<QItemSection> <QItemSection>
<QItemLabel <QItemLabel
:class="{ :class="{
@ -344,6 +363,9 @@ async function getZone(options) {
{{ scope.opt?.agencyMode?.name }}</span {{ scope.opt?.agencyMode?.name }}</span
> >
</QItemLabel> </QItemLabel>
<QItemLabel caption>
{{ `#${scope.opt?.id}` }}
</QItemLabel>
</QItemSection> </QItemSection>
</QItem> </QItem>
</template> </template>
@ -425,14 +447,6 @@ async function getZone(options) {
:rules="validate('ticketList.shipped')" :rules="validate('ticketList.shipped')"
@update:model-value="setShipped" @update:model-value="setShipped"
/> />
<VnInputTime
:label="t('basicData.shippedHour')"
v-model="formData.shipped"
:required="true"
:rules="validate('basicData.shippedHour')"
disabled
@update:model-value="setShipped"
/>
<VnInputDate <VnInputDate
:label="t('basicData.landed')" :label="t('basicData.landed')"
v-model="formData.landed" v-model="formData.landed"

View File

@ -20,6 +20,7 @@ export default {
'isFreezed', 'isFreezed',
'isTaxDataChecked', 'isTaxDataChecked',
'hasElectronicInvoice', 'hasElectronicInvoice',
'defaultAddressFk',
'credit', 'credit',
], ],
include: [ include: [

View File

@ -582,7 +582,7 @@ function setReference(data) {
hide-selected hide-selected
required required
@update:model-value="() => onClientSelected(data)" @update:model-value="() => onClientSelected(data)"
:sort-by="'id ASC'" :sort-by="['id ASC']"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -608,7 +608,7 @@ function setReference(data) {
map-options map-options
required required
:disable="!data.clientId" :disable="!data.clientId"
:sort-by="'isActive DESC'" :sort-by="['isActive DESC']"
@update:model-value="() => fetchAvailableAgencies(data)" @update:model-value="() => fetchAvailableAgencies(data)"
> >
<template #option="scope"> <template #option="scope">

View File

@ -13,6 +13,8 @@ export default {
'daysInForward', 'daysInForward',
'availabled', 'availabled',
'awbFk', 'awbFk',
'isDelivered',
'isReceived',
], ],
include: [ include: [
{ {

View File

@ -182,6 +182,7 @@ const columns = computed(() => [
align: 'left', align: 'left',
showValue: false, showValue: false,
sortable: true, sortable: true,
style: 'max-width: 200px;',
}, },
{ {
label: t('globals.packages'), label: t('globals.packages'),
@ -206,6 +207,7 @@ const columns = computed(() => [
align: 'left', align: 'left',
showValue: false, showValue: false,
sortable: true, sortable: true,
style: 'max-width: 75px;',
}, },
{ {
label: t('extraCommunity.physicKg'), label: t('extraCommunity.physicKg'),

View File

@ -408,7 +408,7 @@ const isUnsatisfied = async (reason) => {
const resendEmail = async () => { const resendEmail = async () => {
const params = { const params = {
recipient: worker.value[0]?.user?.emailUser?.email, recipient: worker.value?.user?.emailUser?.email,
week: selectedWeekNumber.value, week: selectedWeekNumber.value,
year: selectedDateYear.value, year: selectedDateYear.value,
workerId: Number(route.params.id), workerId: Number(route.params.id),

View File

@ -33,14 +33,6 @@ const getLocale = (label) => {
</div> </div>
</template> </template>
<template #body="{ params }"> <template #body="{ params }">
<QItem>
<QItemSection>
<VnInput :label="t('FI')" v-model="params.fi" filled
><template #prepend>
<QIcon name="badge" size="xs"></QIcon> </template
></VnInput>
</QItemSection>
</QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInput :label="t('First Name')" v-model="params.firstName" filled /> <VnInput :label="t('First Name')" v-model="params.firstName" filled />
@ -112,7 +104,6 @@ es:
lastName: Apellidos lastName: Apellidos
userName: Usuario userName: Usuario
extension: Extensión extension: Extensión
FI: NIF
First Name: Nombre First Name: Nombre
Last Name: Apellidos Last Name: Apellidos
User Name: Usuario User Name: Usuario

View File

@ -0,0 +1,118 @@
<script setup>
import { computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useAcl } from 'src/composables/useAcl';
import VnTable from 'components/VnTable/VnTable.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import WorkerSummary from './Card/WorkerSummary.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import WorkerManagementFilter from './WorkerManagementFilter.vue';
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const dataKey = 'ManagementList';
const columns = computed(() => [
{
align: 'left',
label: t('tableColumns.firstName'),
name: 'firstName',
isTitle: true,
columnFilter: {
name: 'firstName',
},
},
{
align: 'left',
label: t('tableColumns.lastName'),
name: 'lastName',
columnFilter: {
name: 'lastName',
},
},
{
align: 'left',
label: t('management.NIF'),
name: 'fi',
columnFilter: {
name: 'fi',
},
cardVisible: true,
},
{
align: 'left',
label: t('tableColumns.email'),
name: 'email',
columnFilter: {
name: 'email',
},
cardVisible: true,
},
{
align: 'left',
label: t('management.ssn'),
labelAbbreviation: t('management.ssn'),
toolTip: t('management.completeSsn'),
name: 'SSN',
columnFilter: {
name: 'SSN',
},
cardVisible: true,
},
{
align: 'left',
name: 'departmentFk',
label: t('tableColumns.department'),
columnFilter: {
component: 'select',
name: 'departmentFk',
attrs: {
url: 'Departments',
},
},
cardVisible: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.department),
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('globals.pageTitles.summary'),
icon: 'preview',
action: (row) => viewSummary(row?.id, WorkerSummary),
isPrimary: true,
},
],
},
]);
onMounted(() => {
const canAccess = useAcl().hasAcl('Worker', 'management', 'WRITE');
});
</script>
<template>
<VnSearchbar
:data-key
url="Workers/filter"
:label="t('management.search')"
:info="t('management.searchInfo')"
/>
<RightMenu>
<template #right-panel>
<WorkerManagementFilter :data-key />
</template>
</RightMenu>
<VnTable
ref="tableRef"
url="Workers/filter"
:columns="columns"
:data-key
:right-search="false"
order="id DESC"
/>
</template>

View File

@ -0,0 +1,99 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const { t, te } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const departments = ref();
const getLocale = (label) => {
const globalLocale = `globals.params.${label}`;
return te(globalLocale) ? t(globalLocale) : t(`management.${label}`);
};
</script>
<template>
<FetchData url="Departments" @on-fetch="(data) => (departments = data)" auto-load />
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ getLocale(tag.label) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params }">
<QItem>
<QItemSection>
<VnInput
:label="t('tableColumns.firstName')"
v-model="params.firstName"
filled
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('tableColumns.lastName')"
v-model="params.lastName"
filled
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('management.NIF')"
v-model="params.fi"
filled
data-cy="worker-filter-fi"
>
<template #prepend>
<QIcon name="badge" size="xs" />
</template>
</VnInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('tableColumns.email')"
v-model="params.email"
filled
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput :label="t('management.ssn')" v-model="params.SSN" filled />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
:label="t('tableColumns.department')"
v-model="params.departmentFk"
:options="departments"
option-value="id"
option-label="name"
emit-value
map-options
dense
filled
/>
</QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>

View File

@ -13,3 +13,11 @@ tableColumns:
SSN: SSN SSN: SSN
extension: Extension extension: Extension
queue: Queue queue: Queue
management:
search: Search worker
searchInfo: You can search by NIF, NSS or worker name
NIF: NIF
ssn: SSN
completeSsn: National Insurance Number
firstName: First Name
lastName: Last Name

View File

@ -16,3 +16,11 @@ tableColumns:
SSN: NSS SSN: NSS
extension: Extensión extension: Extensión
queue: Cola queue: Cola
management:
search: Buscar trabajador
searchInfo: Puedes buscar por NIF, NSS o nombre del trabajador
NIF: NIF
ssn: Núm. SS
completeSsn: Número de Seguridad Social
firstName: Nombre
lastName: Apellidos

View File

@ -89,7 +89,7 @@ watch(
v-model="formData.geoFk" v-model="formData.geoFk"
url="Postcodes/location" url="Postcodes/location"
:fields="['geoFk', 'code', 'townFk', 'countryFk']" :fields="['geoFk', 'code', 'townFk', 'countryFk']"
:sort-by="'code ASC'" :sort-by="['code ASC']"
option-value="geoFk" option-value="geoFk"
option-label="code" option-label="code"
:filter-options="['code']" :filter-options="['code']"

View File

@ -8,9 +8,9 @@ const claimCard = {
meta: { meta: {
menu: [ menu: [
'ClaimBasicData', 'ClaimBasicData',
'ClaimNotes',
'ClaimLines', 'ClaimLines',
'ClaimPhotos', 'ClaimPhotos',
'ClaimNotes',
'ClaimDevelopment', 'ClaimDevelopment',
'ClaimAction', 'ClaimAction',
'ClaimLog', 'ClaimLog',
@ -36,6 +36,15 @@ const claimCard = {
}, },
component: () => import('src/pages/Claim/Card/ClaimBasicData.vue'), component: () => import('src/pages/Claim/Card/ClaimBasicData.vue'),
}, },
{
path: 'notes',
name: 'ClaimNotes',
meta: {
title: 'notes',
icon: 'draft',
},
component: () => import('src/pages/Claim/Card/ClaimNotes.vue'),
},
{ {
path: 'lines', path: 'lines',
name: 'ClaimLines', name: 'ClaimLines',
@ -54,15 +63,6 @@ const claimCard = {
}, },
component: () => import('src/pages/Claim/Card/ClaimPhoto.vue'), component: () => import('src/pages/Claim/Card/ClaimPhoto.vue'),
}, },
{
path: 'notes',
name: 'ClaimNotes',
meta: {
title: 'notes',
icon: 'draft',
},
component: () => import('src/pages/Claim/Card/ClaimNotes.vue'),
},
{ {
path: 'development', path: 'development',
name: 'ClaimDevelopment', name: 'ClaimDevelopment',

View File

@ -225,7 +225,7 @@ const customerCard = {
name: 'CustomerCreditContractsInsurance', name: 'CustomerCreditContractsInsurance',
component: () => component: () =>
import( import(
'src/pages/Customer/components/CustomerCreditContractsInsurance.vue' 'src/pages/Customer/Card/CustomerCreditContractsInsurance.vue'
), ),
}, },
], ],

View File

@ -239,7 +239,7 @@ export default {
icon: 'vn:worker', icon: 'vn:worker',
moduleName: 'Worker', moduleName: 'Worker',
keyBinding: 'w', keyBinding: 'w',
menu: ['WorkerList', 'WorkerDepartment'], menu: ['WorkerList', 'WorkerDepartment', 'WorkerManagement'],
}, },
component: RouterView, component: RouterView,
redirect: { name: 'WorkerMain' }, redirect: { name: 'WorkerMain' },
@ -283,6 +283,22 @@ export default {
departmentCard, departmentCard,
], ],
}, },
{
path: 'management',
name: 'WorkerManagement',
meta: {
title: 'management',
icon: 'view_list',
acls: [
{
model: 'Worker',
props: 'isRoleAdvanced',
accessType: 'READ',
},
],
},
component: () => import('src/pages/Worker/WorkerManagement.vue'),
},
], ],
}, },
], ],

View File

@ -0,0 +1,25 @@
/// <reference types="cypress" />
describe('ClaimLines', () => {
const claimId = 1;
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit(`/#/claim/${claimId}/lines`);
});
it('should add new line', () => {
cy.get('.q-page-sticky > div > .q-btn').click();
cy.get('.q-card > .q-table__container > .q-table__middle > .q-table > thead > tr > .q-table--col-auto-width > .q-checkbox > .q-checkbox__inner > .q-checkbox__bg').click();
cy.get('.q-card__actions > .q-btn--unelevated').click();
cy.get('.q-notification__message').should('have.text', 'Lines added to claim');
})
it('should autofill claimed quantity if 0 or null', () => {
cy.get('thead > tr > .q-table--col-auto-width > .q-checkbox > .q-checkbox__inner > .q-checkbox__bg').click();
cy.get('.q-btn--unelevated').click();
cy.checkNotification('Quantities filled automatically');
cy.get('.q-btn--unelevated').click();
cy.checkNotification('No quantities to fill');
})
});

View File

@ -1,5 +1,4 @@
describe('ClaimNotes', () => { describe('ClaimNotes', () => {
const saveBtn = '.q-field__append > .q-btn > .q-btn__content > .q-icon';
const firstNote = '.q-infinite-scroll :nth-child(1) > .q-card__section--vert'; const firstNote = '.q-infinite-scroll :nth-child(1) > .q-card__section--vert';
beforeEach(() => { beforeEach(() => {
cy.login('developer'); cy.login('developer');
@ -11,9 +10,9 @@ describe('ClaimNotes', () => {
cy.get('.q-textarea') cy.get('.q-textarea')
.should('be.visible') .should('be.visible')
.should('not.be.disabled') .should('not.be.disabled')
.type(message); .type(message, "{{enter}}");
cy.get(saveBtn).click(); cy.dataCy("saveContinueNoteButton").click();
cy.get(firstNote).should('have.text', message); cy.get(firstNote).should('have.text', message);
}); });
}); });

View File

@ -7,7 +7,28 @@ describe('Client credit contracts', () => {
timeout: 5000, timeout: 5000,
}); });
}); });
it('Should load layout', () => { it('Should add a new contract and an additional credit', () => {
cy.get('.q-page').should('be.visible'); cy.dataCy('createBtn').click();
cy.dataCy('Credit_input').type(123);
cy.dataCy('Grade_input').type(9);
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data created');
cy.dataCy('createBtn').should('not.exist');
});
it('Should add an additional credit', () => {
cy.dataCy('viewBtn').eq(0).click();
cy.get('.q-page-sticky > div').click();
cy.dataCy('Credit_input').type(321);
cy.dataCy('Grade_input').type(89);
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data created');
});
it('Should close a contract', () => {
cy.dataCy('closeBtn').eq(0).click();
cy.get('.q-btn--unelevated').click();
cy.checkNotification('Data saved');
cy.dataCy('createBtn').should('exist');
}); });
}); });

View File

@ -53,10 +53,10 @@ describe('InvoiceInDescriptor', () => {
expect(response.statusCode).to.equal(200); expect(response.statusCode).to.equal(200);
}); });
}); });
// https://redmine.verdnatura.es/issues/8767
it.skip('should download the file properly', () => { it('should download the file properly', () => {
cy.visit('/#/invoice-in/1/summary'); cy.visit('/#/invoice-in/1/summary');
cy.validateDownload(() => cy.selectDescriptorOption(5)); cy.selectDescriptorOption(5);
}); });
}); });
@ -84,7 +84,7 @@ describe('InvoiceInDescriptor', () => {
beforeEach(() => cy.visit(`/#/invoice-in/${originalId}/summary`)); beforeEach(() => cy.visit(`/#/invoice-in/${originalId}/summary`));
it.skip('should create a correcting invoice and redirect to original invoice', () => { it('should create a correcting invoice and redirect to original invoice', () => {
createCorrective(); createCorrective();
redirect(originalId); redirect(originalId);
}); });

View File

@ -1,5 +1,5 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe('OrderCatalog', () => { describe.skip('OrderCatalog', () => {
beforeEach(() => { beforeEach(() => {
cy.login('developer'); cy.login('developer');
cy.viewport(1920, 1080); cy.viewport(1920, 1080);

View File

@ -2,6 +2,7 @@
describe('TicketList', () => { describe('TicketList', () => {
beforeEach(() => { beforeEach(() => {
cy.login('developer'); cy.login('developer');
cy.viewport(1920, 1080);
cy.visit('/#/ticket/list', false); cy.visit('/#/ticket/list', false);
}); });
@ -76,7 +77,7 @@ describe('TicketList', () => {
}); });
}).as('ticket'); }).as('ticket');
cy.selectOption('[data-cy="Warehouse_select"]', 'Warehouse Five'); cy.selectOption('[data-cy="Warehouse_select"]', 'Warehouse One');
cy.searchBtnFilterPanel(); cy.searchBtnFilterPanel();
cy.wait('@ticket').then((interception) => { cy.wait('@ticket').then((interception) => {
const data = interception.response.body[0]; const data = interception.response.body[0];

View File

@ -0,0 +1,23 @@
describe('WorkerManagement', () => {
const nif = '12091201A';
const searchButton = '.q-scrollarea__content > .q-btn--standard > .q-btn__content';
const url = '/#/worker/management';
it('should not enter the section', () => {
cy.login('salesPerson');
cy.visit(url);
cy.url().should('include', '/dashboard');
});
it('should enter the section and filter by NIF, then open the worker summary popup', () => {
cy.login('hr');
cy.visit(url);
cy.dataCy('worker-filter-fi').type(nif);
cy.get(searchButton).click();
cy.dataCy('tableAction-0').click();
cy.get('.summaryHeader').should('exist');
cy.get('.summaryHeader').should('be.visible');
cy.get('.summaryBody').should('exist');
cy.get('.summaryBody').should('be.visible');
});
});

View File

@ -194,9 +194,7 @@ Cypress.Commands.add('fillInForm', (obj, opts = {}) => {
cy.selectOption(el, val); cy.selectOption(el, val);
break; break;
case 'date': case 'date':
cy.get(el).type( cy.get(el).type(`{selectall}{backspace}${val}`).blur();
`{selectall}{backspace}${val}`,
).blur();
break; break;
case 'time': case 'time':
cy.get(el).click(); cy.get(el).click();
@ -571,24 +569,6 @@ Cypress.Commands.add('validateCheckbox', (selector, expectedVal = 'true') => {
cy.get(selector).should('have.attr', 'aria-checked', expectedVal.toString()); cy.get(selector).should('have.attr', 'aria-checked', expectedVal.toString());
}); });
Cypress.Commands.add('validateDownload', (trigger, opts = {}) => {
const {
url = /api\/dms\/\d+\/downloadFile\?access_token=.+/,
types = ['text/plain', 'image/jpeg'],
alias = 'download',
} = opts;
cy.intercept('GET', url).as(alias);
trigger().then(() => {
cy.wait(`@${alias}`).then(({ response }) => {
expect(response.statusCode).to.equal(200);
const isValidType = types.some((type) =>
response.headers['content-type'].includes(type),
);
expect(isValidType).to.be.true;
});
});
});
Cypress.Commands.add('validatePdfDownload', (match, trigger) => { Cypress.Commands.add('validatePdfDownload', (match, trigger) => {
cy.window().then((win) => { cy.window().then((win) => {
cy.stub(win, 'open') cy.stub(win, 'open')