fixes #4801 Summary del módulo tickets #31

Merged
joan merged 4 commits from 4801-ticket-summary into dev 2022-12-13 11:54:17 +00:00
8 changed files with 622 additions and 0 deletions
Showing only changes of commit 04693529d7 - Show all commits

View File

@ -22,3 +22,8 @@ $positive: #21ba45;
$negative: #c10015;
$info: #31ccec;
$warning: #f2c037;
$color-spacer-light: rgba(255, 255, 255, .12);
$border-thin-light: 1px solid $color-spacer-light;
$spacing-md: 16px;

View File

@ -0,0 +1,4 @@
export default function (value) {
if (value == null || value === '') return '-';
return value;
}

View File

@ -2,10 +2,14 @@ import toLowerCase from './toLowerCase';
import toDate from './toDate';
import toCurrency from './toCurrency';
import toPercentage from './toPercentage';
import dashIfEmpty from './dashIfEmpty';
import zeroFill from './zeroFill';
export {
toLowerCase,
toDate,
toCurrency,
toPercentage,
dashIfEmpty,
zeroFill
};

4
src/filters/zeroFill.js Normal file
View File

@ -0,0 +1,4 @@
export default function (value, pad) {
alexandre marked this conversation as resolved Outdated
Outdated
Review

Esta funcionalidad la depracaría. Es un formato antiguo y puede dar lugar a problemas en el futuro.

Esta funcionalidad la depracaría. Es un formato antiguo y puede dar lugar a problemas en el futuro.
const valueStr = String(value);
return valueStr.padStart(pad, '0');
}

View File

@ -187,6 +187,48 @@ export default {
selectVideo: 'Select video:',
notFound: 'No videos available',
},
summary: {
state: 'State',
salesPerson: 'Sales person',
agency: 'Agency',
zone: 'Zone',
warehouse: 'Warehouse',
route: 'Route',
invoice: 'Invoice',
shipped: 'Shipped',
landed: 'Landed',
packages: 'Packages',
consigneePhone: 'Consignee phone',
consigneeMobile: 'Consignee mobile',
clientPhone: 'Client phone',
clientMobile: 'Client mobile',
consignee: 'Consignee',
subtotal: 'Subtotal',
vat: 'VAT',
total: 'Total',
saleLines: 'Line items',
item: 'Item',
visible: 'Visible',
available: 'Available',
quantity: 'Quantity',
description: 'Description',
price: 'Price',
discount: 'Discount',
amount: 'Amount',
packing: 'Packing',
hasComponentLack: 'Component lack',
itemShortage: 'Not visible',
claim: 'Claim',
reserved: 'Reserved',
created: 'Created',
package: 'Package',
taxClass: 'Tax class',
services: 'Services',
changeState: 'Change state',
requester: 'Requester',
atender: 'Atender',
request: 'Request'
}
},
claim: {
pageTitles: {

View File

@ -186,6 +186,48 @@ export default {
selectVideo: 'Seleccionar vídeo:',
notFound: 'No hay vídeos disponibles',
},
summary: {
state: 'Estado',
salesPerson: 'Comercial',
agency: 'Agencia',
zone: 'Zona',
warehouse: 'Almacén',
route: 'Ruta',
invoice: 'Factura',
shipped: 'Enviado',
landed: 'Entregado',
packages: 'Bultos',
consigneePhone: 'Tel. consignatario',
consigneeMobile: 'Móv. consignatario',
clientPhone: 'Tel. cliente',
clientMobile: 'Móv. cliente',
consignee: 'Consignatario',
subtotal: 'Subtotal',
vat: 'IVA',
total: 'Total',
saleLines: 'Líneas del pedido',
item: 'Artículo',
visible: 'Visible',
available: 'Disponible',
quantity: 'Cantidad',
description: 'Descripción',
price: 'Precio',
discount: 'Descuento',
amount: 'Importe',
packing: 'Encajado',
hasComponentLack: 'Faltan componentes',
itemShortage: 'No visible',
claim: 'Reclamación',
reserved: 'Reservado',
created: 'Fecha creación',
package: 'Embalaje',
taxClass: 'Tipo IVA',
services: 'Servicios',
changeState: 'Cambiar estado',
requester: 'Solicitante',
atender: 'Comprador',
request: 'Petición de compra'
}
},
claim: {
pageTitles: {

View File

@ -0,0 +1,17 @@
<script setup>
defineProps({
maxLength: {
type: Number,
required: true,
},
item: {
type: Object,
required: true,
},
});
</script>
<template>
<div>{{ $props.item.value5 }}</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,504 @@
<script setup>
alexandre marked this conversation as resolved
Review

La vista previa es accesible desde el listado de tickets (TicketList)

La vista previa es accesible desde el listado de tickets (TicketList)
import { onMounted, ref, computed, onUpdated } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { dashIfEmpty, toDate, toCurrency, zeroFill } from 'src/filters';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
import FetchData from 'components/FetchData.vue';
import FetchedTags from './FetchedTags.vue';
onMounted(() => fetch());
onUpdated(() => fetch());
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const entityId = computed(() => $props.id || route.params.id);
const ticket = ref();
const salesLines = ref(null);
const editableStates = ref([]);
async function fetch() {
const { data } = await axios.get(`Tickets/${entityId.value}/summary`);
if (data) {
ticket.value = data;
console.log(ticket.value);
alexandre marked this conversation as resolved
Review

Quitar Console.log()

Quitar Console.log()
salesLines.value = data.sales;
}
}
function stateColor(state) {
if (state.code === 'OK') return 'text-green';
if (state.code === 'FREE') return 'text-blue-3';
if (state.alertLevel === 1) return 'text-primary';
if (state.alertLevel === 0) return 'text-red';
}
function formattedAddress() {
if (!ticket.value) return '';
const address = this.ticket.address;
const postcode = address.postalCode;
const province = address.province ? `(${address.province.name})` : '';
return `${address.street} - ${postcode} - ${address.city} ${province}`;
}
function isEditable() {
try {
return !this.ticket.ticketState.state.alertLevel;
} catch (e) {
console.error(e);
}
return true;
}
async function changeState(value) {
if (!this.ticket.id) return;
const formData = {
ticketFk: this.ticket.id,
code: value,
};
await axios.post(`TicketTrackings/changeState`, formData);
await router.go(route.fullPath);
}
</script>
<template>
<fetch-data url="States/editableStates" @on-fetch="(data) => (editableStates = data)" auto-load />
<div class="summary container">
<q-card>
<skeleton-summary v-if="!ticket" />
<template v-if="ticket">
<div class="header bg-primary q-pa-sm q-mb-md">
<span>
Ticket #{{ ticket.id }} - {{ ticket.client.name }} ({{ ticket.client.id }}) -
{{ ticket.nickname }}
</span>
<q-btn-dropdown
alexandre marked this conversation as resolved
Review

En vez de ponerlo de color negro, quizás probaría con otro color de la gama "orange", de la paleta de colores, quizás un color más claro y apagado que el encabezado:
https://quasar.dev/style/color-palette

En vez de ponerlo de color negro, quizás probaría con otro color de la gama "orange", de la paleta de colores, quizás un color más claro y apagado que el encabezado: https://quasar.dev/style/color-palette
side
top
color="dark"
:label="t('ticket.summary.changeState')"
:disable="!isEditable()"
>
<q-list>
<q-virtual-scroll
style="max-height: 300px"
:items="editableStates"
separator
v-slot="{ item, index }"
>
<q-item :key="index" dense clickable v-close-popup @click="changeState(item.code)">
<q-item-section>
<q-item-label>{{ item.name }}</q-item-label>
</q-item-section>
</q-item>
</q-virtual-scroll>
</q-list>
</q-btn-dropdown>
</div>
<div class="row q-pa-md q-col-gutter-md q-mb-md">
<div class="col">
<q-list>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.state') }}</q-item-label>
<q-item-label :class="stateColor(ticket.ticketState.state)">
{{ ticket.ticketState.state.name }}
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.salesPerson') }}</q-item-label>
<q-item-label>{{ ticket.client.salesPersonUser.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.agency') }}</q-item-label>
<q-item-label>{{ ticket.agencyMode.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.zone') }}</q-item-label>
<q-item-label class="link">{{ ticket.routeFk }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.warehouse') }}</q-item-label>
<q-item-label>{{ ticket.warehouse.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.invoice') }}</q-item-label>
<q-item-label>{{ dashIfEmpty(ticket.refFk) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.shipped') }}</q-item-label>
<q-item-label>{{ toDate(ticket.shipped) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.landed') }}</q-item-label>
<q-item-label>{{ toDate(ticket.landed) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.packages') }}</q-item-label>
<q-item-label>{{ ticket.packages }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.consigneePhone') }}</q-item-label>
<q-item-label>{{ ticket.address.phone }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.consigneeMobile') }}</q-item-label>
<q-item-label>{{ ticket.address.mobile }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.clientPhone') }}</q-item-label>
<q-item-label>{{ ticket.client.phone }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.clientMobile') }}</q-item-label>
<q-item-label>{{ ticket.client.mobile }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.consignee') }}</q-item-label>
<q-item-label>{{ formattedAddress() }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item v-for="note in ticket.notes" :key="note.id">
<q-item-section>
<q-item-label caption>
{{ note.observationType.description }}
</q-item-label>
<q-item-label>
{{ note.description }}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list class="taxes">
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.subtotal') }}</q-item-label>
<q-item-label>{{ toCurrency(ticket.totalWithoutVat) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.vat') }}</q-item-label>
<q-item-label>{{
toCurrency(ticket.totalWithVat - ticket.totalWithoutVat)
}}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.summary.total') }}</q-item-label>
<q-item-label>{{ toCurrency(ticket.totalWithVat) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</div>
<div class="row q-pa-md" v-if="salesLines.length > 0">
<div class="col">
<q-item-label header class="text-h6">
{{ t('ticket.summary.saleLines') }}
</q-item-label>
<q-table :rows="ticket.sales">
<template #header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th auto-width>{{ t('ticket.summary.item') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.visible') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.available') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.quantity') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.description') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.price') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.discount') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.amount') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.packing') }}</q-th>
</q-tr>
</template>
<template #body="props">
<q-tr :props="props">
<q-td>
<q-btn
flat
round
size="xs"
icon="vn:claims"
v-if="props.row.claim"
color="primary"
:to="{ name: 'ClaimCard', params: { id: props.row.claim.claimFk } }"
>
<q-tooltip
>{{ t('ticket.summary.claim') }}:
{{ props.row.claim.claimFk }}</q-tooltip
>
</q-btn>
<q-btn
flat
round
size="xs"
icon="vn:claims"
v-if="props.row.claimBeginning"
color="primary"
:to="{
name: 'ClaimCard',
params: { id: props.row.claimBeginning.claimFk },
}"
>
<q-tooltip
>{{ t('ticket.summary.claim') }}:
{{ props.row.claimBeginning.claimFk }}</q-tooltip
>
</q-btn>
<q-icon name="warning" v-show="props.row.visible < 0" size="xs" color="primary">
<q-tooltip
>{{ t('ticket.summary.visible') }}: {{ props.row.visible }}</q-tooltip
>
</q-icon>
<q-icon name="vn:reserva" v-show="props.row.reserved" size="xs" color="primary">
<q-tooltip>{{ t('ticket.summary.reserved') }}</q-tooltip>
</q-icon>
<q-icon
name="vn:unavailable"
v-show="props.row.itemShortage"
size="xs"
color="primary"
>
<q-tooltip>{{ t('ticket.summary.itemShortage') }}</q-tooltip>
</q-icon>
<q-icon
name="vn:components"
v-show="props.row.hasComponentLack"
size="xs"
color="primary"
>
<q-tooltip>{{ t('ticket.summary.hasComponentLack') }}</q-tooltip>
</q-icon>
</q-td>
<q-td class="link">{{ zeroFill(props.row.itemFk, 6) }}</q-td>
<q-td>{{ props.row.visible }}</q-td>
<q-td>{{ props.row.available }}</q-td>
<q-td>{{ props.row.quantity }}</q-td>
<q-td>
{{ props.row.concept }}
<fetched-tags :item="props.row.item" :max-length="5"></fetched-tags>
</q-td>
<q-td>{{ props.row.price }}</q-td>
<q-td>{{ props.row.discount }} %</q-td>
<q-td
>{{
toCurrency(
props.row.quantity *
props.row.price *
((100 - props.row.discount) / 100)
)
}}
</q-td>
<q-td>{{ dashIfEmpty(props.row.item.itemPackingTypeFk) }}</q-td>
</q-tr>
</template>
</q-table>
</div>
</div>
<div class="row q-pa-md" v-if="ticket.packagings.length > 0 || ticket.services.length > 0">
<div class="col" v-if="ticket.packagings.length > 0">
<q-item-label header class="text-h6">
{{ t('ticket.summary.packages') }}
</q-item-label>
<q-table :rows="ticket.packagings">
<template #header="props">
<q-tr :props="props">
<q-th auto-width>{{ t('ticket.summary.created') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.package') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.quantity') }}</q-th>
</q-tr>
</template>
<template #body="props">
<q-tr :props="props">
<q-td>{{ toDate(props.row.created) }}</q-td>
<q-td>{{ props.row.packaging.item.name }}</q-td>
<q-td>{{ props.row.quantity }}</q-td>
</q-tr>
</template>
</q-table>
</div>
<div class="col" v-if="ticket.services.length > 0">
<q-item-label header class="text-h6">
{{ t('ticket.summary.services') }}
</q-item-label>
<q-table :rows="ticket.services">
<template #header="props">
<q-tr :props="props">
<q-th auto-width>{{ t('ticket.summary.quantity') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.description') }}</q-th>
alexandre marked this conversation as resolved Outdated
Outdated
Review

Seguir la misma estructura de encabezado de apartados, similar al de CustomerSummary, con el icono en el lado derecho.

Seguir la misma estructura de encabezado de apartados, similar al de CustomerSummary, con el icono en el lado derecho.
<q-th auto-width>{{ t('ticket.summary.price') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.taxClass') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.amount') }}</q-th>
</q-tr>
alexandre marked this conversation as resolved Outdated
Outdated
Review

Quitar fondo de la tabla

Quitar fondo de la tabla
</template>
<template #body="props">
<q-tr :props="props">
<q-td>{{ props.row.quantity }}</q-td>
<q-td>{{ props.row.description }}</q-td>
<q-td>{{ toCurrency(props.row.price) }}</q-td>
<q-td>{{ props.row.taxClass.description }}</q-td>
<q-td>{{ toCurrency(props.row.quantity * props.row.price) }}</q-td>
</q-tr>
</template>
</q-table>
</div>
</div>
<div class="row q-pa-md" v-if="ticket.requests.length > 0">
<div class="col">
<q-item-label header class="text-h6">
{{ t('ticket.summary.request') }}
</q-item-label>
<q-table :rows="ticket.requests">
<template #header="props">
<q-tr :props="props">
<q-th auto-width>{{ t('ticket.summary.description') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.created') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.requester') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.atender') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.quantity') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.price') }}</q-th>
<q-th auto-width>{{ t('ticket.summary.item') }}</q-th>
alexandre marked this conversation as resolved Outdated
Outdated
Review

Quitar el fondo de la tabla, ahora mismo es un card dentro de otro card.

Quitar el fondo de la tabla, ahora mismo es un card dentro de otro card.
<q-th auto-width>Ok</q-th>
</q-tr>
</template>
<template #body="props">
<q-tr :props="props">
<q-td>{{ props.row.description }}</q-td>
<q-td>{{ toDate(props.row.created) }}</q-td>
<q-td>{{ props.row.requester.user.name }}</q-td>
<q-td>{{ props.row.atender.user.name }}</q-td>
<q-td>{{ props.row.quantity }}</q-td>
<q-td>{{ toCurrency(props.row.price) }}</q-td>
<q-td v-if="!props.row.sale">-</q-td>
<q-td v-if="props.row.sale" class="link">{{
zeroFill(props.row.sale.itemFk, 6)
}}</q-td>
<q-td><q-checkbox v-model="props.row.isOk" :disable="true" /></q-td>
</q-tr>
</template>
</q-table>
</div>
</div>
</template>
</q-card>
</div>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 100%;
max-width: 1200px;
}
alexandre marked this conversation as resolved Outdated
Outdated
Review

Reducir el tamaño máximo de la vista previa y comprobar que se vea correctamente desde el listado de tickets. Ejemplo ClientSummary, tiene max-width de 1200px

Reducir el tamaño máximo de la vista previa y comprobar que se vea correctamente desde el listado de tickets. Ejemplo ClientSummary, tiene max-width de 1200px
.summary {
.q-table__container {
text-align: left;
.q-icon {
padding: 2%;
}
}
.taxes {
border: $border-thin-light;
text-align: right;
padding: 8px;
}
.row {
flex-wrap: wrap;
.col {
min-width: 250px;
padding-left: 1.5%;
padding-right: 1.5%;
}
}
.text-h6 {
margin-bottom: $spacing-md;
text-transform: uppercase;
line-height: 1;
padding: 7px 0;
padding-bottom: 5px;
border-bottom: 2px solid $primary;
overflow: hidden;
position: relative;
}
.header {
font-size: 18px;
display: flex;
justify-content: space-between;
align-items: center;
align-content: center;
margin: 0;
text-align: center;
span {
flex: 1;
}
.q-btn {
flex: none;
}
}
}
</style>