0
0
Fork 0
This commit is contained in:
Alex Moreno 2024-02-22 09:11:36 +01:00
commit 9abac4a8fb
35 changed files with 6372 additions and 9530 deletions

View File

@ -2,4 +2,4 @@ FROM node:stretch-slim
RUN npm install -g @quasar/cli RUN npm install -g @quasar/cli
WORKDIR /app WORKDIR /app
COPY dist/spa ./ COPY dist/spa ./
CMD ["quasar", "serve", "./", "--history", "--hostname", "0.0.0.0"] CMD ["quasar", "serve", "./", "--history", "--hostname", "0.0.0.0"]

130
Jenkinsfile vendored
View File

@ -1,99 +1,123 @@
#!/usr/bin/env groovy #!/usr/bin/env groovy
def PROTECTED_BRANCH
def BRANCH_ENV = [
test: 'test',
master: 'production'
]
node {
stage('Setup') {
env.FRONT_REPLICAS = 1
env.NODE_ENV = BRANCH_ENV[env.BRANCH_NAME] ?: 'dev'
PROTECTED_BRANCH = [
'dev',
'test',
'master'
].contains(env.BRANCH_NAME)
// https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
echo "NODE_NAME: ${env.NODE_NAME}"
echo "WORKSPACE: ${env.WORKSPACE}"
configFileProvider([
configFile(fileId: 'salix-front.properties',
variable: 'PROPS_FILE')
]) {
def props = readProperties file: PROPS_FILE
props.each {key, value -> env."${key}" = value }
props.each {key, value -> echo "${key}: ${value}" }
}
if (PROTECTED_BRANCH) {
configFileProvider([
configFile(fileId: "salix-front.branch.${env.BRANCH_NAME}",
variable: 'BRANCH_PROPS_FILE')
]) {
def props = readProperties file: BRANCH_PROPS_FILE
props.each {key, value -> env."${key}" = value }
props.each {key, value -> echo "${key}: ${value}" }
}
}
}
}
pipeline { pipeline {
agent any agent any
options { options {
disableConcurrentBuilds() disableConcurrentBuilds()
} }
tools {
nodejs 'node-v18'
}
environment { environment {
PROJECT_NAME = 'lilium' PROJECT_NAME = 'lilium'
STACK_NAME = "${env.PROJECT_NAME}-${env.BRANCH_NAME}" STACK_NAME = "${env.PROJECT_NAME}-${env.BRANCH_NAME}"
} }
stages { stages {
stage('Checkout') {
steps {
script {
switch (env.BRANCH_NAME) {
case 'master':
env.NODE_ENV = 'production'
env.FRONT_REPLICAS = 2
break
case 'test':
env.NODE_ENV = 'test'
env.FRONT_REPLICAS = 1
break
}
}
setEnv()
}
}
stage('Install') { stage('Install') {
environment { environment {
NODE_ENV = "" NODE_ENV = ""
} }
steps { steps {
nodejs('node-v18') { sh 'npm install --prefer-offline'
sh 'npm install --no-audit --prefer-offline'
}
} }
} }
stage('Test') { stage('Test') {
when { not { anyOf { when {
branch 'test' expression { !PROTECTED_BRANCH }
branch 'master' }
}}}
environment { environment {
NODE_ENV = "" NODE_ENV = ""
} }
parallel { steps {
stage('Frontend') { sh 'npm run test:unit:ci'
steps { }
nodejs('node-v18') { post {
sh 'npm run test:unit:ci' always {
script {
try {
junit 'junitresults.xml'
junit 'junit.xml'
} catch (e) {
echo e.toString()
} }
} }
} }
} }
} }
stage('Build') { stage('Build') {
when { anyOf { when {
branch 'test' expression { PROTECTED_BRANCH }
branch 'master' }
}}
environment { environment {
CREDENTIALS = credentials('docker-registry') CREDENTIALS = credentials('docker-registry')
} }
steps { steps {
nodejs('node-v18') { sh 'quasar build'
sh 'quasar build' script {
def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version
} }
dockerBuild() dockerBuild()
} }
} }
stage('Deploy') { stage('Deploy') {
when { anyOf { when {
branch 'test' expression { PROTECTED_BRANCH }
branch 'master' }
}}
environment { environment {
DOCKER_HOST = "${env.SWARM_HOST}" DOCKER_HOST = "${env.SWARM_HOST}"
} }
steps { steps {
script {
def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version
}
sh "docker stack deploy --with-registry-auth --compose-file docker-compose.yml ${env.STACK_NAME}" sh "docker stack deploy --with-registry-auth --compose-file docker-compose.yml ${env.STACK_NAME}"
} }
} }
} }
post {
always {
script {
if (!['master', 'test'].contains(env.BRANCH_NAME)) {
try {
junit 'junitresults.xml'
junit 'junit.xml'
} catch (e) {
echo e.toString()
}
}
}
}
}
} }

View File

@ -5,13 +5,13 @@ Lilium frontend
## Install the dependencies ## Install the dependencies
```bash ```bash
npm install bun install
``` ```
### Install quasar cli ### Install quasar cli
```bash ```bash
sudo npm install -g @quasar/cli sudo bun install -g @quasar/cli
``` ```
### Start the app in development mode (hot-code reloading, error reporting, etc.) ### Start the app in development mode (hot-code reloading, error reporting, etc.)
@ -23,7 +23,7 @@ quasar dev
### Run unit tests ### Run unit tests
```bash ```bash
npm run test:unit bun run test:unit
``` ```
### Run e2e tests ### Run e2e tests

BIN
bun.lockb Executable file

Binary file not shown.

9264
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "24.8.0", "version": "24.10.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",
@ -46,7 +46,8 @@
"engines": { "engines": {
"node": "^20 || ^18 || ^16", "node": "^20 || ^18 || ^16",
"npm": ">= 8.1.2", "npm": ">= 8.1.2",
"yarn": ">= 1.21.1" "yarn": ">= 1.21.1",
"bun": ">= 1.0.25"
}, },
"overrides": { "overrides": {
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",

5654
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -50,7 +50,10 @@ const value = computed({
return $props.modelValue; return $props.modelValue;
}, },
set(value) { set(value) {
emit('update:modelValue', value); emit(
'update:modelValue',
postcodesOptions.value.find((p) => p.code === value)
);
}, },
}); });
@ -101,16 +104,11 @@ function handleFetch(data) {
:label="t('Location')" :label="t('Location')"
:placeholder="t('search_by_postalcode')" :placeholder="t('search_by_postalcode')"
@input-value="locationFilter" @input-value="locationFilter"
:default-filter="true" :default-filter="false"
:input-debounce="300" :input-debounce="300"
:class="{ required: $attrs.required }" :class="{ required: $attrs.required }"
v-bind="$attrs" v-bind="$attrs"
emit-value
map-options
use-input
clearable clearable
hide-selected
fill-input
> >
<template #form> <template #form>
<CreateNewPostcode @on-data-saved="locationFilter()" /> <CreateNewPostcode @on-data-saved="locationFilter()" />

View File

@ -116,14 +116,14 @@ async function fetchFilter(val) {
} }
async function filterHandler(val, update) { async function filterHandler(val, update) {
if (!$props.defaultFilter) return update();
let newOptions;
if ($props.url) {
newOptions = await fetchFilter(val);
} else newOptions = filter(val, myOptionsOriginal.value);
update( update(
async () => { () => {
if (!$props.defaultFilter) return; myOptions.value = newOptions;
if ($props.url) {
myOptions.value = await fetchFilter(val);
return;
}
myOptions.value = filter(val, myOptionsOriginal.value);
}, },
(ref) => { (ref) => {
if (val !== '' && ref.options.length > 0) { if (val !== '' && ref.options.length > 0) {

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, useSlots, watch, computed } from 'vue'; import { onMounted, useSlots, watch, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue'; import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
@ -39,6 +39,7 @@ const slots = useSlots();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const entity = computed(() => useArrayData($props.dataKey).store.data); const entity = computed(() => useArrayData($props.dataKey).store.data);
const isLoading = ref(false);
defineExpose({ defineExpose({
getData, getData,
@ -60,15 +61,20 @@ async function getData() {
filter: $props.filter, filter: $props.filter,
skip: 0, skip: 0,
}); });
const { data } = await arrayData.fetch({ append: false, updateRouter: false }); isLoading.value = true;
emit('onFetch', data); try {
const { data } = await arrayData.fetch({ append: false, updateRouter: false });
emit('onFetch', data);
} finally {
isLoading.value = false;
}
} }
const emit = defineEmits(['onFetch']); const emit = defineEmits(['onFetch']);
</script> </script>
<template> <template>
<div class="descriptor"> <div class="descriptor">
<template v-if="entity"> <template v-if="entity && !isLoading">
<div class="header bg-primary q-pa-sm justify-between"> <div class="header bg-primary q-pa-sm justify-between">
<slot name="header-extra-action" /> <slot name="header-extra-action" />
<QBtn <QBtn
@ -154,8 +160,13 @@ const emit = defineEmits(['onFetch']);
<slot name="after" /> <slot name="after" />
</template> </template>
<!-- Skeleton --> <!-- Skeleton -->
<SkeletonDescriptor v-if="!entity" /> <SkeletonDescriptor v-if="!entity || isLoading" />
</div> </div>
<QInnerLoading
:label="t('globals.pleaseWait')"
:showing="isLoading"
color="primary"
/>
</template> </template>
<style lang="scss"> <style lang="scss">
@ -210,11 +221,8 @@ const emit = defineEmits(['onFetch']);
margin-bottom: 15px; margin-bottom: 15px;
} }
.list-box { .list-box {
width: 90%;
background-color: var(--vn-gray); background-color: var(--vn-gray);
margin: 10px auto;
padding: 10px 5px 10px 0px;
border-radius: 8px;
.q-item__label { .q-item__label {
color: var(--vn-label); color: var(--vn-label);
} }

View File

@ -90,17 +90,16 @@ watch(props, async () => {
background-color: var(--vn-gray); background-color: var(--vn-gray);
> .q-card.vn-one { > .q-card.vn-one {
width: 350px;
flex: 1; flex: 1;
} }
> .q-card.vn-two { > .q-card.vn-two {
flex: 2; flex: 40%;
} }
> .q-card.vn-three { > .q-card.vn-three {
flex: 4; flex: 75%;
} }
> .q-card.vn-max { > .q-card.vn-max {
width: 100%; flex: 100%;
} }
> .q-card { > .q-card {

View File

@ -1,10 +1,39 @@
<template> <template>
<div id="descriptor-skeleton"> <div id="descriptor-skeleton">
<div class="col q-pl-sm q-pa-sm"> <div class="row justify-between q-pa-sm">
<QSkeleton type="text" square height="45px" /> <QSkeleton square size="40px" />
<QSkeleton type="text" square height="18px" /> <QSkeleton square size="40px" />
<QSkeleton type="text" square height="18px" /> <QSkeleton square height="40px" width="20px" />
<QSkeleton type="text" square height="18px" /> </div>
<div class="col justify-between q-pa-sm q-gutter-y-xs">
<QSkeleton square height="40px" width="150px" />
<QSkeleton square height="30px" width="70px" />
</div>
<div class="col q-pl-sm q-pa-sm q-mb-md">
<div class="row justify-between">
<QSkeleton type="text" square height="30px" width="20%" />
<QSkeleton type="text" square height="30px" width="60%" />
</div>
<div class="row justify-between">
<QSkeleton type="text" square height="30px" width="20%" />
<QSkeleton type="text" square height="30px" width="60%" />
</div>
<div class="row justify-between">
<QSkeleton type="text" square height="30px" width="20%" />
<QSkeleton type="text" square height="30px" width="60%" />
</div>
<div class="row justify-between">
<QSkeleton type="text" square height="30px" width="20%" />
<QSkeleton type="text" square height="30px" width="60%" />
</div>
<div class="row justify-between">
<QSkeleton type="text" square height="30px" width="20%" />
<QSkeleton type="text" square height="30px" width="60%" />
</div>
<div class="row justify-between">
<QSkeleton type="text" square height="30px" width="20%" />
<QSkeleton type="text" square height="30px" width="60%" />
</div>
</div> </div>
<QCardActions> <QCardActions>

View File

@ -1,7 +1,8 @@
<script setup> <script setup>
import { onMounted, ref, computed } from 'vue'; import { onMounted, ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useRoute } from 'vue-router';
import toDate from 'filters/toDate'; import toDate from 'filters/toDate';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
@ -52,6 +53,7 @@ const emit = defineEmits(['refresh', 'clear', 'search', 'init', 'remove']);
const arrayData = useArrayData(props.dataKey, { const arrayData = useArrayData(props.dataKey, {
exprBuilder: props.exprBuilder, exprBuilder: props.exprBuilder,
}); });
const route = useRoute();
const store = arrayData.store; const store = arrayData.store;
const userParams = ref({}); const userParams = ref({});
@ -63,6 +65,18 @@ onMounted(() => {
emit('init', { params: userParams.value }); emit('init', { params: userParams.value });
}); });
watch(
() => route.query.params,
(val) => {
if (!val) {
userParams.value = {};
} else {
const parsedParams = JSON.parse(val);
userParams.value = { ...parsedParams };
}
}
);
const isLoading = ref(false); const isLoading = ref(false);
async function search() { async function search() {
isLoading.value = true; isLoading.value = true;

View File

@ -39,14 +39,14 @@ async function insert() {
ref="vnPaginateRef" ref="vnPaginateRef"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<QCard class="q-pa-md q-mb-md" v-for="(note, index) in rows" :key="index"> <QCard class="q-pa-xs q-mb-md" v-for="(note, index) in rows" :key="index">
<QCardSection horizontal> <QCardSection horizontal>
<slot name="picture"> <slot name="picture">
<VnAvatar :descriptor="false" :worker-id="note.workerFk" /> <VnAvatar :descriptor="false" :worker-id="note.workerFk" />
</slot> </slot>
<QItem class="full-width justify-between items-start"> <QItem class="full-width justify-between items-start">
<VnUserLink <VnUserLink
:name="`${note.worker.firstName} ${note.worker.lastName}`" :name="`${note.worker.user.nickname}`"
:worker-id="note.worker.id" :worker-id="note.worker.id"
/> />
@ -55,7 +55,7 @@ async function insert() {
</slot> </slot>
</QItem> </QItem>
</QCardSection> </QCardSection>
<QCardSection> <QCardSection class="q-pa-sm">
<slot name="text"> <slot name="text">
{{ note.text }} {{ note.text }}
</slot> </slot>
@ -63,15 +63,8 @@ async function insert() {
</QCard> </QCard>
</template> </template>
</VnPaginate> </VnPaginate>
<QPageSticky position="bottom-right" :offset="[25, 25]"> <QPageSticky position="bottom-right" :offset="[25, 25]" v-if="addNote">
<QBtn <QBtn color="primary" icon="add" size="lg" round @click="noteModal = true" />
v-if="addNote"
color="primary"
icon="add"
size="lg"
round
@click="noteModal = true"
/>
</QPageSticky> </QPageSticky>
<QDialog v-model="noteModal" @hide="newNote = ''"> <QDialog v-model="noteModal" @hide="newNote = ''">
<QCard> <QCard>

View File

@ -1,20 +1,21 @@
<script setup> <script setup>
import { ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useState } from 'src/composables/useState';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue';
import CreateDepartmentChild from '../CreateDepartmentChild.vue'; import CreateDepartmentChild from '../CreateDepartmentChild.vue';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useRouter } from 'vue-router';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter();
const { notify } = useNotify(); const { notify } = useNotify();
const state = useState();
const router = useRouter();
const treeRef = ref(null); const treeRef = ref();
const showCreateNodeFormVal = ref(false); const showCreateNodeFormVal = ref(false);
const creationNodeSelectedId = ref(null); const creationNodeSelectedId = ref(null);
const expanded = ref([]); const expanded = ref([]);
@ -24,30 +25,35 @@ const nodes = ref([{ id: null, name: t('Departments'), sons: true, children: [{}
const fetchedChildrensSet = ref(new Set()); const fetchedChildrensSet = ref(new Set());
const onNodeExpanded = (nodeKeysArray) => { const onNodeExpanded = (nodeKeysArray) => {
// Verificar si el nodo ya fue expandido
if (!fetchedChildrensSet.value.has(nodeKeysArray.at(-1))) { if (!fetchedChildrensSet.value.has(nodeKeysArray.at(-1))) {
fetchedChildrensSet.value.add(nodeKeysArray.at(-1)); fetchedChildrensSet.value.add(nodeKeysArray.at(-1));
fetchNodeLeaves(nodeKeysArray.at(-1)); // Llamar a la función para obtener los nodos hijos fetchNodeLeaves(nodeKeysArray.at(-1));
} }
state.set('Tree', nodeKeysArray);
}; };
const fetchNodeLeaves = async (nodeKey) => { const fetchNodeLeaves = async (nodeKey) => {
try { try {
const node = treeRef.value.getNodeByKey(nodeKey); const node = treeRef.value?.getNodeByKey(nodeKey);
if (!node || node.sons === 0) return; if (!node || node.sons === 0) return;
const params = { parentId: node.id }; const params = { parentId: node.id };
const response = await axios.get('/departments/getLeaves', { params }); const response = await axios.get('/departments/getLeaves', { params });
// Si hay datos en la respuesta y tiene hijos, agregarlos al nodo actual
if (response.data) { if (response.data) {
node.children = response.data; node.children = response.data.map((n) => {
node.children.forEach((node) => { const hasChildrens = n.sons > 0;
if (node.sons) node.children = [{}];
n.children = hasChildrens ? [{}] : null;
n.clickable = true;
return n;
}); });
} }
state.set('Tree', node);
} catch (err) { } catch (err) {
console.error('Error fetching department leaves'); console.error('Error fetching department leaves', err);
throw new Error(); throw new Error();
} }
}; };
@ -84,10 +90,49 @@ const onNodeCreated = async () => {
await fetchNodeLeaves(creationNodeSelectedId.value); await fetchNodeLeaves(creationNodeSelectedId.value);
}; };
const redirectToDepartmentSummary = (id) => { onMounted(async (n) => {
if (!id) return; const tree = [...state.get('Tree'), 1];
router.push({ name: 'DepartmentSummary', params: { id } }); const lastStateTree = state.get('TreeState');
}; if (tree) {
for (let n of tree) {
await fetchNodeLeaves(n);
}
expanded.value = tree;
if (lastStateTree) {
tree.push(lastStateTree);
await fetchNodeLeaves(lastStateTree);
}
}
setTimeout(() => {
if (lastStateTree) {
document.getElementById(lastStateTree).scrollIntoView();
}
}, 1000);
});
function handleEvent(type, event, node) {
const isParent = node.sons > 0;
const lastId = isParent ? node.id : node.parentFk;
switch (type) {
case 'path':
state.set('TreeState', lastId);
node.id && router.push({ path: `/department/department/${node.id}/summary` });
break;
case 'tab':
state.set('TreeState', lastId);
node.id &&
window.open(`#/department/department/${node.id}/summary`, '_blank');
break;
default:
node.id &&
router.push({ path: `#/department/department/${node.id}/summary` });
break;
}
}
</script> </script>
<template> <template>
@ -99,15 +144,27 @@ const redirectToDepartmentSummary = (id) => {
label-key="name" label-key="name"
v-model:expanded="expanded" v-model:expanded="expanded"
@update:expanded="onNodeExpanded($event)" @update:expanded="onNodeExpanded($event)"
:default-expand-all="true"
> >
<template #default-header="{ node }"> <template #default-header="{ node }">
<div <div
class="row justify-between full-width q-pr-md cursor-pointer" :id="node.id"
@click.stop="redirectToDepartmentSummary(node.id)" class="qtr row justify-between full-width q-pr-md cursor-pointer"
> >
<span class="text-uppercase"> <div>
{{ node.name }} <span
</span> @click="handleEvent('row', $event, node)"
class="cursor-pointer"
>
{{ node.name }}
<DepartmentDescriptorProxy :id="node.id" />
</span>
</div>
<div
@click.stop.exact="handleEvent('path', $event, node)"
@click.ctrl.stop="handleEvent('tab', $event, node)"
style="flex-grow: 1; width: 10px"
></div>
<div class="row justify-between" style="max-width: max-content"> <div class="row justify-between" style="max-width: max-content">
<QIcon <QIcon
v-if="node.id" v-if="node.id"
@ -149,6 +206,11 @@ const redirectToDepartmentSummary = (id) => {
</QCard> </QCard>
</template> </template>
<style lang="scss" scoped>
span {
color: $primary;
}
</style>
<i18n> <i18n>
es: es:
Departments: Departamentos Departments: Departamentos

View File

@ -105,7 +105,8 @@ export function useArrayData(key, userOptions) {
for (const row of response.data) store.data.push(row); for (const row of response.data) store.data.push(row);
} else { } else {
store.data = response.data; store.data = response.data;
updateRouter && updateStateParams(); if (!document.querySelectorAll('[role="dialog"]'))
updateRouter && updateStateParams();
} }
store.isLoading = false; store.isLoading = false;

View File

@ -13,6 +13,7 @@
// Tip: Use the "Theme Builder" on Quasar's documentation website. // Tip: Use the "Theme Builder" on Quasar's documentation website.
$primary: #ec8916; $primary: #ec8916;
$primary-light: lighten($primary, 35%);
$secondary: #26a69a; $secondary: #26a69a;
$accent: #9c27b0; $accent: #9c27b0;
$white: #fff; $white: #fff;

View File

@ -552,6 +552,7 @@ export default {
responsible: 'Responsible', responsible: 'Responsible',
worker: 'Worker', worker: 'Worker',
redelivery: 'Redelivery', redelivery: 'Redelivery',
returnOfMaterial: 'RMA',
}, },
basicData: { basicData: {
customer: 'Customer', customer: 'Customer',

View File

@ -551,6 +551,7 @@ export default {
responsible: 'Responsable', responsible: 'Responsable',
worker: 'Trabajador', worker: 'Trabajador',
redelivery: 'Devolución', redelivery: 'Devolución',
returnOfMaterial: 'RMA',
}, },
basicData: { basicData: {
customer: 'Cliente', customer: 'Cliente',

View File

@ -135,7 +135,7 @@ async function regularizeClaim() {
message: t('globals.dataSaved'), message: t('globals.dataSaved'),
type: 'positive', type: 'positive',
}); });
await onUpdateGreugeAccept(); if (multiplicatorValue.value) await onUpdateGreugeAccept();
} }
async function onUpdateGreugeAccept() { async function onUpdateGreugeAccept() {

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate, toPercentage } from 'src/filters'; import { toDate, toPercentage } from 'src/filters';
@ -10,6 +10,7 @@ import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import { getUrl } from 'src/composables/getUrl';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -22,7 +23,7 @@ const $props = defineProps({
const route = useRoute(); const route = useRoute();
const state = useState(); const state = useState();
const { t } = useI18n(); const { t } = useI18n();
const salixUrl = ref();
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
@ -71,11 +72,10 @@ const filter = {
}; };
const STATE_COLOR = { const STATE_COLOR = {
pending: 'positive', pending: 'warning',
managed: 'warning', managed: 'info',
resolved: 'negative', resolved: 'positive',
}; };
function stateColor(code) { function stateColor(code) {
return STATE_COLOR[code]; return STATE_COLOR[code];
} }
@ -85,6 +85,9 @@ const setData = (entity) => {
data.value = useCardDescription(entity.client.name, entity.id); data.value = useCardDescription(entity.client.name, entity.id);
state.set('ClaimDescriptor', entity); state.set('ClaimDescriptor', entity);
}; };
onMounted(async () => {
salixUrl.value = await getUrl('');
});
</script> </script>
<template> <template>
@ -167,6 +170,20 @@ const setData = (entity) => {
> >
<QTooltip>{{ t('claim.card.claimedTicket') }}</QTooltip> <QTooltip>{{ t('claim.card.claimedTicket') }}</QTooltip>
</QBtn> </QBtn>
<QBtn
size="md"
icon="assignment"
color="primary"
:href="salixUrl + 'ticket/' + entity.ticketFk + '/sale-tracking'"
>
</QBtn>
<QBtn
size="md"
icon="visibility"
color="primary"
:href="salixUrl + 'ticket/' + entity.ticketFk + '/tracking/index'"
>
</QBtn>
</QCardActions> </QCardActions>
</template> </template>
</CardDescriptor> </CardDescriptor>

View File

@ -11,6 +11,7 @@ import CrudModel from 'components/CrudModel.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnDiscount from 'components/common/vnDiscount.vue'; import VnDiscount from 'components/common/vnDiscount.vue';
import ClaimLinesImport from './ClaimLinesImport.vue'; import ClaimLinesImport from './ClaimLinesImport.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
const quasar = useQuasar(); const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
@ -229,7 +230,14 @@ function showImportDialog() {
</QPopupEdit> </QPopupEdit>
</QTd> </QTd>
</template> </template>
<template #body-cell-description="{ row, value }">
<QTd auto-width align="right" class="text-primary">
{{ value }}
<ItemDescriptorProxy
:id="row.sale.itemFk"
></ItemDescriptorProxy>
</QTd>
</template>
<template #body-cell-discount="{ row, value, rowIndex }"> <template #body-cell-discount="{ row, value, rowIndex }">
<QTd auto-width align="right" class="text-primary"> <QTd auto-width align="right" class="text-primary">
{{ value }} {{ value }}

View File

@ -19,6 +19,12 @@ const claimFilter = {
relation: 'worker', relation: 'worker',
scope: { scope: {
fields: ['id', 'firstName', 'lastName'], fields: ['id', 'firstName', 'lastName'],
include: {
relation: 'user',
scope: {
fields: ['id', 'nickname'],
},
},
}, },
}, },
}; };
@ -30,7 +36,8 @@ const body = {
</script> </script>
<template> <template>
<div class="column items-center"> <div class="column items-center">
<VnNotes style="overflow-y: scroll;" <VnNotes
style="overflow-y: scroll"
:add-note="$props.addNote" :add-note="$props.addNote"
:id="id" :id="id"
url="claimObservations" url="claimObservations"

View File

@ -10,6 +10,7 @@ import { useSession } from 'src/composables/useSession';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import ClaimNotes from 'src/pages/Claim/Card/ClaimNotes.vue'; import ClaimNotes from 'src/pages/Claim/Card/ClaimNotes.vue';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -42,11 +43,6 @@ onMounted(async () => {
claimUrl.value = salixUrl.value + `claim/${entityId.value}/`; claimUrl.value = salixUrl.value + `claim/${entityId.value}/`;
}); });
watch(entityId, async (id) => {
claimDmsFilter.value.where = { claimFk: id };
await claimDmsRef.value.fetch();
});
const detailsColumns = ref([ const detailsColumns = ref([
{ {
name: 'item', name: 'item',
@ -101,11 +97,9 @@ const detailsColumns = ref([
]); ]);
const STATE_COLOR = { const STATE_COLOR = {
pending: 'positive', pending: 'warning',
managed: 'info',
managed: 'warning', resolved: 'positive',
resolved: 'negative',
}; };
function stateColor(code) { function stateColor(code) {
return STATE_COLOR[code]; return STATE_COLOR[code];
@ -170,7 +164,6 @@ function openDialog(dmsId) {
:filter="claimDmsFilter" :filter="claimDmsFilter"
@on-fetch="(data) => setClaimDms(data)" @on-fetch="(data) => setClaimDms(data)"
limit="20" limit="20"
auto-load
ref="claimDmsRef" ref="claimDmsRef"
/> />
<CardSummary ref="summary" :url="`Claims/${entityId}/getSummary`"> <CardSummary ref="summary" :url="`Claims/${entityId}/getSummary`">
@ -210,15 +203,29 @@ function openDialog(dmsId) {
/> />
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('claim.summary.customer')">
<template #value>
<VnUserLink
:name="claim.client?.name"
:worker-id="claim.client?.id"
/>
</template>
</VnLv>
<VnLv :label="t('claim.summary.returnOfMaterial')" :value="claim.rma" />
<QCheckbox
:align-items="right"
:label="t('claim.basicData.picked')"
v-model="claim.hasToPickUp"
/>
</QCard> </QCard>
<QCard class="vn-max claimVnNotes"> <QCard class="vn-three claimVnNotes full-height">
<a class="header" :href="`#/claim/${entityId}/notes`"> <a class="header" :href="`#/claim/${entityId}/notes`">
{{ t('claim.summary.notes') }} {{ t('claim.summary.notes') }}
<QIcon name="open_in_new" color="primary" /> <QIcon name="open_in_new" color="primary" />
</a> </a>
<ClaimNotes :add-note="false" style="height: 350px" order="created ASC" /> <ClaimNotes :add-note="false" style="height: 350px" order="created ASC" />
</QCard> </QCard>
<QCard class="vn-max" v-if="salesClaimed.length > 0"> <QCard class="vn-two" v-if="salesClaimed.length > 0">
<a class="header" :href="`#/claim/${entityId}/lines`"> <a class="header" :href="`#/claim/${entityId}/lines`">
{{ t('claim.summary.details') }} {{ t('claim.summary.details') }}
<QIcon name="open_in_new" color="primary" /> <QIcon name="open_in_new" color="primary" />
@ -231,8 +238,43 @@ function openDialog(dmsId) {
</QTh> </QTh>
</QTr> </QTr>
</template> </template>
<template #body="props">
<QTr :props="props">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
<span v-if="col.name != 'description'">{{
t(col.value)
}}</span>
<QBtn
v-if="col.name == 'description'"
flat
color="blue"
>{{ col.value }}</QBtn
>
<ItemDescriptorProxy
v-if="col.name == 'description'"
:id="2"
></ItemDescriptorProxy>
</QTh>
</QTr>
</template>
</QTable> </QTable>
</QCard> </QCard>
<QCard class="vn-two" v-if="developments.length > 0">
<a class="header" :href="claimUrl + 'development'">
{{ t('claim.summary.development') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QTable :columns="developmentColumns" :rows="developments" flat>
<template #header="props">
<QTr :props="props">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</QTh>
</QTr>
</template>
</QTable>
</QCard>
<QCard class="vn-max" v-if="claimDms.length > 0"> <QCard class="vn-max" v-if="claimDms.length > 0">
<a class="header" :href="`#/claim/${entityId}/photos`"> <a class="header" :href="`#/claim/${entityId}/photos`">
{{ t('claim.summary.photos') }} {{ t('claim.summary.photos') }}
@ -275,22 +317,8 @@ function openDialog(dmsId) {
</div> </div>
</div> </div>
</QCard> </QCard>
<QCard class="vn-two" v-if="developments.length > 0">
<a class="header" :href="claimUrl + 'development'"> <QCard class="vn-max">
{{ t('claim.summary.development') }}
<QIcon name="open_in_new" color="primary" />
</a>
<QTable :columns="developmentColumns" :rows="developments" flat>
<template #header="props">
<QTr :props="props">
<QTh v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</QTh>
</QTr>
</template>
</QTable>
</QCard>
<QCard class="vn-max" v-if="developments.length > 0">
<a class="header" :href="claimUrl + 'action'"> <a class="header" :href="claimUrl + 'action'">
{{ t('claim.summary.actions') }} {{ t('claim.summary.actions') }}
<QIcon name="open_in_new" color="primary" /> <QIcon name="open_in_new" color="primary" />

View File

@ -12,6 +12,7 @@ import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorP
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import ClaimSummary from './Card/ClaimSummary.vue'; import ClaimSummary from './Card/ClaimSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { getUrl } from 'src/composables/getUrl';
const stateStore = useStateStore(); const stateStore = useStateStore();
const router = useRouter(); const router = useRouter();
@ -19,14 +20,19 @@ const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const STATE_COLOR = { const STATE_COLOR = {
pending: 'positive', pending: 'warning',
managed: 'warning', managed: 'info',
resolved: 'negative', resolved: 'positive',
}; };
function getApiUrl() {
return new URL(window.location).origin;
}
function stateColor(code) { function stateColor(code) {
return STATE_COLOR[code]; return STATE_COLOR[code];
} }
function navigate(id) { function navigate(event, id) {
if (event.ctrlKey || event.metaKey)
return window.open(`${getApiUrl()}/#/claim/${id}/summary`);
router.push({ path: `/claim/${id}` }); router.push({ path: `/claim/${id}` });
} }
</script> </script>
@ -66,7 +72,7 @@ function navigate(id) {
<VnPaginate <VnPaginate
data-key="ClaimList" data-key="ClaimList"
url="Claims/filter" url="Claims/filter"
order="claimStateFk" :order="['priority ASC', 'created DESC']"
auto-load auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
@ -74,7 +80,7 @@ function navigate(id) {
:id="row.id" :id="row.id"
:key="row.id" :key="row.id"
:title="row.clientName" :title="row.clientName"
@click="navigate(row.id)" @click="navigate($event, row.id)"
v-for="row of rows" v-for="row of rows"
> >
<template #list-items> <template #list-items>

View File

@ -19,10 +19,8 @@ const $props = defineProps({
default: null, default: null,
}, },
}); });
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });

View File

@ -0,0 +1,21 @@
<script setup>
import DepartmentDescriptor from './DepartmentDescriptor.vue';
import DepartmentSummaryDialog from './DepartmentSummaryDialog.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
</script>
<template>
<QPopupProxy>
<DepartmentDescriptor
v-if="$props.id"
:id="$props.id"
:summary="DepartmentSummaryDialog"
/>
</QPopupProxy>
</template>

View File

@ -0,0 +1,29 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import DepartmentSummary from './DepartmentSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
</script>
<template>
<QDialog ref="dialogRef" @hide="onDialogHide">
<DepartmentSummary v-if="$props.id" :id="$props.id" />
</QDialog>
</template>
<style lang="scss">
.q-dialog .summary .header {
position: sticky;
z-index: $z-max;
top: 0;
}
</style>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue'; import { ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';

View File

@ -186,6 +186,7 @@ async function upsert() {
url="Suppliers" url="Suppliers"
:fields="['id', 'nickname']" :fields="['id', 'nickname']"
sort-by="nickname" sort-by="nickname"
:is-clearable="false"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { onMounted, ref, computed } from 'vue'; import { onMounted, ref, computed, watch } from 'vue';
import { QBtn, QField, QPopupEdit } from 'quasar'; import { QBtn } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@ -46,7 +46,12 @@ const arrayData = useArrayData('ExtraCommunity', {
}, },
}); });
const rows = computed(() => arrayData.store.data || []); const rows = ref([]);
const originalRowDataCopy = ref([]);
const draggedRowIndex = ref(null);
const targetRowIndex = ref(null);
const entryRowIndex = ref(null);
const draggedEntry = ref(null);
const tableColumnComponents = { const tableColumnComponents = {
id: { id: {
@ -55,7 +60,12 @@ const tableColumnComponents = {
}, },
cargoSupplierNickname: { cargoSupplierNickname: {
component: QBtn, component: QBtn,
attrs: { flat: true, color: 'primary', dense: true }, attrs: {
flat: true,
color: 'primary',
dense: true,
class: 'supplier-name-button',
},
}, },
agencyModeName: { agencyModeName: {
component: 'span', component: 'span',
@ -66,16 +76,24 @@ const tableColumnComponents = {
attrs: {}, attrs: {},
}, },
ref: { ref: {
component: QField, component: VnInput,
attrs: { readonly: true, dense: true, class: 'cursor-pointer' }, attrs: { dense: true },
event: (val, field, rowIndex) => ({
'keyup.enter': () => saveFieldValue(val, field, rowIndex),
blur: () => saveFieldValue(val, field, rowIndex),
}),
}, },
stickers: { stickers: {
component: 'span', component: 'span',
attrs: {}, attrs: {},
}, },
kg: { kg: {
component: QField, component: VnInput,
attrs: { readonly: true, dense: true, class: 'cursor-pointer' }, attrs: { dense: true, type: 'number', min: 0, class: 'input-number' },
event: (val, field, rowIndex) => ({
'keyup.enter': () => saveFieldValue(val, field, rowIndex),
blur: () => saveFieldValue(val, field, rowIndex),
}),
}, },
loadedKg: { loadedKg: {
component: 'span', component: 'span',
@ -199,7 +217,7 @@ const columns = computed(() => [
align: 'left', align: 'left',
showValue: true, showValue: true,
sortable: true, sortable: true,
format: (value) => toDate(value.substring(0, 10)), format: (value) => toDate(value),
}, },
{ {
label: t('globals.wareHouseIn'), label: t('globals.wareHouseIn'),
@ -216,7 +234,7 @@ const columns = computed(() => [
align: 'left', align: 'left',
showValue: true, showValue: true,
sortable: true, sortable: true,
format: (value) => toDate(value.substring(0, 10)), format: (value) => toDate(value),
}, },
]); ]);
@ -224,6 +242,22 @@ async function getData() {
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
} }
const onStoreDataChange = () => {
const newData = JSON.parse(JSON.stringify(arrayData.store.data)) || [];
rows.value = newData;
// el objetivo de esto es guardar una copia de los valores iniciales de todas las rows para corroborar si la data cambio antes de guardar los cambios
originalRowDataCopy.value = JSON.parse(JSON.stringify(newData));
};
watch(
arrayData.store,
() => {
if (!arrayData.store.data) return;
onStoreDataChange();
},
{ deep: true, immediate: true }
);
const openReportPdf = () => { const openReportPdf = () => {
const params = { const params = {
...arrayData.store.userParams, ...arrayData.store.userParams,
@ -235,9 +269,14 @@ const openReportPdf = () => {
const saveFieldValue = async (val, field, index) => { const saveFieldValue = async (val, field, index) => {
try { try {
// Evitar la solicitud de guardado si el valor no ha cambiado
if (originalRowDataCopy.value[index][field] == val) return;
const id = rows.value[index].id; const id = rows.value[index].id;
const params = { [field]: val }; const params = { [field]: val };
await axios.patch(`Travels/${id}`, params); await axios.patch(`Travels/${id}`, params);
// Actualizar la copia de los datos originales con el nuevo valor
originalRowDataCopy.value[index][field] = val;
} catch (err) { } catch (err) {
console.error('Error updating travel'); console.error('Error updating travel');
} }
@ -248,6 +287,7 @@ const navigateToTravelId = (id) => {
}; };
const stopEventPropagation = (event, col) => { const stopEventPropagation = (event, col) => {
// Detener la propagación del evento de los siguientes elementos para evitar el click sobre la row que dispararía la función navigateToTravelId
if (!['ref', 'id', 'cargoSupplierNickname', 'kg'].includes(col.name)) return; if (!['ref', 'id', 'cargoSupplierNickname', 'kg'].includes(col.name)) return;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -263,6 +303,121 @@ onMounted(async () => {
landedTo.value.setHours(23, 59, 59, 59); landedTo.value.setHours(23, 59, 59, 59);
await getData(); await getData();
}); });
// Handler del evento @dragstart (inicio del drag) y guarda información inicial
const handleDragStart = (event, rowIndex, entryIndex) => {
draggedRowIndex.value = rowIndex;
entryRowIndex.value = entryIndex;
event.dataTransfer.effectAllowed = 'move';
};
// Handler del evento @dragenter (cuando haces drag sobre une elemento y lo arrastras sobre un posible target de drop) y actualiza el targetIndex
const handleDragEnter = (_, targetIndex) => {
targetRowIndex.value = targetIndex;
};
const saveRowDrop = async (targetRowIndex) => {
const entryId = draggedEntry.value.id;
const travelId = rows.value[targetRowIndex].id;
await axios.patch(`Entries/${entryId}`, { travelFk: travelId });
};
const moveRow = async (draggedRowIndex, targetRowIndex, entryIndex) => {
try {
if (draggedRowIndex === targetRowIndex) return;
// Remover entry de la row original
draggedEntry.value = rows.value[draggedRowIndex].entries.splice(entryIndex, 1)[0];
//Si la row de destino por alguna razón no tiene la propiedad entry la creamos
if (!rows.value[targetRowIndex].entries) rows.value[targetRowIndex].entries = [];
// Añadir entry a la row de destino
rows.value[targetRowIndex].entries.push(draggedEntry.value);
await saveRowDrop(targetRowIndex);
} catch (err) {
cleanDragAndDropData();
console.error('Error moving row', err);
}
};
// Handler de cuando haces un drop tanto dentro como fuera de la tabla para limpiar acciones y data
const handleDragEnd = () => {
stopScroll();
cleanDragAndDropData();
};
// Handler del evento @drop (cuando soltas el elemento draggeado sobre un target)
const handleDrop = () => {
if (
!draggedRowIndex.value &&
draggedRowIndex.value !== 0 &&
!targetRowIndex.value &&
draggedRowIndex.value !== 0
)
return;
moveRow(draggedRowIndex.value, targetRowIndex.value, entryRowIndex.value);
handleDragEnd();
};
const cleanDragAndDropData = () => {
draggedRowIndex.value = null;
targetRowIndex.value = null;
entryRowIndex.value = null;
draggedEntry.value = null;
};
const scrollInterval = ref(null);
const startScroll = (direction) => {
// Iniciar el scroll en la dirección especificada
if (!scrollInterval.value) {
scrollInterval.value = requestAnimationFrame(() => scroll(direction));
}
};
const stopScroll = () => {
if (scrollInterval.value) {
cancelAnimationFrame(scrollInterval.value);
scrollInterval.value = null;
}
};
const scroll = (direction) => {
// Controlar el desplazamiento en la dirección especificada
const yOffset = direction === 'up' ? -2 : 2;
window.scrollBy(0, yOffset);
const windowHeight = window.innerHeight;
const documentHeight = document.body.offsetHeight;
// Verificar si se alcanzaron los límites de la ventana para detener el desplazamiento
if (
(direction === 'up' && window.scrollY > 0) ||
(direction === 'down' && windowHeight + window.scrollY < documentHeight)
) {
scrollInterval.value = requestAnimationFrame(() => scroll(direction));
} else {
stopScroll();
}
};
// Handler del scroll mientras se hace el drag de una row
const handleDragScroll = (event) => {
// Obtener la posición y dimensiones del cursor
const y = event.clientY;
const windowHeight = window.innerHeight;
// Verificar si el cursor está cerca del borde superior o inferior de la ventana
const nearTop = y < 150;
const nearBottom = y > windowHeight - 100;
if (nearTop) {
startScroll('up');
} else if (nearBottom) {
startScroll('down');
} else {
stopScroll();
}
};
</script> </script>
<template> <template>
@ -302,59 +457,60 @@ onMounted(async () => {
row-key="clientId" row-key="clientId"
:pagination="{ rowsPerPage: 0 }" :pagination="{ rowsPerPage: 0 }"
class="full-width" class="full-width"
table-style="user-select: none;"
@drag="handleDragScroll($event)"
@dragend="handleDragEnd($event)"
:separator="!targetRowIndex && targetRowIndex !== 0 ? 'horizontal' : 'none'"
> >
<template #body="props"> <template #body="props">
<QTr <QTr
:props="props" :props="props"
@click="navigateToTravelId(props.row.id)"
class="cursor-pointer bg-vn-primary-row" class="cursor-pointer bg-vn-primary-row"
@click="navigateToTravelId(props.row.id)"
@dragenter="handleDragEnter($event, props.rowIndex)"
@dragover.prevent
@drop="handleDrop()"
:class="{
'dashed-border --top --left --right':
targetRowIndex === props.rowIndex,
'--bottom':
targetRowIndex === props.rowIndex &&
(!props.row.entries || props.row.entries.length === 0),
}"
> >
<QTd <QTd
v-for="col in props.cols" v-for="col in props.cols"
:key="col.name" :key="col.name"
:props="props" :props="props"
@click="stopEventPropagation($event, col)" @click="stopEventPropagation($event, col)"
auto-width
> >
<component <component
:is="tableColumnComponents[col.name].component" :is="tableColumnComponents[col.name].component"
v-bind="tableColumnComponents[col.name].attrs" v-bind="tableColumnComponents[col.name].attrs"
v-model="rows[props.rowIndex][col.field]"
v-on="
tableColumnComponents[col.name].event
? tableColumnComponents[col.name].event(
rows[props.rowIndex][col.field],
col.field,
props.rowIndex
)
: {}
"
> >
<!-- Editable 'ref' and 'kg' QField slot -->
<template
v-if="col.name === 'ref' || col.name === 'kg'"
#control
>
<div
class="self-center full-width no-outline"
tabindex="0"
>
{{ col.value }}
</div>
<QPopupEdit
:key="col.name"
v-model="col.value"
label-set="Save"
label-cancel="Close"
>
<VnInput
v-model="rows[props.pageIndex][col.field]"
dense
autofocus
@keyup.enter="
saveFieldValue(
rows[props.pageIndex][col.field],
col.field,
props.rowIndex
)
"
/>
</QPopupEdit>
</template>
<template v-if="col.showValue"> <template v-if="col.showValue">
{{ col.value }} <span
:class="[
'text-left',
{
'supplier-name':
col.name === 'cargoSupplierNickname',
},
]"
>{{ col.value }}</span
>
</template> </template>
<!-- Main Row Descriptors --> <!-- Main Row Descriptors -->
<TravelDescriptorProxy <TravelDescriptorProxy
v-if="col.name === 'id'" v-if="col.name === 'id'"
@ -367,13 +523,27 @@ onMounted(async () => {
</component> </component>
</QTd> </QTd>
</QTr> </QTr>
<QTr <QTr
v-for="entry in props.row.entries" v-for="(entry, index) in props.row.entries"
:key="entry.id" :key="index"
:props="props" :props="props"
class="bg-vn-secondary-row" class="bg-vn-secondary-row cursor-pointer"
@dragstart="handleDragStart($event, props.rowIndex, index)"
@dragenter="handleDragEnter($event, props.rowIndex)"
@dragover.prevent
@drop="handleDrop()"
:draggable="true"
:class="{
'dragged-row':
entryRowIndex === index && props.rowIndex === draggedRowIndex,
'dashed-border --left --right': targetRowIndex === props.rowIndex,
'--bottom':
targetRowIndex === props.rowIndex &&
index === props.row.entries.length - 1,
}"
> >
<QTd class="row justify-center"> <QTd>
<QBtn flat color="primary">{{ entry.id }} </QBtn> <QBtn flat color="primary">{{ entry.id }} </QBtn>
<EntryDescriptorProxy :id="entry.id" /> <EntryDescriptorProxy :id="entry.id" />
</QTd> </QTd>
@ -381,33 +551,75 @@ onMounted(async () => {
<QBtn flat color="primary" dense>{{ entry.supplierName }}</QBtn> <QBtn flat color="primary" dense>{{ entry.supplierName }}</QBtn>
<SupplierDescriptorProxy :id="entry.supplierFk" /> <SupplierDescriptorProxy :id="entry.supplierFk" />
</QTd> </QTd>
<QTd />
<QTd>
<span>{{ toCurrency(entry.invoiceAmount) }}</span>
</QTd>
<QTd>
<span>{{ entry.reference }}</span>
</QTd>
<QTd>
<span>{{ entry.stickers }}</span>
</QTd>
<QTd></QTd> <QTd></QTd>
<QTd <QTd>
><span>{{ toCurrency(entry.invoiceAmount) }}</span></QTd <span>{{ entry.loadedkg }}</span>
> </QTd>
<QTd <QTd>
><span>{{ entry.reference }}</span></QTd <span>{{ entry.volumeKg }}</span>
> </QTd>
<QTd <QTd />
><span>{{ entry.stickers }}</span></QTd <QTd />
> <QTd />
<QTd></QTd> <QTd />
<QTd
><span>{{ entry.loadedkg }}</span></QTd
>
<QTd
><span>{{ entry.volumeKg }}</span></QTd
>
<QTd></QTd>
<QTd></QTd>
<QTd></QTd>
<QTd></QTd>
</QTr> </QTr>
</template> </template>
</QTable> </QTable>
</QPage> </QPage>
</template> </template>
<style scoped lang="scss">
:deep(.q-table) {
border-collapse: collapse;
}
.dashed-border {
&.--left {
border-left: 1px dashed #ccc;
}
&.--right {
border-right: 1px dashed #ccc;
}
&.--top {
border-top: 1px dashed #ccc;
}
&.--bottom {
border-bottom: 1px dashed #ccc;
}
}
.dragged-row {
background-color: $primary-light;
}
.supplier-name {
display: flex;
max-width: 150px;
@media (max-width: $breakpoint-md-max) {
max-width: 100px;
}
}
.supplier-name-button {
white-space: normal;
width: max-content;
}
</style>
<i18n> <i18n>
en: en:
searchExtraCommunity: Search for extra community shipping searchExtraCommunity: Search for extra community shipping

View File

@ -65,7 +65,7 @@ const decrement = (paramsObj, key) => {
<span>{{ formatFn(tag.value) }}</span> <span>{{ formatFn(tag.value) }}</span>
</div> </div>
</template> </template>
<template #body="{ params }"> <template #body="{ params, searchFn }">
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInput label="id" v-model="params.id" is-outlined /> <VnInput label="id" v-model="params.id" is-outlined />
@ -116,6 +116,7 @@ const decrement = (paramsObj, key) => {
<VnSelectFilter <VnSelectFilter
:label="t('params.agencyModeFk')" :label="t('params.agencyModeFk')"
v-model="params.agencyModeFk" v-model="params.agencyModeFk"
@update:model-value="searchFn()"
:options="agenciesOptions" :options="agenciesOptions"
option-value="agencyFk" option-value="agencyFk"
option-label="name" option-label="name"
@ -129,8 +130,9 @@ const decrement = (paramsObj, key) => {
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
v-model="params.shippedFrom"
:label="t('params.shippedFrom')" :label="t('params.shippedFrom')"
v-model="params.shippedFrom"
@update:model-value="searchFn()"
is-outlined is-outlined
/> />
</QItemSection> </QItemSection>
@ -138,8 +140,9 @@ const decrement = (paramsObj, key) => {
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
v-model="params.landedTo"
:label="t('params.landedTo')" :label="t('params.landedTo')"
v-model="params.landedTo"
@update:model-value="searchFn()"
is-outlined is-outlined
/> />
</QItemSection> </QItemSection>
@ -149,6 +152,7 @@ const decrement = (paramsObj, key) => {
<VnSelectFilter <VnSelectFilter
:label="t('params.warehouseOutFk')" :label="t('params.warehouseOutFk')"
v-model="params.warehouseOutFk" v-model="params.warehouseOutFk"
@update:model-value="searchFn()"
:options="warehousesOptions" :options="warehousesOptions"
option-value="id" option-value="id"
option-label="name" option-label="name"
@ -164,6 +168,7 @@ const decrement = (paramsObj, key) => {
<VnSelectFilter <VnSelectFilter
:label="t('params.warehouseInFk')" :label="t('params.warehouseInFk')"
v-model="params.warehouseInFk" v-model="params.warehouseInFk"
@update:model-value="searchFn()"
:options="warehousesOptions" :options="warehousesOptions"
option-value="id" option-value="id"
option-label="name" option-label="name"
@ -179,6 +184,7 @@ const decrement = (paramsObj, key) => {
<VnSelectFilter <VnSelectFilter
:label="t('supplier.pageTitles.supplier')" :label="t('supplier.pageTitles.supplier')"
v-model="params.cargoSupplierFk" v-model="params.cargoSupplierFk"
@update:model-value="searchFn()"
:options="suppliersOptions" :options="suppliersOptions"
option-value="id" option-value="id"
option-label="name" option-label="name"
@ -194,6 +200,7 @@ const decrement = (paramsObj, key) => {
<VnSelectFilter <VnSelectFilter
:label="t('params.continent')" :label="t('params.continent')"
v-model="params.continent" v-model="params.continent"
@update:model-value="searchFn()"
:options="continentsOptions" :options="continentsOptions"
option-value="code" option-value="code"
option-label="name" option-label="name"
@ -208,19 +215,6 @@ const decrement = (paramsObj, key) => {
</VnFilterPanel> </VnFilterPanel>
</template> </template>
<style scoped>
.input-number >>> input[type='number'] {
-moz-appearance: textfield;
}
.input-number >>> input::-webkit-outer-spin-button,
.input-number >>> input::-webkit-inner-spin-button {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
</style>
<i18n> <i18n>
en: en:
params: params:

View File

@ -1,6 +1,6 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe('InvoiceInBasicData', () => { describe('InvoiceInBasicData', () => {
const selects = '.q-form .q-select'; const selects = ':nth-child(1) > :nth-child(1) > .q-field';
const appendBtns = 'label button'; const appendBtns = 'label button';
const dialogAppendBtns = '.q-dialog label button'; const dialogAppendBtns = '.q-dialog label button';
const dialogInputs = '.q-dialog input'; const dialogInputs = '.q-dialog input';
@ -12,9 +12,7 @@ describe('InvoiceInBasicData', () => {
}); });
it('should edit the provideer and supplier ref', () => { it('should edit the provideer and supplier ref', () => {
cy.get(selects).eq(0).click(); cy.selectOption(selects, 'Bros');
cy.get(selects).eq(0).type('Bros');
cy.get(selects).eq(0).type('{enter}');
cy.get('[title="Reset"]').click(); cy.get('[title="Reset"]').click();
cy.get(appendBtns).eq(0).click(); cy.get(appendBtns).eq(0).click();

View File

@ -7,6 +7,7 @@ describe('InvoiceInList', () => {
const summaryHeaders = '.summaryBody .header'; const summaryHeaders = '.summaryBody .header';
beforeEach(() => { beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer'); cy.login('developer');
cy.visit(`/#/invoice-in/list`); cy.visit(`/#/invoice-in/list`);
}); });

View File

@ -1,6 +1,6 @@
import { vi, describe, expect, it, beforeAll, beforeEach, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, beforeEach, afterEach } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper'; import { createWrapper, axios } from 'app/test/vitest/helper';
import ClaimLines from 'pages/Claim/Card/ClaimLines.vue'; import ClaimLines from '/src/pages/Claim/Card/ClaimLines.vue';
describe('ClaimLines', () => { describe('ClaimLines', () => {
let vm; let vm;