refs 6105 claimNotes and VnNotes created #83

Merged
jorgep merged 11 commits from 6105-createClaimNotes into dev 2023-08-25 08:43:09 +00:00
11 changed files with 251 additions and 13 deletions

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">
<p>
{{ $props.description }}
</p>
</slot>
</div>
</div>
</template>

View File

@ -0,0 +1,123 @@
<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();
jorgep marked this conversation as resolved
Review

Un component global, que se gastara en ticket, worker, etc no deuria tindre algo tan especific de claim

Un component global, que se gastara en ticket, worker, etc no deuria tindre algo tan especific de claim
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"
jorgep marked this conversation as resolved Outdated
Outdated
Review

Sols seria canviar el nom del ref, pq la url i tot si q ho tens dinamic

Sols seria canviar el nom del ref, pq la url i tot si q ho tens dinamic
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>
jorgep marked this conversation as resolved Outdated
Outdated
Review

El ref este si no es gasta en cap lloc lleval

El ref este si no es gasta en cap lloc lleval
<slot name="picture">
jgallego marked this conversation as resolved Outdated
Outdated
Review

El QDialog dixal al final del html, si no queda confus

El QDialog dixal al final del html, si no queda confus
<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') }}
jorgep marked this conversation as resolved Outdated
Outdated
Review

Podries gastar el component VnPaginate i te ahorraries el fetch de dalt i el v.for de aci.
Apart aixina tens millor logica per als filtros (VnPaginate te mes props que podrien ser utils en un futur):

    dataKey: {
        type: String,
        required: true,
    },
    autoLoad: {
        type: Boolean,
        default: false,
    },
    data: {
        type: Array,
        default: null,
    },
    url: {
        type: String,
        default: '',
    },
    filter: {
        type: Object,
        default: null,
    },
    where: {
        type: Object,
        default: null,
    },
    order: {
        type: String,
        default: '',
    },
    limit: {
        type: Number,
        default: 10,
    },
    userParams: {
        type: Object,
        default: null,
    },
    offset: {
        type: Number,
        default: 500,
    },

Pots gastar el component i possarli v-bind="$attrs" i li pasa les propietats que a tu te hajen pasat. I ja no has ni de fenirles tu

Podries gastar el component VnPaginate i te ahorraries el fetch de dalt i el v.for de aci. Apart aixina tens millor logica per als filtros (VnPaginate te mes props que podrien ser utils en un futur): ``` dataKey: { type: String, required: true, }, autoLoad: { type: Boolean, default: false, }, data: { type: Array, default: null, }, url: { type: String, default: '', }, filter: { type: Object, default: null, }, where: { type: Object, default: null, }, order: { type: String, default: '', }, limit: { type: Number, default: 10, }, userParams: { type: Object, default: null, }, offset: { type: Number, default: 500, }, ``` Pots gastar el component i possarli v-bind="$attrs" i li pasa les propietats que a tu te hajen pasat. I ja no has ni de fenirles tu
</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>
jorgep marked this conversation as resolved
Review

fetch? Seria mas correcto submit, insert, etc

fetch? Seria mas correcto submit, insert, etc
.q-card {
max-width: 80em;
}
</style>
<i18n>
es:
Add note here...: Añadir nota aquí...
jorgep marked this conversation as resolved
Review

Quasar te clases (paregut a bootstrap) que te dixa fer coses de estes i te ahorra CSS.
Per exemple esta la clase column que ja te fa esta part:

	    display: flex;
    flex-direction: column;

igual que la part de:

        align-self: flex-start;
        display: flex;
        justify-content: space-between;

Es pot fer tambe:
https://quasar.dev/layout/grid/column#introduction

Quasar te clases (paregut a bootstrap) que te dixa fer coses de estes i te ahorra CSS. Per exemple esta la clase column que ja te fa esta part: ``` display: flex; flex-direction: column; ``` igual que la part de: ``` align-self: flex-start; display: flex; justify-content: space-between; ``` Es pot fer tambe: https://quasar.dev/layout/grid/column#introduction
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

@ -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);
});
});