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

This commit is contained in:
Jose Antonio Tubau 2025-02-28 10:36:58 +01:00
commit 0ec0dcdd9a
126 changed files with 1676 additions and 749 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
node_modules

93
Jenkinsfile vendored
View File

@ -1,6 +1,7 @@
#!/usr/bin/env groovy
def PROTECTED_BRANCH
def IS_LATEST
def BRANCH_ENV = [
test: 'test',
@ -10,15 +11,17 @@ def BRANCH_ENV = [
node {
stage('Setup') {
env.FRONT_REPLICAS = 1
env.NODE_ENV = BRANCH_ENV[env.BRANCH_NAME] ?: 'dev'
PROTECTED_BRANCH = [
'dev',
'test',
'master',
'main',
'beta'
].contains(env.BRANCH_NAME)
]
IS_PROTECTED_BRANCH = PROTECTED_BRANCH.contains(env.BRANCH_NAME)
IS_LATEST = ['master', 'main'].contains(env.BRANCH_NAME)
// https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
echo "NODE_NAME: ${env.NODE_NAME}"
@ -33,7 +36,7 @@ node {
props.each {key, value -> echo "${key}: ${value}" }
}
if (PROTECTED_BRANCH) {
if (IS_PROTECTED_BRANCH) {
configFileProvider([
configFile(fileId: "salix-front.branch.${env.BRANCH_NAME}",
variable: 'BRANCH_PROPS_FILE')
@ -58,6 +61,19 @@ pipeline {
PROJECT_NAME = 'lilium'
}
stages {
stage('Version') {
when {
expression { IS_PROTECTED_BRANCH }
}
steps {
script {
def packageJson = readJSON file: 'package.json'
def version = "${packageJson.version}-build${env.BUILD_ID}"
writeFile(file: 'VERSION.txt', text: version)
echo "VERSION: ${version}"
}
}
}
stage('Install') {
environment {
NODE_ENV = ""
@ -68,48 +84,85 @@ pipeline {
}
stage('Test') {
when {
expression { !PROTECTED_BRANCH }
expression { !IS_PROTECTED_BRANCH }
}
environment {
NODE_ENV = ""
NODE_ENV = ''
CI = 'true'
TZ = 'Europe/Madrid'
}
parallel {
stage('Unit') {
steps {
sh 'pnpm run test:unit:ci'
}
post {
always {
junit(
testResults: 'junitresults.xml',
testResults: 'junit/vitest.xml',
allowEmptyResults: true
)
}
}
}
stage('Build') {
when {
expression { PROTECTED_BRANCH }
}
stage('E2E') {
environment {
CREDENTIALS = credentials('docker-registry')
COMPOSE_PROJECT = "${PROJECT_NAME}-${env.BUILD_ID}"
COMPOSE_PARAMS = "-p ${env.COMPOSE_PROJECT} -f test/cypress/docker-compose.yml --project-directory ."
}
steps {
sh 'quasar build'
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
sh 'rm junit/e2e-*.xml || true'
env.COMPOSE_TAG = PROTECTED_BRANCH.contains(env.CHANGE_TARGET) ? env.CHANGE_TARGET : 'dev'
def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs')
sh "docker-compose ${env.COMPOSE_PARAMS} up -d"
image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ") {
sh 'cypress run --browser chromium || true'
}
}
}
post {
always {
sh "docker-compose ${env.COMPOSE_PARAMS} down -v"
junit(
testResults: 'junit/e2e-*.xml',
allowEmptyResults: true
)
}
}
}
}
}
stage('Build') {
when {
expression { IS_PROTECTED_BRANCH }
}
environment {
VERSION = readFile 'VERSION.txt'
}
steps {
script {
sh 'quasar build'
def baseImage = "salix-frontend:${env.VERSION}"
def image = docker.build(baseImage, ".")
docker.withRegistry("https://${env.REGISTRY}", 'docker-registry') {
image.push()
image.push(env.BRANCH_NAME)
if (IS_LATEST) image.push('latest')
}
}
dockerBuild()
}
}
stage('Deploy') {
when {
expression { PROTECTED_BRANCH }
expression { IS_PROTECTED_BRANCH }
}
environment {
VERSION = readFile 'VERSION.txt'
}
steps {
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
}
withKubeConfig([
serverUrl: "$KUBERNETES_API",
credentialsId: 'kubernetes',

View File

@ -1,12 +1,37 @@
import { defineConfig } from 'cypress';
// https://docs.cypress.io/app/tooling/reporters
// https://docs.cypress.io/app/references/configuration
// https://www.npmjs.com/package/cypress-mochawesome-reporter
let urlHost, reporter, reporterOptions;
if (process.env.CI) {
urlHost = 'front';
reporter = 'junit';
reporterOptions = {
mochaFile: 'junit/e2e-[hash].xml',
toConsole: false,
};
} else {
urlHost = 'localhost';
reporter = 'cypress-mochawesome-reporter';
reporterOptions = {
charts: true,
reportPageTitle: 'Cypress Inline Reporter',
reportFilename: '[status]_[datetime]-report',
embeddedScreenshots: true,
reportDir: 'test/cypress/reports',
inlineAssets: true,
};
}
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:9000/',
experimentalStudio: true,
baseUrl: `http://${urlHost}:9000`,
experimentalStudio: false,
defaultCommandTimeout: 10000,
trashAssetsBeforeRuns: false,
requestTimeout: 10000,
responseTimeout: 30000,
pageLoadTimeout: 60000,
defaultBrowser: 'chromium',
fixturesFolder: 'test/cypress/fixtures',
screenshotsFolder: 'test/cypress/screenshots',
supportFile: 'test/cypress/support/index.js',
@ -14,29 +39,19 @@ export default defineConfig({
downloadsFolder: 'test/cypress/downloads',
video: false,
specPattern: 'test/cypress/integration/**/*.spec.js',
experimentalRunAllSpecs: false,
watchForFileChanges: false,
reporter: 'cypress-mochawesome-reporter',
reporterOptions: {
charts: true,
reportPageTitle: 'Cypress Inline Reporter',
reportFilename: '[status]_[datetime]-report',
embeddedScreenshots: true,
reportDir: 'test/cypress/reports',
inlineAssets: true,
},
experimentalRunAllSpecs: true,
watchForFileChanges: true,
reporter,
reporterOptions,
component: {
componentFolder: 'src',
testFiles: '**/*.spec.js',
supportFile: 'test/cypress/support/unit.js',
},
setupNodeEvents: async (on, config) => {
const plugin = await import('cypress-mochawesome-reporter/plugin');
plugin.default(on);
return config;
},
viewportWidth: 1280,
viewportHeight: 720,
},
experimentalMemoryManagement: true,
defaultCommandTimeout: 10000,
numTestsKeptInMemory: 2,
});

View File

@ -1,7 +0,0 @@
version: '3.7'
services:
main:
image: registry.verdnatura.es/salix-frontend:${VERSION:?}
build:
context: .
dockerfile: ./Dockerfile

45
docs/Dockerfile.dev Normal file
View File

@ -0,0 +1,45 @@
FROM debian:12.9-slim
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gnupg2 \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& npm install -g corepack@0.31.0 \
&& corepack enable pnpm \
&& rm -rf /var/lib/apt/lists/*
RUN apt-get update \
&& apt-get -y --no-install-recommends install \
apt-utils \
chromium \
libasound2 \
libgbm-dev \
libgtk-3-0 \
libgtk2.0-0 \
libnotify-dev \
libnss3 \
libxss1 \
libxtst6 \
xauth \
xvfb \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd -r -g 1000 app \
&& useradd -r -u 1000 -g app -m -d /home/app app
USER app
ENV SHELL=bash
ENV PNPM_HOME="/home/app/.local/share/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN pnpm setup \
&& pnpm install --global cypress@13.6.6 \
&& cypress install
WORKDIR /app

View File

@ -1,6 +1,6 @@
export default [
{
path: '/api',
rule: { target: 'http://0.0.0.0:3000' },
rule: { target: 'http://127.0.0.1:3000' },
},
];

View File

@ -11,6 +11,7 @@
import { configure } from 'quasar/wrappers';
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
import path from 'path';
const target = `http://${process.env.CI ? 'back' : 'localhost'}:3000`;
export default configure(function (/* ctx */) {
return {
@ -108,13 +109,17 @@ export default configure(function (/* ctx */) {
},
proxy: {
'/api': {
target: 'http://0.0.0.0:3000',
target: target,
logLevel: 'debug',
changeOrigin: true,
secure: false,
},
},
open: false,
allowedHosts: [
'front', // Agrega este nombre de host
'localhost', // Opcional, para pruebas locales
],
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework

View File

@ -12,6 +12,7 @@ import SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue';
import { tMobile } from 'src/composables/tMobile';
import { useArrayData } from 'src/composables/useArrayData';
import { getDifferences, getUpdatedValues } from 'src/filters';
const { push } = useRouter();
const quasar = useQuasar();
@ -95,6 +96,10 @@ const $props = defineProps({
type: [String, Boolean],
default: '800px',
},
onDataSaved: {
type: Function,
default: () => {},
},
});
const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed(
@ -284,7 +289,12 @@ function trimData(data) {
}
return data;
}
function onBeforeSave(formData, originalData) {
return getUpdatedValues(
Object.keys(getDifferences(formData, originalData)),
formData,
);
}
async function onKeyup(evt) {
if (evt.key === 'Enter' && !('prevent-submit' in attrs)) {
const input = evt.target;
@ -321,6 +331,7 @@ defineExpose({
class="q-pa-md"
:style="maxWidth ? 'max-width: ' + maxWidth : ''"
id="formModel"
:mapper="onBeforeSave"
>
<QCard>
<slot

View File

@ -1,12 +1,13 @@
<script setup>
import { ref, computed } from 'vue';
import { ref, computed, useAttrs, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState';
import FormModel from 'components/FormModel.vue';
const emit = defineEmits(['onDataSaved', 'onDataCanceled']);
defineProps({
const props = defineProps({
title: {
type: String,
default: '',
@ -22,12 +23,21 @@ defineProps({
});
const { t } = useI18n();
const attrs = useAttrs();
const state = useState();
const formModelRef = ref(null);
const closeButton = ref(null);
const isSaveAndContinue = ref(false);
const onDataSaved = (formData, requestResponse) => {
if (closeButton.value && !isSaveAndContinue.value) closeButton.value.click();
const isSaveAndContinue = ref(props.showSaveAndContinueBtn);
const isLoading = computed(() => formModelRef.value?.isLoading);
const reset = computed(() => formModelRef.value?.reset);
const onDataSaved = async (formData, requestResponse) => {
if (!isSaveAndContinue.value) closeButton.value?.click();
if (isSaveAndContinue.value) {
await nextTick();
state.set(attrs.model, attrs.formInitialData);
}
isSaveAndContinue.value = props.showSaveAndContinueBtn;
emit('onDataSaved', formData, requestResponse);
};
@ -36,9 +46,6 @@ const onClick = async (saveAndContinue) => {
await formModelRef.value.save();
};
const isLoading = computed(() => formModelRef.value?.isLoading);
const reset = computed(() => formModelRef.value?.reset);
defineExpose({
isLoading,
onDataSaved,
@ -74,10 +81,7 @@ defineExpose({
data-cy="FormModelPopup_cancel"
v-close-popup
z-max
@click="
isSaveAndContinue = false;
emit('onDataCanceled');
"
@click="emit('onDataCanceled')"
/>
<QBtn
:flat="showSaveAndContinueBtn"

View File

@ -85,7 +85,15 @@ const refresh = () => window.location.reload();
</QTooltip>
<PinnedModules ref="pinnedModulesRef" />
</QBtn>
<QBtn class="q-pa-none" rounded dense flat no-wrap id="user">
<QBtn
class="q-pa-none"
rounded
dense
flat
no-wrap
id="user"
data-cy="userPanel_btn"
>
<VnAvatar
:worker-id="user.id"
:title="user.name"

View File

@ -91,7 +91,6 @@ const components = {
event: updateEvent,
attrs: {
...defaultAttrs,
style: 'min-width: 150px',
},
forceAttrs,
},
@ -152,7 +151,7 @@ const onTabPressed = async () => {
};
</script>
<template>
<div v-if="showFilter" class="full-width flex-center" style="overflow: hidden">
<div v-if="showFilter" class="full-width" style="overflow: hidden">
<VnColumn
:column="$props.column"
default="input"

View File

@ -23,6 +23,10 @@ const $props = defineProps({
type: Boolean,
default: false,
},
align: {
type: String,
default: 'end',
},
});
const hover = ref();
const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
@ -46,16 +50,27 @@ async function orderBy(name, direction) {
}
defineExpose({ orderBy });
function textAlignToFlex(textAlign) {
return `justify-content: ${
{
'text-center': 'center',
'text-left': 'start',
'text-right': 'end',
}[textAlign] || 'start'
};`;
}
</script>
<template>
<div
@mouseenter="hover = true"
@mouseleave="hover = false"
@click="orderBy(name, model?.direction)"
class="row items-center no-wrap cursor-pointer title"
class="items-center no-wrap cursor-pointer title"
:style="textAlignToFlex(align)"
>
<span :title="label">{{ label }}</span>
<sup v-if="name && model?.index">
<div v-if="name && model?.index">
<QChip
:label="!vertical ? model?.index : ''"
:icon="
@ -92,20 +107,16 @@ defineExpose({ orderBy });
/>
</div>
</QChip>
</sup>
</div>
</div>
</template>
<style lang="scss" scoped>
.title {
display: flex;
justify-content: center;
align-items: center;
height: 30px;
width: 100%;
color: var(--vn-label-color);
}
sup {
vertical-align: super; /* Valor predeterminado */
/* También puedes usar otros valores como "baseline", "top", "text-top", etc. */
white-space: nowrap;
}
</style>

View File

@ -31,6 +31,7 @@ import VnLv from 'components/ui/VnLv.vue';
import VnTableOrder from 'src/components/VnTable/VnOrder.vue';
import VnTableFilter from './VnTableFilter.vue';
import { getColAlign } from 'src/composables/getColAlign';
import RightMenu from '../common/RightMenu.vue';
const arrayData = useArrayData(useAttrs()['data-key']);
const $props = defineProps({
@ -50,10 +51,6 @@ const $props = defineProps({
type: Boolean,
default: true,
},
rightSearchIcon: {
type: Boolean,
default: true,
},
rowClick: {
type: [Function, Boolean],
default: null,
@ -137,6 +134,10 @@ const $props = defineProps({
createComplement: {
type: Object,
},
dataCy: {
type: String,
default: 'vn-table',
},
});
const { t } = useI18n();
@ -252,7 +253,9 @@ function splitColumns(columns) {
col.columnFilter = { inWhere: true, ...col.columnFilter };
splittedColumns.value.columns.push(col);
}
// Status column
splittedColumns.value.create = createOrderSort(splittedColumns.value.create);
if (splittedColumns.value.chips.length) {
splittedColumns.value.columnChips = splittedColumns.value.chips.filter(
(c) => !c.isId,
@ -268,6 +271,24 @@ function splitColumns(columns) {
}
}
function createOrderSort(columns) {
const orderedColumn = columns
.map((column, index) =>
column.createOrder !== undefined ? { ...column, originalIndex: index } : null,
)
.filter((item) => item !== null);
orderedColumn.sort((a, b) => a.createOrder - b.createOrder);
const filteredColumns = columns.filter((col) => col.createOrder === undefined);
orderedColumn.forEach((col) => {
filteredColumns.splice(col.createOrder, 0, col);
});
return filteredColumns;
}
const rowClickFunction = computed(() => {
if ($props.rowClick != undefined) return $props.rowClick;
if ($props.redirect) return ({ id }) => redirectFn(id);
@ -313,8 +334,14 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
if (evt?.shiftKey && added) {
const rowIndex = selectedRows[0].$index;
const selectedIndexes = new Set(selected.value.map((row) => row.$index));
for (const row of rows) {
if (row.$index == rowIndex) break;
const minIndex = selectedIndexes.size
? Math.min(...selectedIndexes, rowIndex)
: 0;
const maxIndex = Math.max(...selectedIndexes, rowIndex);
for (let i = minIndex; i <= maxIndex; i++) {
const row = rows[i];
if (row.$index == rowIndex) continue;
if (!selectedIndexes.has(row.$index)) {
selected.value.push(row);
selectedIndexes.add(row.$index);
@ -337,12 +364,11 @@ function hasEditableFormat(column) {
const clickHandler = async (event) => {
const clickedElement = event.target.closest('td');
const isDateElement = event.target.closest('.q-date');
const isTimeElement = event.target.closest('.q-time');
const isQselectDropDown = event.target.closest('.q-select__dropdown-icon');
const isQSelectDropDown = event.target.closest('.q-select__dropdown-icon');
if (isDateElement || isTimeElement || isQselectDropDown) return;
if (isDateElement || isTimeElement || isQSelectDropDown) return;
if (clickedElement === null) {
await destroyInput(editingRow.value, editingField.value);
@ -413,20 +439,13 @@ async function renderInput(rowId, field, clickedElement) {
eventHandlers: {
'update:modelValue': async (value) => {
if (isSelect && value) {
row[column.name] = value[column.attrs?.optionValue ?? 'id'];
row[column?.name + 'TextValue'] =
value[column.attrs?.optionLabel ?? 'name'];
await column?.cellEvent?.['update:modelValue']?.(
value,
oldValue,
row,
);
await updateSelectValue(value, column, row, oldValue);
} else row[column.name] = value;
await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row);
},
keyup: async (event) => {
if (event.key === 'Enter')
await destroyInput(rowIndex, field, clickedElement);
await destroyInput(rowId, field, clickedElement);
},
keydown: async (event) => {
switch (event.key) {
@ -457,6 +476,17 @@ async function renderInput(rowId, field, clickedElement) {
node.el?.querySelector('span > div > div').focus();
}
async function updateSelectValue(value, column, row, oldValue) {
row[column.name] = value[column.attrs?.optionValue ?? 'id'];
row[column?.name + 'VnTableTextValue'] = value[column.attrs?.optionLabel ?? 'name'];
if (column?.attrs?.find?.label)
row[column?.attrs?.find?.label] = value[column.attrs?.optionLabel ?? 'name'];
await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row);
}
async function destroyInput(rowIndex, field, clickedElement) {
if (!clickedElement)
clickedElement = document.querySelector(
@ -519,9 +549,9 @@ function getToggleIcon(value) {
}
function formatColumnValue(col, row, dashIfEmpty) {
if (col?.format) {
if (selectRegex.test(col?.component) && row[col?.name + 'TextValue']) {
return dashIfEmpty(row[col?.name + 'TextValue']);
if (col?.format || row[col?.name + 'VnTableTextValue']) {
if (selectRegex.test(col?.component) && row[col?.name + 'VnTableTextValue']) {
return dashIfEmpty(row[col?.name + 'VnTableTextValue']);
} else {
return col.format(row, dashIfEmpty);
}
@ -551,23 +581,51 @@ function formatColumnValue(col, row, dashIfEmpty) {
return dashIfEmpty(row[urlRelation][col?.attrs.optionLabel ?? 'name']);
}
if (typeof row[urlRelation] == 'string') return dashIfEmpty(row[urlRelation]);
} else {
}
return dashIfEmpty(row[col?.name]);
}
}
function cardClick(_, row) {
if ($props.redirect) router.push({ path: `/${$props.redirect}/${row.id}` });
}
function removeTextValue(data, getChanges) {
let changes = data.updates;
if (!changes) return data;
for (const change of changes) {
for (const key in change.data) {
if (key.endsWith('VnTableTextValue')) {
delete change.data[key];
}
}
}
data.updates = changes.filter((change) => Object.keys(change.data).length > 0);
if ($attrs?.beforeSaveFn) data = $attrs.beforeSaveFn(data, getChanges);
return data;
}
function handleRowClick(event, row) {
if (event.ctrlKey) return rowCtrlClickFunction.value(event, row);
if (rowClickFunction.value) rowClickFunction.value(row);
}
const rowCtrlClickFunction = computed(() => {
if ($props.rowCtrlClick != undefined) return $props.rowCtrlClick;
if ($props.redirect)
return (evt, { id }) => {
stopEventPropagation(evt);
window.open(`/#/${$props.redirect}/${id}`, '_blank');
};
return () => {};
});
</script>
<template>
<QDrawer
v-if="$props.rightSearch"
v-model="stateStore.rightDrawer"
side="right"
:width="256"
:overlay="$props.overlay"
>
<QScrollArea class="fit">
<RightMenu v-if="$props.rightSearch" :overlay="overlay">
<template #right-panel>
<VnTableFilter
:data-key="$attrs['data-key']"
:columns="columns"
@ -581,8 +639,8 @@ function cardClick(_, row) {
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template>
</VnTableFilter>
</QScrollArea>
</QDrawer>
</template>
</RightMenu>
<CrudModel
v-bind="$attrs"
:class="$attrs['class'] ?? 'q-px-md'"
@ -591,6 +649,7 @@ function cardClick(_, row) {
@on-fetch="(...args) => emit('onFetch', ...args)"
:search-url="searchUrl"
:disable-infinite-scroll="isTableMode"
:before-save-fn="removeTextValue"
@save-changes="reload"
:has-sub-toolbar="$props.hasSubToolbar ?? isEditable"
:auto-load="hasParams || $attrs['auto-load']"
@ -618,7 +677,7 @@ function cardClick(_, row) {
:style="isTableMode && `max-height: ${tableHeight}`"
:virtual-scroll="isTableMode"
@virtual-scroll="handleScroll"
@row-click="(_, row) => rowClickFunction && rowClickFunction(row)"
@row-click="(event, row) => handleRowClick(event, row)"
@update:selected="emit('update:selected', $event)"
@selection="(details) => handleSelection(details, rows)"
:hide-selected-banner="true"
@ -635,6 +694,7 @@ function cardClick(_, row) {
:skip="columnsVisibilitySkipped"
/>
<QBtnToggle
v-if="!tableModes.some((mode) => mode.disable)"
v-model="mode"
toggle-color="primary"
class="bg-vn-section-color"
@ -648,15 +708,14 @@ function cardClick(_, row) {
v-bind:class="col.headerClass"
class="body-cell"
:style="col?.width ? `max-width: ${col?.width}` : ''"
style="padding: inherit"
>
<div
class="no-padding"
:style="
withFilters && $props.columnSearch ? 'height: 75px' : ''
"
:style="[
withFilters && $props.columnSearch ? 'height: 75px' : '',
]"
>
<div class="text-center" style="height: 30px">
<div style="height: 30px">
<QTooltip v-if="col.toolTip">{{ col.toolTip }}</QTooltip>
<VnTableOrder
v-model="orders[col.orderBy ?? col.name]"
@ -664,6 +723,7 @@ function cardClick(_, row) {
:label="col?.labelAbbreviation ?? col?.label"
:data-key="$attrs['data-key']"
:search-url="searchUrl"
:align="getColAlign(col)"
/>
</div>
<VnFilter
@ -954,14 +1014,6 @@ function cardClick(_, row) {
transition-show="scale"
transition-hide="scale"
:full-width="createComplement?.isFullWidth ?? false"
@before-hide="
() => {
if (createRef.isSaveAndContinue) {
showForm = true;
createForm.formInitialData = { ...create.formInitialData };
}
}
"
data-cy="vn-table-create-dialog"
>
<FormModelPopup
@ -972,7 +1024,10 @@ function cardClick(_, row) {
>
<template #form-inputs="{ data }">
<div :style="createComplement?.containerStyle">
<div>
<div
:style="createComplement?.previousStyle"
v-if="!quasar.screen.xs"
>
<slot name="previous-create-dialog" :data="data" />
</div>
<div class="grid-create" :style="createComplement?.columnGridStyle">
@ -985,7 +1040,10 @@ function cardClick(_, row) {
:label="column.label"
>
<VnColumn
:column="column"
:column="{
...column,
...{ disable: column?.createDisable ?? false },
}"
:row="{}"
default="input"
v-model="data[column.name]"
@ -1045,8 +1103,8 @@ es:
}
.body-cell {
padding-left: 2px !important;
padding-right: 2px !important;
padding-left: 4px !important;
padding-right: 4px !important;
position: relative;
}
.bg-chip-secondary {

View File

@ -27,30 +27,58 @@ describe('VnTable', () => {
beforeEach(() => (vm.selected = []));
describe('handleSelection()', () => {
const rows = [{ $index: 0 }, { $index: 1 }, { $index: 2 }];
const rows = [
{ $index: 0 },
{ $index: 1 },
{ $index: 2 },
{ $index: 3 },
{ $index: 4 },
];
it('should add rows to selected when shift key is pressed and rows are added in ascending order', () => {
const selectedRows = [{ $index: 1 }];
it('should add rows to selected when shift key is pressed and rows are added except last one', () => {
vm.handleSelection(
{ evt: { shiftKey: true }, added: true, rows: selectedRows },
rows
rows,
);
expect(vm.selected).toEqual([{ $index: 0 }]);
});
it('should add rows to selected when shift key is pressed and rows are added in descending order', () => {
const selectedRows = [{ $index: 3 }];
vm.handleSelection(
{ evt: { shiftKey: true }, added: true, rows: selectedRows },
rows,
);
expect(vm.selected).toEqual([{ $index: 0 }, { $index: 1 }, { $index: 2 }]);
});
it('should not add rows to selected when shift key is not pressed', () => {
const selectedRows = [{ $index: 1 }];
vm.handleSelection(
{ evt: { shiftKey: false }, added: true, rows: selectedRows },
rows
rows,
);
expect(vm.selected).toEqual([]);
});
it('should not add rows to selected when rows are not added', () => {
const selectedRows = [{ $index: 1 }];
vm.handleSelection(
{ evt: { shiftKey: true }, added: false, rows: selectedRows },
rows
rows,
);
expect(vm.selected).toEqual([]);
});
it('should add all rows between the smallest and largest selected indexes', () => {
vm.selected = [{ $index: 1 }, { $index: 3 }];
const selectedRows = [{ $index: 4 }];
vm.handleSelection(
{ evt: { shiftKey: true }, added: true, rows: selectedRows },
rows,
);
expect(vm.selected).toEqual([{ $index: 1 }, { $index: 3 }, { $index: 2 }]);
});
});
});

View File

@ -11,6 +11,13 @@ const stateStore = useStateStore();
const slots = useSlots();
const hasContent = useHasContent('#right-panel');
defineProps({
overlay: {
type: Boolean,
default: false,
},
});
onMounted(() => {
if ((!slots['right-panel'] && !hasContent.value) || quasar.platform.is.mobile)
stateStore.rightDrawer = false;
@ -34,7 +41,12 @@ onMounted(() => {
</QBtn>
</div>
</Teleport>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256">
<QDrawer
v-model="stateStore.rightDrawer"
side="right"
:width="256"
:overlay="overlay"
>
<QScrollArea class="fit">
<div id="right-panel"></div>
<slot v-if="!hasContent" name="right-panel" />

View File

@ -27,7 +27,7 @@ const checkboxModel = computed({
</script>
<template>
<div>
<QCheckbox v-bind="$attrs" v-on="$attrs" v-model="checkboxModel" />
<QCheckbox v-bind="$attrs" v-model="checkboxModel" />
<QIcon
v-if="info"
v-bind="$attrs"

View File

@ -48,7 +48,8 @@ function toValueAttrs(attrs) {
<span
v-for="toComponent of componentArray"
:key="toComponent.name"
class="column flex-center fit"
class="column fit"
:class="toComponent?.component == 'checkbox' ? 'flex-center' : ''"
>
<component
v-if="toComponent?.component"

View File

@ -107,6 +107,7 @@ const manageDate = (date) => {
@click="isPopupOpen = !isPopupOpen"
@keydown="isPopupOpen = false"
hide-bottom-space
:data-cy="$attrs.dataCy ?? $attrs.label + '_inputDate'"
>
<template #append>
<QIcon

View File

@ -302,8 +302,6 @@ defineExpose({ opts: myOptions, vnSelectRef });
function handleKeyDown(event) {
if (event.key === 'Tab' && !event.shiftKey) {
event.preventDefault();
const inputValue = vnSelectRef.value?.inputValue;
if (inputValue) {

View File

@ -76,6 +76,15 @@ onBeforeMount(async () => {
);
});
const routeName = computed(() => {
const DESCRIPTOR_PROXY = 'DescriptorProxy';
let name = $props.dataKey;
if ($props.dataKey.includes(DESCRIPTOR_PROXY)) {
name = name.split(DESCRIPTOR_PROXY)[0];
}
return `${name}Summary`;
});
async function getData() {
store.url = $props.url;
store.filter = $props.filter ?? {};
@ -154,9 +163,7 @@ const toModule = computed(() =>
{{ t('components.smartCard.openSummary') }}
</QTooltip>
</QBtn>
<RouterLink
:to="{ name: `${dataKey}Summary`, params: { id: entity.id } }"
>
<RouterLink :to="{ name: routeName, params: { id: entity.id } }">
<QBtn
class="link"
color="white"
@ -196,7 +203,6 @@ const toModule = computed(() =>
<QItemLabel class="subtitle">
#{{ getValueFromPath(subtitle) ?? entity.id }}
</QItemLabel>
<QBtn
round
flat
@ -210,7 +216,6 @@ const toModule = computed(() =>
{{ t('globals.copyId') }}
</QTooltip>
</QBtn>
<!-- </QItemLabel> -->
</QItem>
</QList>
<div class="list-box q-mt-xs">
@ -220,7 +225,7 @@ const toModule = computed(() =>
<div class="icons">
<slot name="icons" :entity="entity" />
</div>
<div class="actions justify-center">
<div class="actions justify-center" data-cy="descriptor_actions">
<slot name="actions" :entity="entity" />
</div>
<slot name="after" />

View File

@ -204,8 +204,9 @@ async function search() {
}
:deep(.q-field--focused) {
.q-icon {
color: black;
.q-icon,
.q-placeholder {
color: var(--vn-black-text-color);
}
}

View File

@ -29,7 +29,6 @@ export async function checkEntryLock(entryFk, userFk) {
.dialog({
component: VnConfirm,
componentProps: {
'data-cy': 'entry-lock-confirm',
title: t('entry.lock.title'),
message: t('entry.lock.message', {
userName: data?.user?.nickname,

View File

@ -1,14 +1,14 @@
export function getColAlign(col) {
let align;
switch (col.component) {
case 'time':
case 'date':
case 'select':
align = 'left';
break;
case 'number':
align = 'right';
break;
case 'time':
case 'date':
case 'checkbox':
align = 'center';
break;

View File

@ -3,6 +3,8 @@ import { useI18n } from 'vue-i18n';
export default function (value, options = {}) {
if (!value) return;
if (!isValidDate(value)) return null;
if (!options.dateStyle && !options.timeStyle) {
options.day = '2-digit';
options.month = '2-digit';
@ -10,7 +12,12 @@ export default function (value, options = {}) {
}
const { locale } = useI18n();
const date = new Date(value);
const newDate = new Date(value);
return new Intl.DateTimeFormat(locale.value, options).format(date);
return new Intl.DateTimeFormat(locale.value, options).format(newDate);
}
// handle 0000-00-00
function isValidDate(date) {
const parsedDate = new Date(date);
return parsedDate instanceof Date && !isNaN(parsedDate.getTime());
}

View File

@ -153,6 +153,7 @@ globals:
maxTemperature: Max
minTemperature: Min
changePass: Change password
setPass: Set password
deleteConfirmTitle: Delete selected elements
changeState: Change state
raid: 'Raid {daysInForward} days'

View File

@ -157,6 +157,7 @@ globals:
maxTemperature: Máx
minTemperature: Mín
changePass: Cambiar contraseña
setPass: Establecer contraseña
deleteConfirmTitle: Eliminar los elementos seleccionados
changeState: Cambiar estado
raid: 'Redada {daysInForward} días'
@ -786,7 +787,7 @@ worker:
notes: Notas
operator:
numberOfWagons: Número de vagones
train: tren
train: Tren
itemPackingType: Tipo de embalaje
warehouse: Almacén
sector: Sector

View File

@ -25,12 +25,13 @@ const $props = defineProps({
const { t } = useI18n();
const { hasAccount } = toRefs($props);
const { openConfirmationModal } = useVnConfirm();
const arrayData = useArrayData('Account');
const route = useRoute();
const router = useRouter();
const state = useState();
const user = state.getUser();
const { notify } = useQuasar();
const account = computed(() => useArrayData('Account').store.data[0]);
const account = computed(() => arrayData.store.data);
account.value.hasAccount = hasAccount.value;
const entityId = computed(() => +route.params.id);
const hasitManagementAccess = ref();
@ -39,7 +40,7 @@ const isHimself = computed(() => user.value.id === account.value.id);
const url = computed(() =>
isHimself.value
? 'Accounts/change-password'
: `Accounts/${entityId.value}/setPassword`
: `Accounts/${entityId.value}/setPassword`,
);
async function updateStatusAccount(active) {
@ -153,6 +154,7 @@ onMounted(() => {
t('account.card.actions.disableAccount.title'),
t('account.card.actions.disableAccount.subtitle'),
() => deleteAccount(),
() => deleteAccount(),
)
"
>
@ -172,6 +174,7 @@ onMounted(() => {
t('account.card.actions.enableAccount.title'),
t('account.card.actions.enableAccount.subtitle'),
() => updateStatusAccount(true),
() => updateStatusAccount(true),
)
"
>
@ -186,6 +189,7 @@ onMounted(() => {
t('account.card.actions.disableAccount.title'),
t('account.card.actions.disableAccount.subtitle'),
() => updateStatusAccount(false),
() => updateStatusAccount(false),
)
"
>
@ -201,6 +205,7 @@ onMounted(() => {
t('account.card.actions.activateUser.title'),
t('account.card.actions.activateUser.title'),
() => updateStatusUser(true),
() => updateStatusUser(true),
)
"
>
@ -215,6 +220,7 @@ onMounted(() => {
t('account.card.actions.deactivateUser.title'),
t('account.card.actions.deactivateUser.title'),
() => updateStatusUser(false),
() => updateStatusUser(false),
)
"
>

View File

@ -27,6 +27,7 @@ const claimActionsForm = ref();
const rows = ref([]);
const selectedRows = ref([]);
const destinationTypes = ref([]);
const shelvings = ref([]);
const totalClaimed = ref(null);
const DEFAULT_MAX_RESPONSABILITY = 5;
const DEFAULT_MIN_RESPONSABILITY = 1;
@ -56,6 +57,12 @@ const columns = computed(() => [
field: (row) => row.claimDestinationFk,
align: 'left',
},
{
name: 'shelving',
label: t('shelvings.shelving'),
field: (row) => row.shelvingFk,
align: 'left',
},
{
name: 'Landed',
label: t('Landed'),
@ -125,6 +132,10 @@ async function updateDestination(claimDestinationFk, row, options = {}) {
options.reload && claimActionsForm.value.reload();
}
}
async function updateShelving(shelvingFk, row) {
await axios.patch(`ClaimEnds/${row.id}`, { shelvingFk });
claimActionsForm.value.reload();
}
async function regularizeClaim() {
await post(`Claims/${claimId}/regularizeClaim`);
@ -200,6 +211,7 @@ async function post(query, params) {
auto-load
@on-fetch="(data) => (destinationTypes = data)"
/>
<FetchData url="Shelvings" auto-load @on-fetch="(data) => (shelvings = data)" />
<RightMenu v-if="claim">
<template #right-panel>
<QCard class="totalClaim q-my-md q-pa-sm no-box-shadow">
@ -312,6 +324,20 @@ async function post(query, params) {
/>
</QTd>
</template>
<template #body-cell-shelving="{ row }">
<QTd>
<VnSelect
v-model="row.shelvingFk"
:options="shelvings"
option-label="code"
option-value="id"
style="width: 100px"
hide-selected
@update:model-value="(value) => updateShelving(value, row)"
/>
</QTd>
</template>
<template #body-cell-price="{ value }">
<QTd align="center">
{{ toCurrency(value) }}
@ -354,7 +380,7 @@ async function post(query, params) {
(value) =>
updateDestination(
value,
props.row
props.row,
)
"
/>
@ -371,6 +397,17 @@ async function post(query, params) {
</QTable>
</template>
<template #moreBeforeActions>
<QBtn
color="primary"
text-color="white"
:unelevated="true"
:label="tMobile('Import claim')"
:title="t('Import claim')"
icon="Download"
@click="importToNewRefundTicket"
:disable="claim.claimStateFk == resolvedStateId"
:loading="loading"
/>
<QBtn
color="primary"
text-color="white"
@ -394,17 +431,6 @@ async function post(query, params) {
@click="dialogDestination = !dialogDestination"
:loading="loading"
/>
<QBtn
color="primary"
text-color="white"
:unelevated="true"
:label="tMobile('Import claim')"
:title="t('Import claim')"
icon="Upload"
@click="importToNewRefundTicket"
:disable="claim.claimStateFk == resolvedStateId"
:loading="loading"
/>
</template>
</CrudModel>
<QDialog v-model="dialogDestination">

View File

@ -40,7 +40,7 @@ const workersOptions = ref([]);
</VnRow>
<VnRow>
<VnSelect
:label="t('claim.assignedTo')"
:label="t('claim.attendedBy')"
v-model="data.workerFk"
:options="workersOptions"
option-value="id"

View File

@ -233,20 +233,27 @@ function claimUrl(section) {
<ClaimDescriptorMenu :claim="entity.claim" />
</template>
<template #body="{ entity: { claim, salesClaimed, developments } }">
<QCard class="vn-one" v-if="$route.name != 'ClaimSummary'">
<QCard class="vn-one">
<VnTitle
:url="claimUrl('basic-data')"
:text="t('globals.pageTitles.basicData')"
/>
<VnLv :label="t('claim.created')" :value="toDate(claim.created)" />
<VnLv :label="t('claim.state')">
<VnLv
v-if="$route.name != 'ClaimSummary'"
:label="t('claim.created')"
:value="toDate(claim.created)"
/>
<VnLv v-if="$route.name != 'ClaimSummary'" :label="t('claim.state')">
<template #value>
<QChip :color="stateColor(claim.claimState.code)" dense>
{{ claim.claimState.description }}
</QChip>
</template>
</VnLv>
<VnLv :label="t('globals.salesPerson')">
<VnLv
v-if="$route.name != 'ClaimSummary'"
:label="t('globals.salesPerson')"
>
<template #value>
<VnUserLink
:name="claim.client?.salesPersonUser?.name"
@ -254,7 +261,7 @@ function claimUrl(section) {
/>
</template>
</VnLv>
<VnLv :label="t('claim.attendedBy')">
<VnLv v-if="$route.name != 'ClaimSummary'" :label="t('claim.attendedBy')">
<template #value>
<VnUserLink
:name="claim.worker?.user?.nickname"
@ -262,7 +269,7 @@ function claimUrl(section) {
/>
</template>
</VnLv>
<VnLv :label="t('claim.customer')">
<VnLv v-if="$route.name != 'ClaimSummary'" :label="t('claim.customer')">
<template #value>
<span class="link cursor-pointer">
{{ claim.client?.name }}
@ -274,6 +281,11 @@ function claimUrl(section) {
:label="t('claim.pickup')"
:value="`${dashIfEmpty(claim.pickup)}`"
/>
<VnLv
:label="t('globals.packages')"
:value="`${dashIfEmpty(claim.packages)}`"
:translation="(value) => t(`claim.packages`)"
/>
</QCard>
<QCard class="vn-two">
<VnTitle :url="claimUrl('notes')" :text="t('claim.notes')" />

View File

@ -19,30 +19,36 @@ const columns = [
name: 'itemFk',
label: t('Id item'),
columnFilter: false,
align: 'left',
align: 'right',
},
{
name: 'ticketFk',
label: t('Ticket'),
columnFilter: false,
align: 'left',
align: 'right',
},
{
name: 'claimDestinationFk',
label: t('Destination'),
columnFilter: false,
align: 'left',
align: 'right',
},
{
name: 'shelvingCode',
label: t('Shelving'),
columnFilter: false,
align: 'right',
},
{
name: 'landed',
label: t('Landed'),
format: (row) => toDate(row.landed),
align: 'left',
align: 'center',
},
{
name: 'quantity',
label: t('Quantity'),
align: 'left',
align: 'right',
},
{
name: 'concept',
@ -52,18 +58,18 @@ const columns = [
{
name: 'price',
label: t('Price'),
align: 'left',
align: 'right',
},
{
name: 'discount',
label: t('Discount'),
format: ({ discount }) => toPercentage(discount / 100),
align: 'left',
align: 'right',
},
{
name: 'total',
label: t('Total'),
align: 'left',
align: 'right',
},
];
</script>

View File

@ -106,7 +106,6 @@ const props = defineProps({
:label="t('claim.zone')"
v-model="params.zoneFk"
url="Zones"
:use-like="false"
outlined
rounded
dense

View File

@ -13,7 +13,6 @@ claim:
province: Province
zone: Zone
customerId: client ID
assignedTo: Assigned
created: Created
details: Details
item: Item

View File

@ -13,7 +13,6 @@ claim:
province: Provincia
zone: Zona
customerId: ID de cliente
assignedTo: Asignado a
created: Creado
details: Detalles
item: Artículo

View File

@ -143,6 +143,7 @@ const exprBuilder = (param, value) => {
outlined
rounded
auto-load
sortBy="name ASC"
/></QItemSection>
</QItem>
<QItem class="q-mb-sm">

View File

@ -78,10 +78,20 @@ const columns = computed(() => [
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
fields: ['id', 'name', 'firstName'],
where: { role: 'salesPerson' },
optionFilter: 'firstName',
},
columnFilter: {
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name', 'firstName'],
where: { role: 'salesPerson' },
optionLabel: 'firstName',
optionValue: 'id',
},
},
create: false,
columnField: {
component: null,

View File

@ -77,7 +77,6 @@ onBeforeMount(() => {
function setPaymentType(accounting) {
if (!accounting) return;
accountingType.value = accounting.accountingType;
initialData.description = [];
initialData.payed = Date.vnNew();
isCash.value = accountingType.value.code == 'cash';
@ -87,14 +86,14 @@ function setPaymentType(accounting) {
initialData.payed.getDate() + accountingType.value.daysInFuture,
);
maxAmount.value = accountingType.value && accountingType.value.maxAmount;
if (accountingType.value.code == 'compensation')
return (initialData.description = '');
if (accountingType.value.receiptDescription)
initialData.description.push(accountingType.value.receiptDescription);
if (initialData.description) initialData.description.push(initialData.description);
initialData.description = initialData.description.join(', ');
let descriptions = [];
if (accountingType.value.receiptDescription)
descriptions.push(accountingType.value.receiptDescription);
if (initialData.description) descriptions.push(initialData.description);
initialData.description = descriptions.join(', ');
}
const calculateFromAmount = (event) => {

View File

@ -18,6 +18,7 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import CustomerSamplesPreview from 'src/pages/Customer/components/CustomerSamplesPreview.vue';
import FormPopup from 'src/components/FormPopup.vue';
import { useArrayData } from 'src/composables/useArrayData';
const { dialogRef, onDialogOK } = useDialogPluginComponent();
@ -39,7 +40,7 @@ const optionsSamplesVisible = ref([]);
const sampleType = ref({ hasPreview: false });
const initialData = reactive({});
const entityId = computed(() => route.params.id);
const customer = computed(() => state.get('Customer'));
const customer = computed(() => useArrayData('Customer').store?.data);
const filterEmailUsers = { where: { userFk: user.value.id } };
const filterClientsAddresses = {
include: [
@ -65,9 +66,9 @@ const filterSamplesVisible = {
defineEmits(['confirm', ...useDialogPluginComponent.emits]);
onBeforeMount(async () => {
initialData.clientFk = customer.value.id;
initialData.recipient = customer.value.email;
initialData.recipientId = customer.value.id;
initialData.clientFk = customer.value?.id;
initialData.recipient = customer.value?.email;
initialData.recipientId = customer.value?.id;
});
const setEmailUser = (data) => {

View File

@ -16,7 +16,6 @@ import ItemDescriptor from 'src/pages/Item/Card/ItemDescriptor.vue';
import axios from 'axios';
import VnSelectEnum from 'src/components/common/VnSelectEnum.vue';
import { checkEntryLock } from 'src/composables/checkEntryLock';
import SkeletonDescriptor from 'src/components/ui/SkeletonDescriptor.vue';
const $props = defineProps({
id: {
@ -55,6 +54,7 @@ const columns = [
toggleIndeterminate: false,
},
create: true,
createOrder: 12,
width: '25px',
},
{
@ -88,22 +88,13 @@ const columns = [
isEditable: false,
columnFilter: false,
},
{
name: 'entryFk',
isId: true,
visible: false,
isEditable: false,
disable: true,
create: true,
columnFilter: false,
},
{
align: 'center',
label: 'Id',
name: 'itemFk',
component: 'number',
isEditable: false,
width: '40px',
width: '35px',
},
{
labelAbbreviation: '',
@ -111,7 +102,7 @@ const columns = [
name: 'hex',
columnSearch: false,
isEditable: false,
width: '5px',
width: '9px',
component: 'select',
attrs: {
url: 'Inks',
@ -138,6 +129,7 @@ const columns = [
name: 'itemFk',
visible: false,
create: true,
createOrder: 0,
columnFilter: false,
},
{
@ -161,6 +153,8 @@ const columns = [
name: 'stickers',
component: 'input',
create: true,
createOrder: 1,
attrs: {
positive: false,
},
@ -181,6 +175,7 @@ const columns = [
url: 'packagings',
fields: ['id'],
optionLabel: 'id',
optionValue: 'id',
},
create: true,
width: '40px',
@ -192,7 +187,7 @@ const columns = [
component: 'number',
create: true,
width: '35px',
format: (row, dashIfEmpty) => parseFloat(row['weight']).toFixed(1),
format: (row) => parseFloat(row['weight']).toFixed(1),
},
{
labelAbbreviation: 'P',
@ -271,6 +266,7 @@ const columns = [
},
width: '45px',
create: true,
createOrder: 3,
style: getQuantityStyle,
},
{
@ -280,6 +276,7 @@ const columns = [
toolTip: t('Buying value'),
name: 'buyingValue',
create: true,
createOrder: 2,
component: 'number',
attrs: {
positive: false,
@ -312,6 +309,7 @@ const columns = [
toolTip: t('Package'),
name: 'price2',
component: 'number',
createDisable: true,
width: '35px',
create: true,
format: (row) => parseFloat(row['price2']).toFixed(2),
@ -321,6 +319,7 @@ const columns = [
label: t('Box'),
name: 'price3',
component: 'number',
createDisable: true,
cellEvent: {
'update:modelValue': async (value, oldValue, row) => {
row['price2'] = row['price2'] * (value / oldValue);
@ -330,6 +329,25 @@ const columns = [
create: true,
format: (row) => parseFloat(row['price3']).toFixed(2),
},
{
align: 'center',
labelAbbreviation: 'CM',
label: t('Check min price'),
toolTip: t('Check min price'),
name: 'hasMinPrice',
attrs: {
toggleIndeterminate: false,
},
component: 'checkbox',
cellEvent: {
'update:modelValue': async (value, oldValue, row) => {
await axios.patch(`Items/${row['itemFk']}`, {
hasMinPrice: value,
});
},
},
width: '25px',
},
{
align: 'center',
labelAbbreviation: 'Min.',
@ -350,25 +368,6 @@ const columns = [
},
format: (row) => parseFloat(row['minPrice']).toFixed(2),
},
{
align: 'center',
labelAbbreviation: 'CM',
label: t('Check min price'),
toolTip: t('Check min price'),
name: 'hasMinPrice',
attrs: {
toggleIndeterminate: false,
},
component: 'checkbox',
cellEvent: {
'update:modelValue': async (value, oldValue, row) => {
await axios.patch(`Items/${row['itemFk']}`, {
hasMinPrice: value,
});
},
},
width: '25px',
},
{
align: 'center',
labelAbbreviation: t('P.Sen'),
@ -378,6 +377,9 @@ const columns = [
component: 'number',
isEditable: false,
width: '40px',
style: () => {
return { color: 'var(--vn-label-color)' };
},
},
{
align: 'center',
@ -417,6 +419,9 @@ const columns = [
component: 'input',
isEditable: false,
width: '35px',
style: () => {
return { color: 'var(--vn-label-color)' };
},
},
];
@ -502,13 +507,14 @@ async function setBuyUltimate(itemFk, data) {
},
});
const buyUltimateData = buyUltimate.data[0];
if (!buyUltimateData) return;
const allowedKeys = columns
.filter((col) => col.create === true)
.map((col) => col.name);
allowedKeys.forEach((key) => {
if (buyUltimateData.hasOwnProperty(key) && key !== 'entryFk') {
if (buyUltimateData?.hasOwnProperty(key) && key !== 'entryFk') {
if (!['stickers', 'quantity'].includes(key)) data[key] = buyUltimateData[key];
}
});
@ -601,6 +607,7 @@ onMounted(() => {
ref="entryBuysRef"
data-key="EntryBuys"
:url="`Entries/${entityId}/getBuyList`"
search-url="EntryBuys"
save-url="Buys/crud"
:disable-option="{ card: true }"
v-model:selected="selectedRows"
@ -630,22 +637,25 @@ onMounted(() => {
isFullWidth: true,
containerStyle: {
display: 'flex',
'flex-wrap': 'wrap',
gap: '16px',
position: 'relative',
height: '450px',
},
columnGridStyle: {
'max-width': '50%',
flex: 1,
'margin-right': '30px',
flex: 1,
},
previousStyle: {
'max-width': '30%',
height: '500px',
},
displayPrevious: true,
}"
:is-editable="editableMode"
:without-header="!editableMode"
:with-filters="editableMode"
:right-search="false"
:right-search-icon="false"
:right-search="editableMode"
:right-search-icon="true"
:row-click="false"
:columns="columns"
:beforeSaveFn="beforeSave"
@ -654,6 +664,7 @@ onMounted(() => {
auto-load
footer
data-cy="entry-buys"
overlay
>
<template #column-hex="{ row }">
<VnColor :colors="row?.hexJson" style="height: 100%; min-width: 2000px" />

View File

@ -65,7 +65,7 @@ const entriesTableColumns = computed(() => [
]);
function downloadCSV(rows) {
const headers = ['id', 'itemFk', 'name', 'stickers', 'packing', 'comment'];
const headers = ['id', 'itemFk', 'name', 'stickers', 'packing', 'grouping', 'comment'];
const csvRows = rows.map((row) => {
const buy = row;
@ -77,6 +77,7 @@ function downloadCSV(rows) {
item.name || '',
buy.stickers,
buy.packing,
buy.grouping,
item.comment || '',
].join(',');
});

View File

@ -11,6 +11,8 @@ import VnTable from 'components/VnTable/VnTable.vue';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
import VnSelectTravelExtended from 'src/components/common/VnSelectTravelExtended.vue';
import { toDate } from 'src/filters';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import EntrySummary from './Card/EntrySummary.vue';
const { t } = useI18n();
const tableRef = ref();
@ -18,6 +20,7 @@ const defaultEntry = ref({});
const state = useState();
const user = state.getUser();
const dataKey = 'EntryList';
const { viewSummary } = useSummaryDialog();
const entryQueryFilter = {
include: [
@ -199,7 +202,6 @@ const columns = computed(() => [
optionValue: 'code',
optionLabel: 'description',
},
cardVisible: true,
width: '65px',
format: (row, dashIfEmpty) => dashIfEmpty(row.entryTypeDescription),
},
@ -223,6 +225,19 @@ const columns = computed(() => [
visible: false,
create: true,
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('components.smartCard.viewSummary'),
icon: 'preview',
isPrimary: true,
action: (row) => viewSummary(row.id, EntrySummary, 'xlg-width'),
},
],
},
]);
function getBadgeAttrs(row) {
const date = row.landed;
@ -268,16 +283,7 @@ onBeforeMount(async () => {
</script>
<template>
<VnSection
:data-key="dataKey"
prefix="entry"
url="Entries/filter"
:array-data-props="{
url: 'Entries/filter',
order: 'landed DESC',
userFilter: EntryFilter,
}"
>
<VnSection :data-key="dataKey" prefix="entry">
<template #advanced-menu>
<EntryFilter :data-key="dataKey" />
</template>
@ -286,6 +292,7 @@ onBeforeMount(async () => {
v-if="defaultEntry.defaultSupplierFk"
ref="tableRef"
:data-key="dataKey"
search-url="EntryList"
url="Entries/filter"
:filter="entryQueryFilter"
order="landed DESC"

View File

@ -57,7 +57,7 @@ const columns = computed(() => [
create: true,
component: 'number',
summation: true,
width: '60px',
width: '50px',
},
{
align: 'center',
@ -95,7 +95,7 @@ const columns = computed(() => [
},
},
],
'data-cy': 'table-actions',
dataCy: 'table-actions',
},
]);
@ -286,7 +286,7 @@ function round(value) {
justify-content: center;
}
.column {
min-width: 30%;
min-width: 40%;
margin-top: 5%;
display: flex;
flex-direction: column;

View File

@ -101,7 +101,8 @@ const columns = [
</template>
<style lang="css" scoped>
.container {
max-width: 50vw;
max-width: 100%;
width: 50%;
overflow: auto;
justify-content: center;
align-items: center;
@ -109,9 +110,6 @@ const columns = [
background-color: var(--vn-section-color);
padding: 2%;
}
.container > div > div > .q-table__top.relative-position.row.items-center {
background-color: red !important;
}
</style>
<i18n>
es:

View File

@ -202,7 +202,7 @@ function setCursor(ref) {
:option-label="col.optionLabel"
:filter-options="['id', 'name']"
:tooltip="t('Create a new expense')"
@keydown.tab="
@keydown.tab.prevent="
autocompleteExpense(
$event,
row,

View File

@ -163,10 +163,14 @@ const showExportationLetter = () => {
<QMenu anchor="top end" self="top start">
<QList>
<QItem v-ripple clickable @click="showSendInvoiceDialog('pdf')">
<QItemSection>{{ t('Send PDF') }}</QItemSection>
<QItemSection data-cy="InvoiceOutDescriptorMenuSendPdfOption">
{{ t('Send PDF') }}
</QItemSection>
</QItem>
<QItem v-ripple clickable @click="showSendInvoiceDialog('csv')">
<QItemSection>{{ t('Send CSV') }}</QItemSection>
<QItemSection data-cy="InvoiceOutDescriptorMenuSendCsvOption">
{{ t('Send CSV') }}
</QItemSection>
</QItem>
</QList>
</QMenu>

View File

@ -21,7 +21,6 @@ import VnSection from 'src/components/common/VnSection.vue';
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const tableRef = ref();
const invoiceOutSerialsOptions = ref([]);
const customerOptions = ref([]);
const selectedRows = ref([]);
const hasSelectedCards = computed(() => selectedRows.value.length > 0);
@ -368,7 +367,6 @@ watchEffect(selectedRows);
url="InvoiceOutSerials"
v-model="data.serial"
:label="t('invoiceOutModule.serial')"
:options="invoiceOutSerialsOptions"
option-label="description"
option-value="code"
option-filter

View File

@ -2,6 +2,7 @@ invoiceOut:
search: Search invoice
searchInfo: You can search by invoice reference
params:
id: ID
company: Company
country: Country
clientId: Client
@ -24,6 +25,7 @@ invoiceOut:
min: Min
max: Max
hasPdf: Has PDF
search: Contains
card:
issued: Issued
customerCard: Customer card

View File

@ -2,6 +2,7 @@ invoiceOut:
search: Buscar factura emitida
searchInfo: Puedes buscar por referencia de la factura
params:
id: ID
company: Empresa
country: País
clientId: Cliente
@ -24,6 +25,7 @@ invoiceOut:
min: Min
max: Max
hasPdf: Tiene PDF
search: Contiene
card:
issued: Fecha emisión
customerCard: Ficha del cliente

View File

@ -120,22 +120,9 @@ const updateStock = async () => {
</template>
</VnLv>
<VnLv :label="t('globals.producer')" :value="dashIfEmpty(entity.subName)" />
<VnLv
v-if="entity.value5"
:label="t('item.descriptor.color')"
:value="entity.value5"
>
</VnLv>
<VnLv
v-if="entity.value6"
:label="t('item.descriptor.category')"
:value="entity.value6"
/>
<VnLv
v-if="entity.value7"
:label="t('item.list.stems')"
:value="entity.value7"
/>
<VnLv v-if="entity?.value5" :label="entity?.tag5" :value="entity.value5" />
<VnLv v-if="entity?.value6" :label="entity?.tag6" :value="entity.value6" />
<VnLv v-if="entity?.value7" :label="entity?.tag7" :value="entity.value7" />
</template>
<template #icons="{ entity }">
<QCardActions v-if="entity" class="q-gutter-x-md">

View File

@ -27,7 +27,7 @@ const user = state.getUser();
const today = Date.vnNew();
today.setHours(0, 0, 0, 0);
const warehousesOptions = ref([]);
const itemBalances = computed(() => arrayDataItemBalances.store.data);
const itemBalances = computed(() => arrayDataItemBalances.store.data || []);
const where = computed(() => arrayDataItemBalances.store.filter.where || {});
const showWhatsBeforeInventory = ref(false);
const inventoriedDate = ref(null);

View File

@ -87,7 +87,7 @@ const insertTag = (rows) => {
tagFk: undefined,
}"
:default-remove="false"
:filter="{
:user-filter="{
fields: ['id', 'itemFk', 'tagFk', 'value', 'priority'],
where: { itemFk: route.params.id },
include: {
@ -119,6 +119,7 @@ const insertTag = (rows) => {
"
:required="true"
:rules="validate('itemTag.tagFk')"
:data-cy="`tag${row?.tag?.name}`"
/>
<VnSelect
v-if="row.tag?.isFree === false"
@ -145,6 +146,7 @@ const insertTag = (rows) => {
:label="t('itemTags.value')"
:is-clearable="false"
@keyup.enter.stop="(data) => itemTagsRef.onSubmit(data)"
:data-cy="`tag${row?.tag?.name}Value`"
/>
<VnInput
:label="t('itemBasicData.relevancy')"
@ -162,6 +164,7 @@ const insertTag = (rows) => {
name="delete"
size="sm"
dense
:data-cy="`deleteTag${row?.tag?.name}`"
>
<QTooltip>
{{ t('itemTags.removeTag') }}
@ -177,6 +180,7 @@ const insertTag = (rows) => {
icon="add"
v-shortcut="'+'"
fab
data-cy="createNewTag"
>
<QTooltip>
{{ t('itemTags.addTag') }}

View File

@ -8,6 +8,7 @@ import VnInput from 'src/components/common/VnInput.vue';
import FetchData from 'components/FetchData.vue';
import { useArrayData } from 'src/composables/useArrayData';
import VnSelectWorker from 'src/components/common/VnSelectWorker.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
const { t } = useI18n();
const props = defineProps({
@ -52,7 +53,7 @@ onMounted(async () => {
name: key,
value,
selectedField: { name: key, label: t(`params.${key}`) },
})
}),
);
}
exprBuilder('state', arrayData.store?.userParams?.state);
@ -157,6 +158,32 @@ onMounted(async () => {
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
v-model="params.from"
:label="t('params.from')"
is-outlined
/>
</QItemSection>
<QItemSection>
<VnInputDate
v-model="params.to"
:label="t('params.to')"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('params.daysOnward')"
v-model="params.daysOnward"
lazy-rules
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
@ -175,11 +202,10 @@ onMounted(async () => {
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('params.daysOnward')"
v-model="params.daysOnward"
lazy-rules
is-outlined
<QCheckbox
:label="t('params.mine')"
v-model="params.mine"
:toggle-indeterminate="false"
/>
</QItemSection>
</QItem>

View File

@ -3,7 +3,7 @@ import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useQuasar } from 'quasar';
import { dashIfEmpty, toDate, toHour } from 'src/filters';
import { toDate, toHour } from 'src/filters';
import { useRouter } from 'vue-router';
import { usePrintService } from 'src/composables/usePrintService';
@ -280,7 +280,7 @@ const openTicketsDialog = (id) => {
</QCardSection>
<QCardSection class="q-pt-none">
<VnInputDate
:label="t('route.Stating date')"
:label="t('route.Starting date')"
v-model="startingDate"
autofocus
/>

View File

@ -1,5 +1,5 @@
<script setup>
import { computed, ref } from 'vue';
import { computed, ref, markRaw } from 'vue';
import { useI18n } from 'vue-i18n';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { toHour } from 'src/filters';
@ -8,6 +8,7 @@ import RouteFilter from 'pages/Route/Card/RouteFilter.vue';
import VnTable from 'components/VnTable/VnTable.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnSection from 'src/components/common/VnSection.vue';
import VnSelectWorker from 'src/components/common/VnSelectWorker.vue';
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
@ -38,17 +39,7 @@ const columns = computed(() => [
align: 'left',
name: 'workerFk',
label: t('route.Worker'),
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
useLike: false,
optionFilter: 'firstName',
find: {
value: 'workerFk',
label: 'workerUserName',
},
},
component: markRaw(VnSelectWorker),
create: true,
cardVisible: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef),
@ -59,6 +50,10 @@ const columns = computed(() => [
name: 'agencyName',
label: t('route.Agency'),
cardVisible: true,
},
{
label: t('route.Agency'),
name: 'agencyModeFk',
component: 'select',
attrs: {
url: 'agencyModes',
@ -69,14 +64,19 @@ const columns = computed(() => [
},
},
create: true,
columnClass: 'expand',
columnFilter: false,
visible: false,
},
{
align: 'left',
name: 'vehiclePlateNumber',
label: t('route.Vehicle'),
cardVisible: true,
},
{
name: 'vehicleFk',
label: t('route.Vehicle'),
cardVisible: true,
component: 'select',
attrs: {
url: 'vehicles',
@ -90,6 +90,7 @@ const columns = computed(() => [
},
create: true,
columnFilter: false,
visible: false,
},
{
align: 'left',
@ -156,6 +157,7 @@ const columns = computed(() => [
<VnTable
:data-key
:columns="columns"
ref="tableRef"
:right-search="false"
redirect="route"
:create="{

View File

@ -45,8 +45,6 @@ const filter = {
:label="t('parking.sector')"
:value="entity.sector?.description"
/>
<VnLv :label="t('parking.row')" :value="entity.row" />
<VnLv :label="t('parking.column')" :value="entity.column" />
</QCard>
</template>
</CardSummary>

View File

@ -1,19 +1,15 @@
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { computed, onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import VnPaginate from 'components/ui/VnPaginate.vue';
import CardList from 'components/ui/CardList.vue';
import VnLv from 'components/ui/VnLv.vue';
import ParkingFilter from './ParkingFilter.vue';
import ParkingSummary from './Card/ParkingSummary.vue';
import exprBuilder from './ParkingExprBuilder.js';
import VnTable from 'components/VnTable/VnTable.vue';
import VnSection from 'src/components/common/VnSection.vue';
import ParkingFilter from './ParkingFilter.vue';
import exprBuilder from './ParkingExprBuilder.js';
import ParkingSummary from './Card/ParkingSummary.vue';
const stateStore = useStateStore();
const { push } = useRouter();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const dataKey = 'ParkingList';
@ -24,7 +20,48 @@ onUnmounted(() => (stateStore.rightDrawer = false));
const filter = {
fields: ['id', 'sectorFk', 'code', 'pickingOrder'],
};
const columns = computed(() => [
{
align: 'left',
name: 'code',
label: t('globals.code'),
isId: true,
isTitle: true,
columnFilter: false,
sortable: true,
},
{
align: 'left',
name: 'sector',
label: t('parking.sector'),
format: (val) => val.sector.description ?? '',
sortable: true,
cardVisible: true,
},
{
align: 'left',
name: 'pickingOrder',
label: t('parking.pickingOrder'),
sortable: true,
cardVisible: true,
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('components.smartCard.viewSummary'),
icon: 'preview',
action: (row) => viewSummary(row.id, ParkingSummary),
isPrimary: true,
},
],
},
]);
</script>
<template>
<VnSection
:data-key="dataKey"
@ -40,41 +77,24 @@ const filter = {
<ParkingFilter data-key="ParkingList" />
</template>
<template #body>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnPaginate :data-key="dataKey">
<template #body="{ rows }">
<CardList
v-for="row of rows"
:key="row.id"
:id="row.id"
:title="row.code"
@click="
push({ path: `/shelving/parking/${row.id}/summary` })
"
<VnTable
:data-key="dataKey"
:columns="columns"
is-editable="false"
:right-search="false"
:use-model="true"
:disable-option="{ table: true }"
redirect="shelving/parking"
default-mode="card"
>
<template #list-items>
<VnLv
label="Sector"
:value="row.sector?.description"
/>
<VnLv
:label="t('parking.pickingOrder')"
:value="row.pickingOrder"
/>
</template>
<template #actions>
<template #actions="{ row }">
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, ParkingSummary)"
color="primary"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
</QPage>
</VnTable>
</template>
</VnSection>
</template>

View File

@ -6,7 +6,10 @@ import VnSection from 'src/components/common/VnSection.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'src/components/FetchData.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import SupplierSummary from './Card/SupplierSummary.vue';
const { viewSummary } = useSummaryDialog();
const { t } = useI18n();
const tableRef = ref();
const dataKey = 'SupplierList';
@ -103,6 +106,19 @@ const columns = computed(() => [
},
},
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('components.smartCard.viewSummary'),
icon: 'preview',
isPrimary: true,
action: (row) => viewSummary(row.id, SupplierSummary, 'md-width'),
},
],
},
]);
</script>
<template>

View File

@ -1,32 +1,26 @@
<script setup>
import { ref } from 'vue';
import axios from 'axios';
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { toCurrency } from 'src/filters';
import VnUsesMana from 'components/ui/VnUsesMana.vue';
const $props = defineProps({
mana: {
type: Number,
default: null,
},
newPrice: {
type: Number,
default: 0,
},
usesMana: {
type: Boolean,
default: false,
},
manaCode: {
type: String,
default: 'mana',
},
sale: {
type: Object,
default: null,
},
});
const route = useRoute();
const mana = ref(null);
const usesMana = ref(false);
const emit = defineEmits(['save', 'cancel']);
const { t } = useI18n();
@ -38,20 +32,35 @@ const save = (sale = $props.sale) => {
QPopupProxyRef.value.hide();
};
const getMana = async () => {
const { data } = await axios.get(`Tickets/${route.params.id}/getSalesPersonMana`);
mana.value = data;
await getUsesMana();
};
const getUsesMana = async () => {
const { data } = await axios.get('Sales/usesMana');
usesMana.value = data;
};
const cancel = () => {
emit('cancel');
QPopupProxyRef.value.hide();
};
const hasMana = computed(() => typeof mana.value === 'number');
defineExpose({ save });
</script>
<template>
<QPopupProxy ref="QPopupProxyRef" data-cy="ticketEditManaProxy">
<QPopupProxy
ref="QPopupProxyRef"
@before-show="getMana"
data-cy="ticketEditManaProxy"
>
<div class="container">
<QSpinner v-if="!mana" color="primary" size="md" />
<div v-else>
<div class="header">Mana: {{ toCurrency(mana) }}</div>
<div class="q-pa-md">
<QSpinner v-if="!hasMana" color="primary" size="md" />
<div class="q-pa-md" v-else>
<slot :popup="QPopupProxyRef" />
<div v-if="usesMana" class="column q-gutter-y-sm q-mt-sm">
<VnUsesMana :mana-code="manaCode" />
@ -63,7 +72,7 @@ defineExpose({ save });
</span>
</div>
</div>
</div>
<div class="row">
<QBtn
color="primary"

View File

@ -45,7 +45,6 @@ const isTicketEditable = ref(false);
const sales = ref([]);
const editableStatesOptions = ref([]);
const selectedSales = ref([]);
const mana = ref(null);
const manaCode = ref('mana');
const ticketState = computed(() => store.data?.ticketState?.state?.code);
const transfer = ref({
@ -175,17 +174,21 @@ const getSaleTotal = (sale) => {
return price - discount;
};
const getRowUpdateInputEvents = (sale) => ({
'keyup.enter': () => {
changeQuantity(sale);
},
blur: () => {
changeQuantity(sale);
},
});
const resetChanges = async () => {
arrayData.fetch({ append: false });
tableRef.value.reload();
};
const rowToUpdate = ref(null);
const changeQuantity = async (sale) => {
if (
!sale.itemFk ||
sale.quantity == null ||
edit.value?.oldQuantity === sale.quantity
)
if (!sale.itemFk || sale.quantity == null || sale?.originalQuantity === sale.quantity)
return;
if (!sale.id) return addSale(sale);
@ -197,13 +200,10 @@ const changeQuantity = async (sale) => {
const updateQuantity = async (sale) => {
try {
let { quantity, id } = sale;
if (!rowToUpdate.value) return;
rowToUpdate.value = null;
sale.isNew = false;
const params = { quantity: quantity };
await axios.post(`Sales/${id}/updateQuantity`, params);
await axios.post(`Sales/${id}/updateQuantity`, { quantity });
notify('globals.dataSaved', 'positive');
tableRef.value.reload();
resetChanges();
} catch (e) {
const { quantity } = tableRef.value.CrudModelRef.originalData.find(
(s) => s.id === sale.id,
@ -247,7 +247,7 @@ const updateConcept = async (sale) => {
const data = { newConcept: sale.concept };
await axios.post(`Sales/${sale.id}/updateConcept`, data);
notify('globals.dataSaved', 'positive');
tableRef.value.reload();
resetChanges();
};
const DEFAULT_EDIT = {
@ -258,18 +258,6 @@ const DEFAULT_EDIT = {
oldQuantity: null,
};
const edit = ref({ ...DEFAULT_EDIT });
const usesMana = ref(null);
const getUsesMana = async () => {
const { data } = await axios.get('Sales/usesMana');
usesMana.value = data;
};
const getMana = async () => {
const { data } = await axios.get(`Tickets/${route.params.id}/getSalesPersonMana`);
mana.value = data;
await getUsesMana();
};
const selectedValidSales = computed(() => {
if (!sales.value) return;
@ -277,7 +265,6 @@ const selectedValidSales = computed(() => {
});
const onOpenEditPricePopover = async (sale) => {
await getMana();
edit.value = {
sale: JSON.parse(JSON.stringify(sale)),
price: sale.price,
@ -285,7 +272,6 @@ const onOpenEditPricePopover = async (sale) => {
};
const onOpenEditDiscountPopover = async (sale) => {
await getMana();
if (isLocked.value) return;
if (sale) {
edit.value = {
@ -306,14 +292,13 @@ const changePrice = async (sale) => {
await confirmUpdate(() => updatePrice(sale, newPrice));
} else updatePrice(sale, newPrice);
}
await getMana();
};
const updatePrice = async (sale, newPrice) => {
await axios.post(`Sales/${sale.id}/updatePrice`, { newPrice });
sale.price = newPrice;
edit.value = { ...DEFAULT_EDIT };
notify('globals.dataSaved', 'positive');
tableRef.value.reload();
resetChanges();
};
const changeDiscount = async (sale) => {
@ -345,7 +330,7 @@ const updateDiscount = async (sales, newDiscount = null) => {
};
await axios.post(`Tickets/${route.params.id}/updateDiscount`, params);
notify('globals.dataSaved', 'positive');
tableRef.value.reload();
resetChanges();
};
const getNewPrice = computed(() => {
@ -413,7 +398,7 @@ const removeSales = async () => {
await axios.post('Sales/deleteSales', params);
removeSelectedSales();
notify('globals.dataSaved', 'positive');
window.location.reload();
resetChanges();
};
const setTransferParams = async () => {
@ -599,9 +584,7 @@ watch(
:is-ticket-editable="isTicketEditable"
:sales="selectedValidSales"
:disable="!hasSelectedRows"
:mana="mana"
:ticket-config="ticketConfig"
@get-mana="getMana()"
@update-discounts="updateDiscounts"
@refresh-table="resetChanges"
/>
@ -772,9 +755,7 @@ watch(
v-if="row.isNew || isTicketEditable"
type="number"
v-model.number="row.quantity"
@blur="changeQuantity(row)"
@keyup.enter.stop="changeQuantity(row)"
@update:model-value="() => (rowToUpdate = row)"
v-on="getRowUpdateInputEvents(row)"
@focus="edit.oldQuantity = row.quantity"
/>
<span v-else>{{ row.quantity }}</span>
@ -786,7 +767,6 @@ watch(
</QBtn>
<TicketEditManaProxy
ref="editPriceProxyRef"
:mana="mana"
:sale="row"
:new-price="getNewPrice"
@save="changePrice"
@ -809,10 +789,8 @@ watch(
<TicketEditManaProxy
ref="editManaProxyRef"
:mana="mana"
:sale="row"
:new-price="getNewPrice"
:uses-mana="usesMana"
:mana-code="manaCode"
@save="changeDiscount"
>

View File

@ -34,10 +34,6 @@ const props = defineProps({
type: Array,
default: () => [],
},
mana: {
type: Number,
default: null,
},
ticketConfig: {
type: Array,
default: () => [],
@ -50,6 +46,7 @@ const { dialog } = useQuasar();
const { notify } = useNotify();
const acl = useAcl();
const btnDropdownRef = ref(null);
const editManaProxyRef = ref(null);
const { openConfirmationModal } = useVnConfirm();
const newDiscount = ref(null);
@ -131,13 +128,13 @@ const createClaim = () => {
openConfirmationModal(
t('Claim out of time'),
t('Do you want to continue?'),
onCreateClaimAccepted
onCreateClaimAccepted,
);
else
openConfirmationModal(
t('Do you want to create a claim?'),
false,
onCreateClaimAccepted
onCreateClaimAccepted,
);
};
@ -216,8 +213,14 @@ const createRefund = async (withWarehouse) => {
<QItemSection>
<QItemLabel>{{ t('Update discount') }}</QItemLabel>
</QItemSection>
<TicketEditManaProxy :mana="props.mana" @save="changeMultipleDiscount()">
<TicketEditManaProxy
ref="editManaProxyRef"
:sale="row"
@save="changeMultipleDiscount"
>
<VnInput
autofocus
@keyup.enter.stop="() => editManaProxyRef.save(row)"
v-model.number="newDiscount"
:label="t('ticketSale.discount')"
type="number"

View File

@ -31,7 +31,7 @@ const oldQuantity = ref(null);
watch(
() => route.params.id,
async () => nextTick(async () => await saleTrackingFetchDataRef.value.fetch())
async () => nextTick(async () => await saleTrackingFetchDataRef.value.fetch()),
);
const columns = computed(() => [
@ -212,7 +212,7 @@ const updateShelving = async (sale) => {
const { data: patchResponseData } = await axios.patch(
`ItemShelvings/${sale.itemShelvingFk}`,
params
params,
);
const filter = {
fields: ['parkingFk'],
@ -385,7 +385,7 @@ const qCheckBoxController = (sale, action) => {
</template>
<template #body-cell-parking="{ row }">
<QTd style="width: 10%">
{{ dashIfEmpty(row.parkingFk) }}
{{ dashIfEmpty(row.parkingCode) }}
</QTd>
</template>
<template #body-cell-actions="{ row }">

View File

@ -46,6 +46,15 @@ const descriptorData = useArrayData('Ticket');
onMounted(async () => {
ticketUrl.value = (await getUrl('ticket/')) + entityId.value + '/';
});
const formattedAddress = computed(() => {
if (!ticket.value) return '';
const address = ticket.value.address;
const postcode = address.postalCode;
const province = address.province ? `(${address.province.name})` : '';
return `${address.street} - ${postcode} - ${address.city} ${province}`;
});
function isEditable() {
try {
@ -238,7 +247,7 @@ onMounted(async () => {
/>
<VnLv
:label="t('ticket.summary.consigneeStreet')"
:value="entity.address?.street"
:value="formattedAddress"
/>
</QCard>
<QCard class="vn-one" v-if="entity.notes.length">

View File

@ -293,6 +293,7 @@ en:
clientFk: Customer
orderFk: Order
from: From
shipped: Shipped
to: To
salesPersonFk: Salesperson
stateFk: State
@ -320,6 +321,7 @@ es:
clientFk: Cliente
orderFk: Pedido
from: Desde
shipped: F. envío
to: Hasta
salesPersonFk: Comercial
stateFk: Estado

View File

@ -108,13 +108,11 @@ const columns = computed(() => [
},
{
align: 'left',
name: 'shippedDate',
name: 'shipped',
cardVisible: true,
label: t('ticketList.shipped'),
columnFilter: {
component: 'date',
alias: 't',
inWhere: true,
},
format: ({ shippedDate }) => toDate(shippedDate),
},
@ -216,6 +214,7 @@ const columns = computed(() => [
{
title: t('components.smartCard.viewSummary'),
icon: 'preview',
isPrimary: true,
action: (row, evt) => {
if (evt && evt.ctrlKey) {
const url = router.resolve({
@ -253,7 +252,7 @@ const fetchAvailableAgencies = async (formData) => {
const { options, agency } = response;
if (options) agenciesOptions.value = options;
if (agency) formData.agencyModeId = agency;
if (agency) formData.agencyModeId = agency.agencyModeFk;
};
const fetchClient = async (formData) => {

View File

@ -17,6 +17,12 @@ const maritalStatus = [
{ code: 'M', name: t('Married') },
{ code: 'S', name: t('Single') },
];
async function setAdvancedSummary(data) {
const advanced = (await useAdvancedSummary('Workers', data.id)) ?? {};
Object.assign(form.value.formData, advanced);
await nextTick();
if (form.value) form.value.hasChanges = false;
}
</script>
<template>
<FetchData
@ -36,18 +42,22 @@ const maritalStatus = [
:url-update="`Workers/${$route.params.id}`"
auto-load
model="Worker"
@on-fetch="
async (data) => {
Object.assign(data, (await useAdvancedSummary('Workers', data.id)) ?? {});
await $nextTick();
if (form) form.hasChanges = false;
}
"
@on-fetch="setAdvancedSummary"
>
<template #form="{ data }">
<VnRow>
<VnInput :label="t('Name')" clearable v-model="data.firstName" />
<VnInput :label="t('Last name')" clearable v-model="data.lastName" />
<VnInput
:label="t('Name')"
clearable
v-model="data.firstName"
:required="true"
/>
<VnInput
:label="t('Last name')"
clearable
v-model="data.lastName"
:required="true"
/>
</VnRow>
<VnRow>
<VnInput v-model="data.phone" :label="t('Business phone')" clearable />

View File

@ -3,7 +3,7 @@ import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnTable from 'components/VnTable/VnTable.vue';
import { toDate } from 'src/filters';
import { dashIfEmpty, toDate } from 'src/filters';
import { useQuasar } from 'quasar';
import axios from 'axios';
@ -15,7 +15,7 @@ const quasar = useQuasar();
async function reactivateWorker() {
const hasToReactive = tableRef.value.CrudModelRef.formData.find(
(data) => !data.ended
(data) => !data.ended,
);
if (hasToReactive) {
quasar
@ -38,25 +38,25 @@ const columns = computed(() => [
{
name: 'started',
label: t('worker.business.tableVisibleColumns.started'),
align: 'left',
format: ({ started }) => toDate(started),
component: 'date',
cardVisible: true,
create: true,
width: '90px',
},
{
name: 'ended',
label: t('worker.business.tableVisibleColumns.ended'),
align: 'left',
format: ({ ended }) => toDate(ended),
component: 'date',
cardVisible: true,
create: true,
width: '90px',
},
{
label: t('worker.business.tableVisibleColumns.company'),
align: 'left',
toolTip: t('worker.business.tableVisibleColumns.company'),
name: 'companyCodeFk',
component: 'select',
attrs: {
@ -65,23 +65,23 @@ const columns = computed(() => [
optionLabel: 'code',
optionValue: 'code',
},
cardVisible: true,
create: true,
width: '60px',
},
{
align: 'left',
name: 'reasonEndFk',
component: 'select',
label: t('worker.business.tableVisibleColumns.reasonEnd'),
toolTip: t('worker.business.tableVisibleColumns.reasonEnd'),
attrs: {
url: 'BusinessReasonEnds',
fields: ['id', 'reason'],
optionLabel: 'reason',
},
cardVisible: true,
format: ({ reason }, dashIfEmpty) => dashIfEmpty(reason),
},
{
align: 'left',
name: 'departmentFk',
component: 'select',
label: t('worker.business.tableVisibleColumns.department'),
@ -89,15 +89,19 @@ const columns = computed(() => [
url: 'Departments',
fields: ['id', 'name'],
optionLabel: 'name',
optionValue: 'id',
},
cardVisible: true,
create: true,
width: '80px',
format: ({ departmentName }, dashIfEmpty) => dashIfEmpty(departmentName),
},
{
align: 'left',
name: 'workerBusinessProfessionalCategoryFk',
component: 'select',
label: t('worker.business.tableVisibleColumns.professionalCategory'),
toolTip: t('worker.business.tableVisibleColumns.professionalCategory'),
attrs: {
url: 'WorkerBusinessProfessionalCategories',
fields: ['id', 'description', 'code'],
@ -105,6 +109,9 @@ const columns = computed(() => [
},
cardVisible: true,
create: true,
width: '100px',
format: ({ professionalDescription }, dashIfEmpty) =>
dashIfEmpty(professionalDescription),
},
{
align: 'left',
@ -118,6 +125,8 @@ const columns = computed(() => [
},
cardVisible: true,
create: true,
format: ({ calendarTypeDescription }, dashIfEmpty) =>
dashIfEmpty(calendarTypeDescription),
},
{
align: 'left',
@ -131,6 +140,8 @@ const columns = computed(() => [
},
cardVisible: true,
create: true,
width: '100px',
format: ({ workCenterName }, dashIfEmpty) => dashIfEmpty(workCenterName),
},
{
align: 'left',
@ -144,6 +155,7 @@ const columns = computed(() => [
},
cardVisible: true,
create: true,
format: ({ payrollDescription }, dashIfEmpty) => dashIfEmpty(payrollDescription),
},
{
align: 'left',
@ -157,6 +169,7 @@ const columns = computed(() => [
},
cardVisible: true,
create: true,
format: ({ occupationName }, dashIfEmpty) => dashIfEmpty(occupationName),
},
{
align: 'left',
@ -165,6 +178,7 @@ const columns = computed(() => [
component: 'input',
cardVisible: true,
create: true,
width: '50px',
},
{
align: 'left',
@ -177,6 +191,8 @@ const columns = computed(() => [
},
cardVisible: true,
create: true,
format: ({ workerBusinessTypeName }, dashIfEmpty) =>
dashIfEmpty(workerBusinessTypeName),
},
{
align: 'left',
@ -185,6 +201,7 @@ const columns = computed(() => [
component: 'input',
cardVisible: true,
create: true,
width: '70px',
},
{
align: 'left',
@ -193,6 +210,7 @@ const columns = computed(() => [
component: 'input',
cardVisible: true,
create: true,
width: '70px',
},
{
name: 'notes',
@ -208,7 +226,7 @@ const columns = computed(() => [
<VnTable
ref="tableRef"
data-key="WorkerBusiness"
:url="`Workers/${entityId}/Business`"
:url="`Workers/${entityId}/getWorkerBusiness`"
save-url="/Businesses/crud"
:create="{
urlCreate: `Workers/${entityId}/Business`,
@ -218,13 +236,12 @@ const columns = computed(() => [
}"
order="id DESC"
:columns="columns"
default-mode="card"
auto-load
:disable-option="{ table: true }"
:disable-option="{ card: true }"
:right-search="false"
card-class="grid-two q-gutter-x-xl q-gutter-y-md q-pr-lg q-py-lg"
:is-editable="true"
:use-model="true"
:right-search-icon="false"
@save-changes="(data) => reactivateWorker(data)"
/>
</template>

View File

@ -111,6 +111,7 @@ const handlePhotoUpdated = (evt = false) => {
<template #body="{ entity }">
<VnLv :label="t('globals.user')" :value="entity.user?.name" />
<VnLv
class="ellipsis-text"
:label="t('globals.params.email')"
:value="entity.user?.emailUser?.email"
copy
@ -177,6 +178,12 @@ const handlePhotoUpdated = (evt = false) => {
.photo {
height: 256px;
}
.ellipsis-text {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
<i18n>

View File

@ -12,6 +12,11 @@ const $props = defineProps({
<template>
<QPopupProxy>
<WorkerDescriptor v-if="$props.id" :id="$props.id" :summary="WorkerSummary" />
<WorkerDescriptor
v-if="$props.id"
:id="$props.id"
:summary="WorkerSummary"
data-key="WorkerDescriptorProxy"
/>
</QPopupProxy>
</template>

View File

@ -3,6 +3,7 @@ import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnTable from 'components/VnTable/VnTable.vue';
import { dashIfEmpty } from 'src/filters';
const tableRef = ref();
const { t } = useI18n();
const route = useRoute();
@ -44,9 +45,12 @@ const columns = [
create: true,
component: 'select',
attrs: {
url: 'centers',
url: 'medicalCenters',
fields: ['id', 'name'],
},
format: (row, dashIfEmpty) => {
return dashIfEmpty(row.center?.name);
},
},
{
align: 'left',

View File

@ -54,9 +54,8 @@ watch(
selected.value = [];
}
},
{ immediate: true, deep: true }
{ immediate: true, deep: true },
);
</script>
<template>
@ -105,6 +104,7 @@ watch(
:options="trainsData"
hide-selected
v-model="row.trainFk"
:required="true"
/>
</VnRow>
<VnRow>
@ -115,12 +115,14 @@ watch(
option-label="code"
option-value="code"
v-model="row.itemPackingTypeFk"
:required="true"
/>
<VnSelect
:label="t('worker.operator.warehouse')"
:options="warehousesData"
hide-selected
v-model="row.warehouseFk"
:required="true"
/>
</VnRow>
<VnRow>
@ -175,6 +177,7 @@ watch(
:label="t('worker.operator.isOnReservationMode')"
v-model="row.isOnReservationMode"
lazy-rules
:required="true"
/>
</VnRow>
<VnRow>

View File

@ -1,8 +1,8 @@
src/pages/Worker/Card/WorkerPBX.vue
<script setup>
import { useI18n } from 'vue-i18n';
import FormModel from 'src/components/FormModel.vue';
import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n();
</script>
<template>
@ -26,3 +26,8 @@ import VnInput from 'src/components/common/VnInput.vue';
</template>
</FormModel>
</template>
<i18n>
es:
It must be a 4-digit number and must not end in 00: Debe ser un número de 4 cifras y no terminar en 00
</i18n>

View File

@ -140,6 +140,7 @@ function reloadData() {
id="deviceProductionFk"
hide-selected
data-cy="pda-dialog-select"
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">

View File

@ -1,16 +1,9 @@
<script setup>
import VnSection from 'src/components/common/VnSection.vue';
import WorkerDepartmentTree from './WorkerDepartmentTree.vue';
</script>
<template>
<VnSection data-key="WorkerDepartment" :search-bar="false">
<template #body>
<div class="flex flex-center q-pa-md">
<WorkerDepartmentTree />
</div>
</template>
</VnSection>
<QPage class="q-pa-md flex justify-center"> <WorkerDepartmentTree /> </QPage>
</template>
<i18n>

View File

@ -65,6 +65,7 @@ const tableFilter = {
const columns = computed(() => [
{
align: 'left',
name: 'id',
label: t('list.id'),
chip: {
@ -74,8 +75,6 @@ const columns = computed(() => [
columnFilter: {
inWhere: true,
},
columnClass: 'shrink-column',
component: 'number',
},
{
align: 'left',
@ -107,6 +106,7 @@ const columns = computed(() => [
format: (row, dashIfEmpty) => dashIfEmpty(row?.agencyMode?.name),
},
{
align: 'left',
name: 'price',
label: t('list.price'),
cardVisible: true,
@ -114,11 +114,9 @@ const columns = computed(() => [
columnFilter: {
inWhere: true,
},
columnClass: 'shrink-column',
component: 'number',
},
{
align: 'center',
align: 'left',
name: 'hour',
label: t('list.close'),
cardVisible: true,
@ -180,8 +178,6 @@ function formatRow(row) {
<ZoneFilterPanel data-key="ZonesList" />
</template>
</RightMenu>
<div class="table-container">
<div class="column items-center">
<VnTable
ref="tableRef"
data-key="ZonesList"
@ -196,8 +192,6 @@ function formatRow(row) {
:columns="columns"
redirect="zone"
:right-search="false"
table-height="85vh"
order="id ASC"
>
<template #column-addressFk="{ row }">
{{ dashIfEmpty(formatRow(row)) }}
@ -245,8 +239,6 @@ function formatRow(row) {
/>
</template>
</VnTable>
</div>
</div>
</template>
<i18n>
@ -254,20 +246,3 @@ es:
Search zone: Buscar zona
You can search zones by id or name: Puedes buscar zonas por id o nombre
</i18n>
<style lang="scss" scoped>
.table-container {
display: flex;
justify-content: center;
}
.column {
display: flex;
flex-direction: column;
align-items: center;
min-width: 70%;
}
:deep(.shrink-column) {
width: 8%;
}
</style>

View File

@ -1,3 +1,7 @@
reports/*
videos/*
screenshots/*
downloads/*
storage/*
reports/*
docker/logs/*

View File

@ -0,0 +1,149 @@
{
"db": {
"connector": "memory",
"timezone": "local"
},
"vn": {
"connector": "vn-mysql",
"database": "vn",
"debug": false,
"host": "db",
"port": "3306",
"username": "root",
"password": "root",
"connectionLimit": 100,
"queueLimit": 100,
"multipleStatements": true,
"legacyUtcDateProcessing": false,
"timezone": "local",
"connectTimeout": 40000,
"acquireTimeout": 90000,
"waitForConnections": true,
"maxIdleTime": 60000,
"idleTimeout": 60000
},
"osticket": {
"connector": "memory",
"timezone": "local"
},
"tempStorage": {
"name": "tempStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/tmp",
"maxFileSize": "262144000",
"allowedContentTypes": [
"application/x-7z-compressed",
"application/x-zip-compressed",
"application/x-rar-compressed",
"application/octet-stream",
"application/pdf",
"application/zip",
"application/rar",
"multipart/x-zip",
"image/png",
"image/jpeg",
"image/jpg",
"image/webp",
"video/mp4"
]
},
"dmsStorage": {
"name": "dmsStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/dms",
"maxFileSize": "262144000",
"allowedContentTypes": [
"application/x-7z-compressed",
"application/x-zip-compressed",
"application/x-rar-compressed",
"application/octet-stream",
"application/pdf",
"application/zip",
"application/rar",
"multipart/x-zip",
"image/png",
"image/jpeg",
"image/jpg",
"image/webp"
]
},
"imageStorage": {
"name": "imageStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/image",
"maxFileSize": "52428800",
"allowedContentTypes": [
"image/png",
"image/jpeg",
"image/jpg",
"image/webp"
]
},
"invoiceStorage": {
"name": "invoiceStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/pdfs/invoice",
"maxFileSize": "52428800",
"allowedContentTypes": [
"application/octet-stream",
"application/pdf"
]
},
"claimStorage": {
"name": "claimStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/dms",
"maxFileSize": "31457280",
"allowedContentTypes": [
"image/png",
"image/jpeg",
"image/jpg",
"image/webp",
"video/mp4"
]
},
"entryStorage": {
"name": "entryStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/dms",
"maxFileSize": "31457280",
"allowedContentTypes": [
"image/png",
"image/jpeg",
"image/jpg",
"image/webp",
"video/mp4"
]
},
"supplierStorage": {
"name": "supplierStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/dms",
"maxFileSize": "31457280",
"allowedContentTypes": [
"image/png",
"image/jpeg",
"image/jpg",
"image/webp",
"video/mp4",
"application/pdf"
]
},
"accessStorage": {
"name": "accessStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/access",
"maxFileSize": "524288000",
"allowedContentTypes": [
"application/x-7z-compressed"
]
}
}

View File

@ -0,0 +1,21 @@
version: '3.7'
services:
back:
image: 'registry.verdnatura.es/salix-back:${COMPOSE_TAG:-dev}'
volumes:
- ./test/cypress/storage:/salix/storage
- ./test/cypress/back/datasources.json:/salix/loopback/server/datasources.json
depends_on:
- db
dns_search: .
front:
image: lilium-dev:latest
command: pnpm exec quasar dev
volumes:
- .:/app
environment:
- CI
- TZ
dns_search: .
db:
image: 'registry.verdnatura.es/salix-db:${COMPOSE_TAG:-dev}'

View File

View File

@ -41,7 +41,7 @@ describe('OrderCatalog', () => {
}
});
cy.get(
'[data-cy="vn-searchbar"] > .q-field > .q-field__inner > .q-field__control'
'[data-cy="vn-searchbar"] > .q-field > .q-field__inner > .q-field__control',
).type('{enter}');
cy.get(':nth-child(1) > [data-cy="catalogFilterCategory"]').click();
cy.dataCy('catalogFilterValueDialogBtn').last().click();

View File

@ -0,0 +1,24 @@
describe('ClaimNotes', () => {
const descriptorOptions = '[data-cy="descriptor-more-opts-menu"] > .q-list';
const url = '/#/account/1/summary';
it('should see all the account options', () => {
cy.login('itManagement');
cy.visit(url);
cy.dataCy('descriptor-more-opts').click();
cy.get(descriptorOptions)
.find('.q-item')
.its('length')
.then((count) => {
cy.log('Número de opciones:', count);
expect(count).to.equal(5);
});
});
it('should not see any option', () => {
cy.login('salesPerson');
cy.visit(url);
cy.dataCy('descriptor-more-opts').click();
cy.get(descriptorOptions).should('not.be.visible');
});
});

View File

@ -1,6 +1,6 @@
/// <reference types="cypress" />
describe('ClaimAction', () => {
const claimId = 2;
const claimId = 1;
const firstRow = 'tbody > :nth-child(1)';
const destinationRow = '.q-item__section > .q-field';
@ -24,9 +24,9 @@ describe('ClaimAction', () => {
const rowData = [true];
cy.fillRow(firstRow, rowData);
cy.get('[title="Change destination"]').click();
cy.get('[title="Change destination"]').click({ force: true });
cy.selectOption(destinationRow, 'Confeccion');
cy.get('.q-card > .q-card__actions > .q-btn--standard').click();
cy.get('.q-card > .q-card__actions > .q-btn--standard').click({ force: true });
});
it('should regularize', () => {

View File

@ -35,8 +35,7 @@ describe('ClaimDevelopment', () => {
cy.saveCard();
});
// TODO: #8112
xit('should add and remove new line', () => {
it('should add and remove new line', () => {
cy.wait(['@workers', '@workers']);
cy.addCard();

View File

@ -8,7 +8,8 @@ describe('ClaimNotes', () => {
it('should add a new note', () => {
const message = 'This is a new message.';
cy.get('.q-textarea').type(message);
cy.get('.q-textarea').should('not.be.disabled').type(message);
cy.get(saveBtn).click();
cy.get(firstNote).should('have.text', message);
});

View File

@ -23,37 +23,35 @@ describe.skip('ClaimPhoto', () => {
});
it('should open first image dialog change to second and close', () => {
cy.get(
':nth-child(1) > .q-card > .q-img > .q-img__container > .q-img__image'
).click();
cy.get(':nth-last-child(1) > .q-card').click();
cy.get('.q-carousel__slide > .q-img > .q-img__container > .q-img__image').should(
'be.visible'
'be.visible',
);
cy.get('.q-carousel__control > .q-btn > .q-btn__content > .q-icon').click();
cy.get('.q-carousel__control > button').click();
cy.get(
'.q-dialog__inner > .q-toolbar > .q-btn > .q-btn__content > .q-icon'
'.q-dialog__inner > .q-toolbar > .q-btn > .q-btn__content > .q-icon',
).click();
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', () => {
cy.get(
'.multimediaParent > :nth-child(3) > .q-btn > .q-btn__content > .q-icon'
'.multimediaParent > :nth-last-child(1) > .q-btn > .q-btn__content > .q-icon',
).click();
cy.get(
'.q-card__actions > .q-btn--unelevated > .q-btn__content > .block'
'.q-card__actions > .q-btn--unelevated > .q-btn__content > .block',
).click();
cy.get('.q-notification__message').should('have.text', 'Data deleted');
cy.get(
'.multimediaParent > :nth-child(3) > .q-btn > .q-btn__content > .q-icon'
'.multimediaParent > :nth-last-child(1) > .q-btn > .q-btn__content > .q-icon',
).click();
cy.get(
'.q-card__actions > .q-btn--unelevated > .q-btn__content > .block'
'.q-card__actions > .q-btn--unelevated > .q-btn__content > .block',
).click();
cy.get('.q-notification__message').should('have.text', 'Data deleted');
});

View File

@ -4,7 +4,6 @@ describe('Client consignee', () => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1107/address');
cy.domContentLoad();
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');

View File

@ -8,7 +8,7 @@ describe('Client basic data', () => {
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
cy.dataCy('customerPhone').find('input').should('be.visible');
cy.dataCy('customerPhone').find('input').type('123456789');
cy.dataCy('customerPhone').find('input').clear().type('123456789');
cy.get('.q-btn-group > .q-btn--standard').click();
cy.intercept('PATCH', '/api/Clients/1102', (req) => {
const { body } = req;

View File

@ -4,7 +4,6 @@ describe('Client fiscal data', () => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1107/fiscal-data');
cy.domContentLoad();
});
it('Should change required value when change customer', () => {
cy.get('.q-card').should('be.visible');

View File

@ -1,7 +1,6 @@
/// <reference types="cypress" />
describe('Client list', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('/#/customer/list', {
timeout: 5000,
@ -28,7 +27,7 @@ describe('Client list', () => {
Email: { val: `user.test${randomInt}@cypress.com` },
'Sales person': { val: 'salesPerson', type: 'select' },
Location: { val: '46000', type: 'select' },
'Business type': { val: 'Otros', type: 'select' },
'Business type': { val: 'others', type: 'select' },
};
cy.fillInForm(data);
@ -37,6 +36,7 @@ describe('Client list', () => {
cy.checkNotification('Data created');
cy.url().should('include', '/summary');
});
it('Client list search client', () => {
const search = 'Jessica Jones';
cy.searchByLabel('Name', search);
@ -59,6 +59,7 @@ describe('Client list', () => {
cy.checkValueForm(1, search);
cy.checkValueForm(2, search);
});
it('Client founded create order', () => {
const search = 'Jessica Jones';
cy.searchByLabel('Name', search);

View File

@ -1,4 +1,4 @@
describe('Entry', () => {
describe.skip('Entry', () => {
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('buyer');
@ -67,7 +67,7 @@ describe('Entry', () => {
it('Should notify when entry is lock by another user', () => {
const checkLockMessage = () => {
cy.get('[data-cy="entry-lock-confirm"]').should('be.visible');
cy.get('[role="dialog"]').should('be.visible');
cy.get('[data-cy="VnConfirm_message"] > span').should(
'contain.text',
'This entry has been locked by buyerNick',
@ -184,9 +184,8 @@ describe('Entry', () => {
}
function deleteEntry() {
cy.get('[data-cy="descriptor-more-opts"]').click();
cy.waitForElement('div[data-cy="delete-entry"]');
cy.get('div[data-cy="delete-entry"]').should('be.visible').click();
cy.get('[data-cy="descriptor-more-opts"]').should('be.visible').click();
cy.waitForElement('div[data-cy="delete-entry"]').click();
cy.url().should('include', 'list');
}

View File

@ -9,7 +9,7 @@ describe('EntryStockBought', () => {
cy.get('[data-col-field="reserve"][data-row-index="0"]').click();
cy.get('input[name="reserve"]').type('10{enter}');
cy.get('button[title="Save"]').click();
cy.get('.q-notification__message').should('have.text', 'Data saved');
cy.checkNotification('Data saved');
});
it('Should add a new reserved space for buyerBoss', () => {
cy.addBtnClick();

View File

@ -9,7 +9,7 @@ describe('InvoiceInList', () => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit(`/#/invoice-in/list`);
cy.get('#searchbar input').should('be.visible').type('{enter}');
cy.get('#searchbar input').type('{enter}');
});
it('should redirect on clicking a invoice', () => {
@ -21,7 +21,7 @@ describe('InvoiceInList', () => {
cy.url().should('include', `/invoice-in/${id}/summary`);
});
});
// https://redmine.verdnatura.es/issues/8420
it('should open the details', () => {
cy.get(firstDetailBtn).click();
cy.get(summaryHeaders).eq(1).contains('Basic data');

View File

@ -1,6 +1,16 @@
/// <reference types="cypress" />
describe('InvoiceOut list', () => {
const serial = 'Española rapida';
const columnCheckbox =
'.bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner';
const firstRowDescriptor =
'tbody > :nth-child(1) > [data-col-field="clientFk"] > .no-padding > .link';
const firstRowCheckbox =
'tbody > :nth-child(1) > :nth-child(1) > .q-checkbox > .q-checkbox__inner ';
const summaryPopupIcon = '.header > :nth-child(2) > .q-btn__content > .q-icon';
const filterBtn = '.q-scrollarea__content > .q-btn--standard > .q-btn__content';
const firstSummaryIcon =
':nth-child(1) > .text-right > [data-cy="tableAction-0"] > .q-btn__content > .q-icon';
beforeEach(() => {
cy.viewport(1920, 1080);
@ -9,18 +19,32 @@ describe('InvoiceOut list', () => {
cy.typeSearchbar('{enter}');
});
it('should search and filter an invoice and enter to the summary', () => {
cy.typeSearchbar('1{enter}');
cy.get('.q-virtual-scroll__content > :nth-child(2) > :nth-child(7)').click();
cy.get('.header > a.q-btn > .q-btn__content').click();
cy.typeSearchbar('{enter}');
cy.dataCy('InvoiceOutFilterAmountBtn').find('input').type('8.88{enter}');
it('should download one pdf from the subtoolbar button', () => {
cy.get(firstRowCheckbox).click();
cy.dataCy('InvoiceOutDownloadPdfBtn').click();
});
it('should download all pdfs', () => {
cy.get('.bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner').click();
cy.get(columnCheckbox).click();
cy.dataCy('InvoiceOutDownloadPdfBtn').click();
cy.get('.bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner').click();
});
it('should open the invoice descriptor from table icon', () => {
cy.get(firstSummaryIcon).click();
cy.get('.cardSummary').should('be.visible');
cy.get('.summaryHeader > div').should('include.text', 'A1111111');
});
it('should open the client descriptor', () => {
cy.get(firstRowDescriptor).click();
cy.get(summaryPopupIcon).click();
});
it('should filter the results by client ID, then check the first result is correct', () => {
cy.dataCy('Customer ID_input').type('1103');
cy.get(filterBtn).click();
cy.get(firstRowDescriptor).click();
cy.get('.q-item > .q-item__label').should('include.text', '1103');
});
it('should give an error when manual invoicing a ticket that is already invoiced', () => {
@ -31,11 +55,14 @@ describe('InvoiceOut list', () => {
cy.checkNotification('This ticket is already invoiced');
});
it('should create a manual invoice and enter to its summary', () => {
it('should create a manual invoice and enter to its summary, then delete that invoice', () => {
cy.dataCy('vnTableCreateBtn').click();
cy.dataCy('InvoiceOutCreateTicketinput').type(8);
cy.dataCy('InvoiceOutCreateTicketinput').type(9);
cy.selectOption('[data-cy="InvoiceOutCreateSerialSelect"]', serial);
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data created');
cy.dataCy('descriptor-more-opts').click();
cy.get('.q-menu > .q-list > :nth-child(4)').click();
cy.dataCy('VnConfirm_confirm').click();
});
});

View File

@ -1,11 +1,26 @@
/// <reference types="cypress" />
describe('InvoiceOut negative bases', () => {
const getDescriptors = (opt) =>
`:nth-child(1) > [data-col-field="${opt}"] > .no-padding > .link`;
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit(`/#/invoice-out/negative-bases`);
});
it('should open the posible descriptors', () => {
cy.get(getDescriptors('clientId')).click();
cy.get('.descriptor').should('be.visible');
cy.get('.q-item > .q-item__label').should('include.text', '1101');
cy.get(getDescriptors('ticketFk')).click();
cy.get('.descriptor').should('be.visible');
cy.get('.q-item > .q-item__label').should('include.text', '23');
cy.get(getDescriptors('workerName')).click();
cy.get('.descriptor').should('be.visible');
cy.get('.q-item > .q-item__label').should('include.text', '18');
});
it('should filter and download as CSV', () => {
cy.get('input[name="ticketFk"]').type('23{enter}');
cy.get('#subToolbar > .q-btn').click();

View File

@ -5,40 +5,91 @@ describe('InvoiceOut summary', () => {
Type: { val: 'Error in customer data', type: 'select' },
};
const firstRowDescriptors = (opt) =>
`tbody > :nth-child(1) > :nth-child(${opt}) > .q-btn`;
const toCustomerSummary = '[href="#/customer/1101"]';
const toTicketList = '[href="#/ticket/list?table={%22refFk%22:%22T1111111%22}"]';
const selectMenuOption = (opt) => `.q-menu > .q-list > :nth-child(${opt})`;
const confirmSend = '.q-btn--unelevated';
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit(`/#/invoice-out/list`);
cy.visit(`/#/invoice-out/1/summary`);
});
it('should generate the invoice PDF', () => {
cy.typeSearchbar('T1111111{enter}');
cy.dataCy('descriptor-more-opts').click();
cy.get('.q-menu > .q-list > :nth-child(6)').click();
cy.dataCy('VnConfirm_confirm').click();
cy.checkNotification('The invoice PDF document has been regenerated');
it('open the descriptors', () => {
cy.get(firstRowDescriptors(1)).click();
cy.get('.descriptor').should('be.visible');
cy.get('.q-item > .q-item__label').should('include.text', '1');
cy.get(firstRowDescriptors(2)).click();
cy.get('.descriptor').should('be.visible');
cy.get('.q-item > .q-item__label').should('include.text', '1101');
});
it('should refund the invoice ', () => {
it('should open the client summary and the ticket list', () => {
cy.get(toCustomerSummary).click();
cy.get('.descriptor').should('be.visible');
cy.get('.q-item > .q-item__label').should('include.text', '1101');
});
it('should open the ticket list', () => {
cy.get(toTicketList).click();
cy.get('.descriptor').should('be.visible');
cy.dataCy('vnFilterPanelChip').should('include.text', 'T1111111');
});
it('should transfer the invoice ', () => {
cy.typeSearchbar('T1111111{enter}');
cy.dataCy('descriptor-more-opts').click();
cy.get('.q-menu > .q-list > :nth-child(7)').click();
cy.get('#q-portal--menu--3 > .q-menu > .q-list > :nth-child(2)').click();
cy.checkNotification('The following refund ticket have been created');
cy.get(selectMenuOption(1)).click();
cy.fillInForm(transferInvoice);
cy.get('.q-mt-lg > .q-btn').click();
cy.checkNotification('Transferred invoice');
});
it('should send the invoice as PDF', () => {
cy.dataCy('descriptor-more-opts').click();
cy.get(selectMenuOption(3)).click();
cy.dataCy('InvoiceOutDescriptorMenuSendPdfOption').click();
cy.get(confirmSend).click();
cy.checkNotification('Notification sent');
});
it('should send the invoice as CSV', () => {
cy.dataCy('descriptor-more-opts').click();
cy.get(selectMenuOption(3)).click();
cy.dataCy('InvoiceOutDescriptorMenuSendCsvOption').click();
cy.get(confirmSend).click();
cy.checkNotification('Notification sent');
});
it('should delete an invoice ', () => {
cy.typeSearchbar('T2222222{enter}');
cy.dataCy('descriptor-more-opts').click();
cy.get('.q-menu > .q-list > :nth-child(4)').click();
cy.get(selectMenuOption(4)).click();
cy.dataCy('VnConfirm_confirm').click();
cy.checkNotification('InvoiceOut deleted');
});
it('should transfer the invoice ', () => {
cy.typeSearchbar('T1111111{enter}');
it('should book the invoice', () => {
cy.dataCy('descriptor-more-opts').click();
cy.get('.q-menu > .q-list > :nth-child(1)').click();
cy.fillInForm(transferInvoice);
cy.get('.q-mt-lg > .q-btn').click();
cy.checkNotification('Transferred invoice');
cy.get(selectMenuOption(5)).click();
cy.dataCy('VnConfirm_confirm').click();
cy.checkNotification('InvoiceOut booked');
});
it('should generate the invoice PDF', () => {
cy.dataCy('descriptor-more-opts').click();
cy.get(selectMenuOption(6)).click();
cy.dataCy('VnConfirm_confirm').click();
cy.checkNotification('The invoice PDF document has been regenerated');
});
it('should refund the invoice ', () => {
cy.dataCy('descriptor-more-opts').click();
cy.get(selectMenuOption(7)).click();
cy.get('#q-portal--menu--3 > .q-menu > .q-list > :nth-child(2)').click();
cy.checkNotification('The following refund ticket have been created');
});
});

View File

@ -11,7 +11,7 @@ describe('Handle Items FixedPrice', () => {
cy.visit('/#/item/fixed-price', { timeout: 5000 });
cy.waitForElement('.q-table');
cy.get(
'.q-header > .q-toolbar > :nth-child(1) > .q-btn__content > .q-icon'
'.q-header > .q-toolbar > :nth-child(1) > .q-btn__content > .q-icon',
).click();
});
it.skip('filter', function () {
@ -38,7 +38,7 @@ describe('Handle Items FixedPrice', () => {
cy.get('.q-gutter-x-sm > .q-btn > .q-btn__content > .q-icon').click();
cy.get(`${firstRow} > .text-right > .q-btn > .q-btn__content > .q-icon`).click();
cy.get(
'.q-card__actions > .q-btn--unelevated > .q-btn__content > .block'
'.q-card__actions > .q-btn--unelevated > .q-btn__content > .block',
).click();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});
@ -56,7 +56,7 @@ describe('Handle Items FixedPrice', () => {
cy.get(' .bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner ').click();
cy.get('#subToolbar > .q-btn--flat').click();
cy.get(
'.q-card__actions > .q-btn--unelevated > .q-btn__content > .block'
'.q-card__actions > .q-btn--unelevated > .q-btn__content > .block',
).click();
cy.get('.q-notification__message').should('have.text', 'Data saved');
});

View File

@ -15,6 +15,7 @@ describe('Item list', () => {
cy.get('.q-menu .q-item').contains('Anthurium').click();
cy.get('.q-virtual-scroll__content > :nth-child(4) > :nth-child(4)').click();
});
// https://redmine.verdnatura.es/issues/8421
it.skip('should create an item', () => {
const data = {
@ -28,7 +29,7 @@ describe('Item list', () => {
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data created');
cy.get(
':nth-child(2) > .q-drawer > .q-drawer__content > .q-scrollarea > .q-scrollarea__container > .q-scrollarea__content'
':nth-child(2) > .q-drawer > .q-drawer__content > .q-scrollarea > .q-scrollarea__container > .q-scrollarea__content',
).should('be.visible');
});
});

View File

@ -1,36 +1,33 @@
/// <reference types="cypress" />
describe('Item tag', () => {
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit(`/#/item/1/tags`);
cy.get('.q-page').should('be.visible');
cy.waitForElement('[data-cy="itemTags"]');
});
const createNewTag = 'createNewTag';
const saveBtn = 'crudModelDefaultSaveBtn';
const newTag = 'tagundefined';
it('should throw an error adding an existent tag', () => {
cy.get('.q-page').should('be.visible');
cy.get('.q-page-sticky > div').click();
cy.get('.q-page-sticky > div').click();
cy.dataCy('Tag_select').eq(7).type('Tallos');
cy.get('.q-menu .q-item').contains('Tallos').click();
cy.get(':nth-child(8) > [label="Value"]').type('1');
cy.dataCy('crudModelDefaultSaveBtn').click();
cy.dataCy(createNewTag).click();
cy.dataCy(newTag).should('be.visible').click().type('Genero{enter}');
cy.dataCy('tagGeneroValue').eq(1).should('be.visible');
cy.dataCy(saveBtn).click();
cy.checkNotification("The tag or priority can't be repeated for an item");
});
it('should add a new tag', () => {
cy.get('.q-page').should('be.visible');
cy.get('.q-page-sticky > div').click();
cy.get('.q-page-sticky > div').click();
cy.dataCy('Tag_select').eq(7).click();
cy.get('.q-menu .q-item').contains('Ancho de la base').type('{enter}');
cy.get(':nth-child(8) > [label="Value"]').type('50');
cy.dataCy('crudModelDefaultSaveBtn').click();
cy.dataCy(createNewTag).click();
cy.dataCy(newTag).should('be.visible').click().type('Forma{enter}');
cy.dataCy('tagFormaValue').should('be.visible').type('50');
cy.dataCy(saveBtn).click();
cy.checkNotification('Data saved');
cy.dataCy('itemTags')
.children(':nth-child(8)')
.find('.justify-center > .q-icon')
.click();
cy.dataCy('VnConfirm_confirm').click();
cy.dataCy('deleteTagForma').should('be.visible').click();
cy.dataCy('VnConfirm_confirm').should('be.visible').click();
cy.checkNotification('Data saved');
});
});

View File

@ -24,7 +24,7 @@ describe('Recover Password', () => {
it('should change password to user', () => {
// Get token from mail
cy.request(
`http://localhost:3000/api/Mails?filter=%7B%22where%22%3A%20%7B%22receiver%22%3A%20%22${username}%40mydomain.com%22%7D%2C%20%22order%22%3A%20%5B%22id%20DESC%22%5D%7D&access_token=DEFAULT_TOKEN`
`/api/Mails?filter=%7B%22where%22%3A%20%7B%22receiver%22%3A%20%22${username}%40mydomain.com%22%7D%2C%20%22order%22%3A%20%5B%22id%20DESC%22%5D%7D&access_token=DEFAULT_TOKEN`
).then((response) => {
const regex = /access_token=([a-zA-Z0-9]+)/;
const [match] = response.body[0].body.match(regex);

View File

@ -11,7 +11,7 @@ describe('Two Factor', () => {
it('should enable two factor to sysadmin', () => {
cy.request(
'PATCH',
`http://localhost:3000/api/VnUsers/${userId}/update-user?access_token=DEFAULT_TOKEN`,
`/api/VnUsers/${userId}/update-user?access_token=DEFAULT_TOKEN`,
{ twoFactor: 'email' }
);
});
@ -41,7 +41,7 @@ describe('Two Factor', () => {
// Get code from mail
cy.request(
`http://localhost:3000/api/Mails?filter=%7B%22where%22%3A%20%7B%22receiver%22%3A%20%22${username}%40mydomain.com%22%7D%2C%20%22order%22%3A%20%5B%22id%20DESC%22%5D%7D&access_token=DEFAULT_TOKEN`
`/api/Mails?filter=%7B%22where%22%3A%20%7B%22receiver%22%3A%20%22${username}%40mydomain.com%22%7D%2C%20%22order%22%3A%20%5B%22id%20DESC%22%5D%7D&access_token=DEFAULT_TOKEN`
).then((response) => {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = response.body[0].body;

View File

@ -1,23 +0,0 @@
/// <reference types="cypress" />
describe('ParkingBasicData', () => {
const codeInput = 'form .q-card .q-input input';
const sectorSelect = 'form .q-card .q-select input';
const sectorOpt = '.q-menu .q-item';
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/shelving/parking/1/basic-data`);
});
it('should edit the code and sector', () => {
cy.get(sectorSelect).type('Second');
cy.get(sectorOpt).click();
cy.get(codeInput).eq(0).clear();
cy.get(codeInput).eq(0).type('900-001');
cy.saveCard();
cy.get(sectorSelect).should('have.value', 'Second sector');
cy.get(codeInput).should('have.value', '900-001');
});
});

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