Merge pull request 'test' (!92) from test into master
gitea/salix-front/pipeline/head This commit looks good Details

Reviewed-on: #92
This commit is contained in:
Javi Gallego 2023-09-07 07:51:36 +00:00
commit f1af3b80bd
16 changed files with 294 additions and 36 deletions

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "23.32.01", "version": "23.36.01",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {

View File

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

View File

@ -38,7 +38,13 @@ const pinnedModulesRef = ref();
</QTooltip> </QTooltip>
</QBtn> </QBtn>
<RouterLink to="/"> <RouterLink to="/">
<QBtn class="q-ml-xs" color="primary" flat round> <QBtn
class="q-ml-xs"
color="primary"
flat
round
v-if="!quasar.platform.is.mobile"
>
<QAvatar square size="md"> <QAvatar square size="md">
<QImg <QImg
src="~/assets/logo_icon.svg" src="~/assets/logo_icon.svg"
@ -56,7 +62,7 @@ const pinnedModulesRef = ref();
<QBadge label="Beta" align="top" /> <QBadge label="Beta" align="top" />
</QToolbarTitle> </QToolbarTitle>
<QSpace /> <QSpace />
<div id="searchbar"></div> <div id="searchbar" class="searchbar"></div>
<QSpace /> <QSpace />
<div class="q-pl-sm q-gutter-sm row items-center no-wrap"> <div class="q-pl-sm q-gutter-sm row items-center no-wrap">
<div id="actions-prepend"></div> <div id="actions-prepend"></div>
@ -70,13 +76,27 @@ const pinnedModulesRef = ref();
{{ t('Go to Salix') }} {{ t('Go to Salix') }}
</QTooltip> </QTooltip>
</QBtn> </QBtn>
<QBtn id="pinnedModules" icon="apps" flat dense rounded> <QBtn
:class="{ 'q-pa-none': quasar.platform.is.mobile }"
id="pinnedModules"
icon="apps"
flat
dense
rounded
>
<QTooltip bottom> <QTooltip bottom>
{{ t('globals.pinnedModules') }} {{ t('globals.pinnedModules') }}
</QTooltip> </QTooltip>
<PinnedModules ref="pinnedModulesRef" /> <PinnedModules ref="pinnedModulesRef" />
</QBtn> </QBtn>
<QBtn rounded dense flat no-wrap id="user"> <QBtn
:class="{ 'q-pa-none': quasar.platform.is.mobile }"
rounded
dense
flat
no-wrap
id="user"
>
<QAvatar size="lg"> <QAvatar size="lg">
<QImg <QImg
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`" :src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
@ -95,6 +115,11 @@ const pinnedModulesRef = ref();
</QHeader> </QHeader>
</template> </template>
<style lang="scss" scoped>
.searchbar {
width: max-content;
}
</style>
<i18n> <i18n>
en: en:
Go to Salix: Go to Salix Go to Salix: Go to Salix

View File

@ -35,19 +35,13 @@ async function redirect() {
<template> <template>
<QMenu anchor="bottom left" max-width="300px" max-height="400px"> <QMenu anchor="bottom left" max-width="300px" max-height="400px">
<div v-if="pinnedModules.length >= 0" class="row justify-around q-pa-md"> <div v-if="pinnedModules.length >= 0" class="row justify-around q-pa-md">
<QBtn <QBtn flat stack size="lg" icon="more_up" @click="redirect($route.params.id)">
flat <div class="button-text">Salix</div>
stack
size="lg"
icon="more_up"
class="col-5"
@click="redirect($route.params.id)"
>
<div class="text-center button-text">Salix</div>
</QBtn> </QBtn>
<QBtn flat stack size="lg" icon="home" class="col-5" to="/"> <QBtn flat stack size="lg" icon="home" to="/">
<div class="text-center button-text">{{ t('Home') }}</div> <div class="button-text">{{ t('Home') }}</div>
</QBtn> </QBtn>
<div class="row col-12 justify-around q-mt-md"> <div class="row col-12 justify-around q-mt-md">
<QBtn <QBtn
flat flat

View File

@ -0,0 +1,26 @@
<script setup>
import { useSession } from 'src/composables/useSession';
const $props = defineProps({
worker: { type: Number, required: true },
description: { type: String, default: null },
});
const session = useSession();
const token = session.getToken();
</script>
<template>
<div class="avatar-picture column items-center">
<QAvatar color="orange">
<QImg
:src="`/api/Images/user/160x160/${$props.worker}/download?access_token=${token}`"
spinner-color="white"
/>
</QAvatar>
<div class="description">
<slot name="description" v-if="$props.description">
<p>
{{ $props.description }}
</p>
</slot>
</div>
</div>
</template>

View File

@ -0,0 +1,126 @@
<script setup>
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnAvatar from 'src/components/ui/VnAvatar.vue';
import { toDateHour } from 'src/filters';
import { ref } from 'vue';
import axios from 'axios';
import { useI18n } from 'vue-i18n';
import VnPaginate from './VnPaginate.vue';
const $props = defineProps({
id: { type: String, required: true },
url: { type: String, default: null },
filter: { type: Object, default: () => {} },
body: { type: Object, default: () => {} },
addNote: { type: Boolean, default: false },
});
const { t } = useI18n();
const noteModal = ref(false);
const newNote = ref('');
const vnPaginateRef = ref();
async function insert() {
const body = $props.body;
Object.assign(body, { text: newNote.value });
await axios.post($props.url, body);
vnPaginateRef.value.fetch();
}
</script>
<template>
<div class="column items-center">
<VnPaginate
:data-key="$props.url"
:url="$props.url"
order="created DESC"
:limit="20"
:filter="$props.filter"
auto-load
ref="vnPaginateRef"
>
<template #body="{ rows }">
<QCard class="q-pa-md q-mb-md" v-for="(note, index) in rows" :key="index">
<QCardSection horizontal>
<slot name="picture">
<VnAvatar :worker="note.workerFk" />
</slot>
<QItem class="full-width justify-between items-start">
<span class="link">
{{ `${note.worker.firstName} ${note.worker.lastName}` }}
<WorkerDescriptorProxy :id="note.worker.id" />
</span>
<slot name="actions">
{{ toDateHour(note.created) }}
</slot>
</QItem>
</QCardSection>
<QCardSection>
<slot name="text">
{{ note.text }}
</slot>
</QCardSection>
</QCard>
</template>
</VnPaginate>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
v-if="addNote"
color="primary"
icon="add"
size="lg"
round
@click="noteModal = true"
/>
</QPageSticky>
<QDialog v-model="noteModal" persistent>
<QCard>
<QCardSection>
<QItem class="q-px-none">
<span class="text-primary text-h6 full-width">
<QIcon name="draft" class="q-mr-xs" />
{{ t('Add note') }}
</span>
<QBtn icon="close" flat round dense v-close-popup />
</QItem>
</QCardSection>
<QCardSection>
<QInput
autofocus
type="textarea"
:hint="t('Add note here...')"
filled
size="lg"
autogrow
v-model="newNote"
></QInput>
</QCardSection>
<QCardActions class="justify-end q-mr-sm">
<QBtn
flat
:label="t('globals.close')"
color="primary"
v-close-popup
/>
<QBtn
:label="t('globals.save')"
color="primary"
v-close-popup
@click="insert"
/>
</QCardActions>
</QCard>
</QDialog>
</div>
</template>
<style lang="scss" scoped>
.q-card {
max-width: 80em;
}
.q-dialog .q-card {
width: 400px;
}
</style>
<i18n>
es:
Add note here...: Añadir nota aquí...
Add note: Añadir nota
</i18n>

View File

@ -49,6 +49,7 @@ const props = defineProps({
}); });
const emit = defineEmits(['onFetch', 'onPaginate']); const emit = defineEmits(['onFetch', 'onPaginate']);
defineExpose({ fetch });
const isLoading = ref(false); const isLoading = ref(false);
const pagination = ref({ const pagination = ref({
sortBy: props.order, sortBy: props.order,
@ -82,7 +83,6 @@ async function fetch() {
if (!arrayData.hasMoreData.value) { if (!arrayData.hasMoreData.value) {
isLoading.value = false; isLoading.value = false;
} }
emit('onFetch', store.data); emit('onFetch', store.data);
} }

View File

@ -2,6 +2,8 @@
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useQuasar } from 'quasar';
const quasar = useQuasar();
const props = defineProps({ const props = defineProps({
dataKey: { dataKey: {
@ -93,7 +95,7 @@ async function search() {
autofocus autofocus
> >
<template #prepend> <template #prepend>
<QIcon name="search" /> <QIcon name="search" v-if="!quasar.platform.is.mobile" />
</template> </template>
<template #append> <template #append>
<QIcon <QIcon
@ -112,13 +114,9 @@ async function search() {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.q-field {
width: 250px;
}
@media screen and (min-width: $breakpoint-sm-max) { @media screen and (min-width: $breakpoint-sm-max) {
.q-field { .q-field {
width: 400px; width: 450px;
} }
} }

View File

@ -1,6 +1,7 @@
import toLowerCase from './toLowerCase'; import toLowerCase from './toLowerCase';
import toDate from './toDate'; import toDate from './toDate';
import toDateString from './toDateString'; import toDateString from './toDateString';
import toDateHour from './toDateHour';
import toCurrency from './toCurrency'; import toCurrency from './toCurrency';
import toPercentage from './toPercentage'; import toPercentage from './toPercentage';
import toLowerCamel from './toLowerCamel'; import toLowerCamel from './toLowerCamel';
@ -11,6 +12,7 @@ export {
toLowerCamel, toLowerCamel,
toDate, toDate,
toDateString, toDateString,
toDateHour,
toCurrency, toCurrency,
toPercentage, toPercentage,
dashIfEmpty, dashIfEmpty,

12
src/filters/toDateHour.js Normal file
View File

@ -0,0 +1,12 @@
export default function toDateHour(date) {
const dateHour = new Date(date).toLocaleDateString('es-ES', {
timeZone: 'Europe/Madrid',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
return dateHour;
}

View File

@ -266,6 +266,7 @@ export default {
rma: 'RMA', rma: 'RMA',
photos: 'Photos', photos: 'Photos',
log: 'Audit logs', log: 'Audit logs',
notes: 'Notes',
}, },
list: { list: {
customer: 'Customer', customer: 'Customer',

View File

@ -265,6 +265,7 @@ export default {
rma: 'RMA', rma: 'RMA',
photos: 'Fotos', photos: 'Fotos',
log: 'Registros de auditoría', log: 'Registros de auditoría',
notes: 'Notas',
}, },
list: { list: {
customer: 'Cliente', customer: 'Cliente',

View File

@ -3,11 +3,12 @@ import LeftMenu from 'components/LeftMenu.vue';
import { getUrl } from 'composables/getUrl'; import { getUrl } from 'composables/getUrl';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { computed, onMounted } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import ClaimDescriptor from './ClaimDescriptor.vue'; import ClaimDescriptor from './ClaimDescriptor.vue';
import { onMounted } from 'vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -21,11 +22,6 @@ const $props = defineProps({
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
const claimSections = [
{ name: 'Notes', url: '/note/index', icon: 'draft' },
{ name: 'Development', url: '/development', icon: 'vn:traceability' },
{ name: 'Action', url: '/action', icon: 'vn:actions' },
];
let salixUrl; let salixUrl;
onMounted(async () => { onMounted(async () => {
@ -49,17 +45,24 @@ onMounted(async () => {
<QSeparator /> <QSeparator />
<QList> <QList>
<QItem <QItem
v-for="section in claimSections"
:key="section.name"
active-class="text-primary" active-class="text-primary"
:href="salixUrl + section.url"
clickable clickable
v-ripple v-ripple
:href="`${salixUrl}/development`"
> >
<QItemSection avatar> <QItemSection avatar>
<QIcon :name="section.icon" /> <QIcon name="vn:traceability"></QIcon>
</QItemSection> </QItemSection>
<QItemSection> {{ t(section.name) }} </QItemSection> <QItemSection>{{ t('Development') }}</QItemSection>
</QItem>
<QItem
active-class="text-primary"
clickable
v-ripple
:href="`${salixUrl}/action`"
>
<QItemSection avatar><QIcon name="vn:actions"></QIcon></QItemSection>
<QItemSection>{{ t('Action') }}</QItemSection>
</QItem> </QItem>
</QList> </QList>
</QScrollArea> </QScrollArea>

View File

@ -0,0 +1,37 @@
<script setup>
import { useRoute } from 'vue-router';
import { useState } from 'src/composables/useState';
import VnNotes from 'src/components/ui/VnNotes.vue';
const route = useRoute();
const state = useState();
const user = state.getUser();
const id = route.params.id;
const claimFilter = {
where: { claimFk: id },
fields: ['created', 'workerFk', 'text'],
include: {
relation: 'worker',
scope: {
fields: ['id', 'firstName', 'lastName'],
},
},
};
const body = {
claimFk: id,
workerFk: user.value.id,
};
</script>
<template>
<div class="col items-center">
<VnNotes
:add-note="true"
:id="id"
url="claimObservations"
:filter="claimFilter"
:body="body"
/>
</div>
</template>

View File

@ -11,7 +11,14 @@ export default {
redirect: { name: 'ClaimMain' }, redirect: { name: 'ClaimMain' },
menus: { menus: {
main: ['ClaimList', 'ClaimRmaList'], main: ['ClaimList', 'ClaimRmaList'],
card: ['ClaimBasicData', 'ClaimLines', 'ClaimRma', 'ClaimPhotos', 'ClaimLog'], card: [
'ClaimBasicData',
'ClaimLines',
'ClaimRma',
'ClaimPhotos',
'ClaimLog',
'ClaimNotes',
],
}, },
children: [ children: [
{ {
@ -103,6 +110,15 @@ export default {
}, },
component: () => import('src/pages/Claim/Card/ClaimLog.vue'), component: () => import('src/pages/Claim/Card/ClaimLog.vue'),
}, },
{
name: 'ClaimNotes',
path: 'notes',
meta: {
title: 'notes',
icon: 'draft',
},
component: () => import('src/pages/Claim/Card/ClaimNotes.vue'),
},
], ],
}, },
], ],

View File

@ -0,0 +1,17 @@
/// <reference types="cypress" />
describe('ClaimNotes', () => {
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/claim/${2}/notes`);
});
it('should add a new note', () => {
const message = 'This is a new message.';
cy.get('.q-page-sticky button').click();
cy.get('.q-dialog .q-card__section:nth-child(2)').type(message);
cy.get('.q-card__actions button:nth-child(2)').click();
cy.get('.q-card .q-card__section:nth-child(2)')
.eq(0)
.should('have.text', message);
});
});