Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 4722-routes_refactor
gitea/salix-front/pipeline/head This commit looks good Details

This commit is contained in:
Joan Sanchez 2022-12-13 13:38:47 +01:00
commit 5a397a5302
23 changed files with 1370 additions and 58 deletions

View File

@ -0,0 +1,54 @@
<script setup>
defineProps({
maxLength: {
type: Number,
required: true,
},
item: {
type: Object,
required: true,
},
});
</script>
<template>
<div class="fetchedTags">
<div class="wrap">
<div class="inline-tag" :class="{ empty: !$props.item.value5 }">{{ $props.item.value5 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value6 }">{{ $props.item.value6 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value7 }">{{ $props.item.value7 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value8 }">{{ $props.item.value8 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value9 }">{{ $props.item.value9 }}</div>
<div class="inline-tag" :class="{ empty: !$props.item.value10 }">{{ $props.item.value10 }}</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.fetchedTags {
align-items: center;
.wrap {
width: 100%;
flex-wrap: wrap;
display: flex;
}
.inline-tag {
height: 1rem;
margin: 0.05rem;
color: $secondary;
text-align: center;
font-size: smaller;
padding: 1px;
flex: 1;
border: 1px solid $color-spacer;
text-overflow: ellipsis;
overflow: hidden;
min-width: 4rem;
max-width: 4rem;
}
.empty {
border: 1px solid $color-spacer-light;
}
}
</style>

View File

@ -22,3 +22,9 @@ $positive: #21ba45;
$negative: #c10015; $negative: #c10015;
$info: #31ccec; $info: #31ccec;
$warning: #f2c037; $warning: #f2c037;
$color-spacer-light: rgba(255, 255, 255, .12);
$color-spacer:rgba(255, 255, 255, .3);
$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

@ -3,6 +3,7 @@ import toDate from './toDate';
import toCurrency from './toCurrency'; import toCurrency from './toCurrency';
import toPercentage from './toPercentage'; import toPercentage from './toPercentage';
import toLowerCamel from './toLowerCamel'; import toLowerCamel from './toLowerCamel';
import dashIfEmpty from './dashIfEmpty';
export { export {
toLowerCase, toLowerCase,
@ -10,4 +11,5 @@ export {
toDate, toDate,
toCurrency, toCurrency,
toPercentage, toPercentage,
dashIfEmpty,
}; };

View File

@ -187,6 +187,49 @@ export default {
selectVideo: 'Select video:', selectVideo: 'Select video:',
notFound: 'No videos available', 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',
goTo: 'Go to'
}
}, },
claim: { claim: {
pageTitles: { pageTitles: {
@ -251,6 +294,50 @@ export default {
returnOfMaterial: 'Return of material authorization (RMA)', returnOfMaterial: 'Return of material authorization (RMA)',
}, },
}, },
invoiceOut: {
pageTitles: {
invoiceOuts: 'InvoiceOuts',
list: 'List',
createInvoiceOut: 'Create invoice out',
summary: 'Summary',
basicData: 'Basic Data'
},
list: {
ref: 'Reference',
issued: 'Issued',
amount: 'Amount',
client: 'Client',
created: 'Created',
company: 'Company',
dued: 'Due date'
},
card: {
issued: 'Issued',
amount: 'Amount',
client: 'Client',
company: 'Company',
customerCard: 'Customer card',
ticketList: 'Ticket List'
},
summary: {
issued: 'Issued',
created: 'Created',
dued: 'Due',
booked: 'Booked',
company: 'Company',
taxBreakdown: 'Tax breakdown',
type: 'Type',
taxableBase: 'Taxable base',
rate: 'Rate',
fee: 'Fee',
tickets: 'Tickets',
ticketId: 'Ticket id',
nickname: 'Alias',
shipped: 'Shipped',
totalWithVat: 'Amount',
}
},
components: { components: {
topbar: {}, topbar: {},
userPanel: { userPanel: {

View File

@ -186,6 +186,49 @@ export default {
selectVideo: 'Seleccionar vídeo:', selectVideo: 'Seleccionar vídeo:',
notFound: 'No hay vídeos disponibles', 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',
goTo: 'Ir a'
}
}, },
claim: { claim: {
pageTitles: { pageTitles: {
@ -250,6 +293,50 @@ export default {
returnOfMaterial: 'Autorización de retorno de materiales (RMA)', returnOfMaterial: 'Autorización de retorno de materiales (RMA)',
}, },
}, },
invoiceOut: {
pageTitles: {
invoiceOuts: 'Fact. emitidas',
list: 'Listado',
createInvoiceOut: 'Crear fact. emitida',
summary: 'Resumen',
basicData: 'Datos básicos'
},
list: {
ref: 'Referencia',
issued: 'Fecha emisión',
amount: 'Importe',
client: 'Cliente',
created: 'Fecha creación',
company: 'Empresa',
dued: 'Fecha vencimineto'
},
card: {
issued: 'Fecha emisión',
amount: 'Importe',
client: 'Cliente',
company: 'Empresa',
customerCard: 'Ficha del cliente',
ticketList: 'Listado de tickets'
},
summary: {
issued: 'Fecha',
created: 'Fecha creación',
dued: 'Vencimiento',
booked: 'Contabilizada',
company: 'Empresa',
taxBreakdown: 'Desglose impositivo',
type: 'Tipo',
taxableBase: 'Base imp.',
rate: 'Tarifa',
fee: 'Cuota',
tickets: 'Tickets',
ticketId: 'Id ticket',
nickname: 'Alias',
shipped: 'F. envío',
totalWithVat: 'Importe',
}
},
components: { components: {
topbar: {}, topbar: {},
userPanel: { userPanel: {

View File

@ -19,3 +19,11 @@ const { dialogRef, onDialogHide } = useDialogPluginComponent();
<claim-summary v-if="$props.id" :id="$props.id" /> <claim-summary v-if="$props.id" :id="$props.id" />
</q-dialog> </q-dialog>
</template> </template>
<style lang="scss">
.q-dialog .summary .header {
position: sticky;
z-index: $z-max;
top: 0;
}
</style>

View File

@ -19,3 +19,11 @@ const { dialogRef, onDialogHide } = useDialogPluginComponent();
<customer-summary v-if="$props.id" :id="$props.id" /> <customer-summary v-if="$props.id" :id="$props.id" />
</q-dialog> </q-dialog>
</template> </template>
<style lang="scss">
.q-dialog .summary .header {
position: sticky;
z-index: $z-max;
top: 0;
}
</style>

View File

@ -0,0 +1,18 @@
<script setup>
import { useState } from 'src/composables/useState';
import InvoiceOutDescriptor from './InvoiceOutDescriptor.vue';
const state = useState();
</script>
<template>
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
<q-scroll-area class="fit">
<InvoiceOutDescriptor />
</q-scroll-area>
</q-drawer>
<q-page-container>
<q-page class="q-pa-md">
<router-view></router-view>
</q-page>
</q-page-container>
</template>

View File

@ -0,0 +1,103 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toCurrency, toDate } from 'src/filters';
import axios from 'axios';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue';
import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
onMounted(async () => {
await fetch();
});
const route = useRoute();
const { t } = useI18n();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const invoiceOut = ref();
async function fetch() {
const filter = {
include: [
{
relation: 'company',
scope: {
fields: ['id', 'code'],
},
},
{
relation: 'client',
scope: {
fields: ['id', 'name', 'email'],
},
},
],
};
const options = { params: { filter } };
const { data } = await axios.get(`InvoiceOuts/${entityId.value}`, options);
if (data) invoiceOut.value = data;
}
const filter = computed(() => {
return invoiceOut.value ? JSON.stringify({ refFk: invoiceOut.value.ref }) : null;
});
</script>
<template>
<card-descriptor v-if="invoiceOut" module="InvoiceOut" :data="invoiceOut" :description="invoiceOut.ref">
<template #body>
<q-list>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.card.issued') }}</q-item-label>
<q-item-label>{{ toDate(invoiceOut.issued) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.card.amount') }}</q-item-label>
<q-item-label>{{ toCurrency(invoiceOut.amount) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section v-if="invoiceOut.company">
<q-item-label caption>{{ t('invoiceOut.card.client') }}</q-item-label>
<q-item-label class="link">
{{ invoiceOut.client.name }}
<q-popup-proxy>
<customer-descriptor-popover :id="invoiceOut.client.id" />
</q-popup-proxy>
</q-item-label>
</q-item-section>
<q-item-section v-if="invoiceOut.company">
<q-item-label caption>{{ t('invoiceOut.card.company') }}</q-item-label>
<q-item-label>{{ invoiceOut.company.code }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<q-card-actions>
<q-btn
size="md"
icon="vn:client"
color="primary"
:to="{ name: 'CustomerCard', params: { id: invoiceOut.client.id } }"
>
<q-tooltip>{{ t('invoiceOut.card.customerCard') }}</q-tooltip>
</q-btn>
<q-btn size="md" icon="vn:ticket" color="primary" :to="{ name: 'TicketList', params: { q: filter } }">
<q-tooltip>{{ t('invoiceOut.card.ticketList') }}</q-tooltip>
</q-btn>
</q-card-actions>
</template>
</card-descriptor>
</template>

View File

@ -0,0 +1,15 @@
<script setup>
import InvoiceOutDescriptor from './InvoiceOutDescriptor.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
</script>
<template>
<q-card>
<invoiceOut-descriptor v-if="$props.id" :id="$props.id" />
</q-card>
</template>

View File

@ -0,0 +1,201 @@
<script setup>
import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { toCurrency, toDate } from 'src/filters';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
onMounted(() => fetch());
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const entityId = computed(() => $props.id || route.params.id);
const invoiceOut = ref(null);
const tax = ref(null);
const tikets = ref(null);
function fetch() {
const id = entityId.value;
axios.get(`InvoiceOuts/${id}/summary`).then(({ data }) => {
invoiceOut.value = data.invoiceOut;
tax.value = data.invoiceOut.taxesBreakdown;
console.log('tax', tax);
});
axios.get(`InvoiceOuts/${id}/getTickets`).then(({ data }) => {
tikets.value = data;
});
}
const taxColumns = ref([
{
name: 'item',
label: 'invoiceOut.summary.type',
field: (row) => row.name,
sortable: true,
},
{
name: 'landed',
label: 'invoiceOut.summary.taxableBase',
field: (row) => row.taxableBase,
format: (value) => toCurrency(value),
sortable: true,
},
{
name: 'quantity',
label: 'invoiceOut.summary.rate',
field: (row) => row.rate,
sortable: true,
},
{
name: 'invoiceOuted',
label: 'invoiceOut.summary.fee',
field: (row) => row.vat,
sortable: true,
},
]);
const ticketsColumns = ref([
{
name: 'item',
label: 'invoiceOut.summary.ticketId',
field: (row) => row.id,
sortable: true,
},
{
name: 'quantity',
label: 'invoiceOut.summary.nickname',
field: (row) => row.nickname,
sortable: true,
},
{
name: 'landed',
label: 'invoiceOut.summary.shipped',
field: (row) => row.shipped,
format: (value) => toDate(value),
sortable: true,
},
{
name: 'landed',
label: 'invoiceOut.summary.totalWithVat',
field: (row) => row.totalWithVat,
format: (value) => toCurrency(value),
sortable: true,
},
]);
</script>
<template>
<div class="summary container">
<q-card>
<skeleton-summary v-if="!invoiceOut" />
<template v-if="invoiceOut">
<div class="header bg-primary q-pa-sm q-mb-md">
{{ invoiceOut.ref }} - {{ invoiceOut.client.socialName }}
</div>
<q-list>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.summary.issued') }}</q-item-label>
<q-item-label>{{ toDate(invoiceOut.issued) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.summary.dued') }}</q-item-label>
<q-item-label>{{ toDate(invoiceOut.dued) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.summary.created') }}</q-item-label>
<q-item-label>{{ toDate(invoiceOut.created) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.summary.booked') }}</q-item-label>
<q-item-label>{{ toDate(invoiceOut.booked) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.summary.company') }}</q-item-label>
<q-item-label>{{ invoiceOut.company.code }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<q-card-section class="q-pa-md">
<h6>{{ t('invoiceOut.summary.taxBreakdown') }}</h6>
<q-table :columns="taxColumns" :rows="tax" flat>
<template #header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</q-th>
</q-tr>
</template>
</q-table>
</q-card-section>
<q-card-section class="q-pa-md">
<h6>{{ t('invoiceOut.summary.tickets') }}</h6>
<q-table :columns="ticketsColumns" :rows="tikets" flat>
<template #header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ t(col.label) }}
</q-th>
</q-tr>
</template>
</q-table>
</q-card-section>
</template>
</q-card>
</div>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 100%;
min-width: 950px;
max-width: 950px;
}
.summary {
.header {
text-align: center;
font-size: 18px;
}
#slider-container {
max-width: 80%;
margin: 0 auto;
.q-slider {
.q-slider__marker-labels:nth-child(1) {
transform: none;
}
.q-slider__marker-labels:nth-child(2) {
transform: none;
left: auto !important;
right: 0%;
}
}
}
}
.q-dialog .summary {
max-width: 1200px;
}
</style>

View File

@ -0,0 +1,21 @@
<script setup>
import { useDialogPluginComponent } from 'quasar';
import InvoiceOutSummary from './InvoiceOutSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
defineEmits([...useDialogPluginComponent.emits]);
const { dialogRef, onDialogHide } = useDialogPluginComponent();
</script>
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<invoiceOut-summary v-if="$props.id" :id="$props.id" />
</q-dialog>
</template>

View File

@ -0,0 +1,83 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import Paginate from 'src/components/Paginate.vue';
import InvoiceOutSummaryDialog from './Card/InvoiceOutSummaryDialog.vue';
import { toDate, toCurrency } from 'src/filters/index';
const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
function navigate(id) {
router.push({ path: `/invoiceOut/${id}` });
}
function viewSummary(id) {
quasar.dialog({
component: InvoiceOutSummaryDialog,
componentProps: {
id,
},
});
}
</script>
<template>
<q-page class="q-pa-md">
<paginate url="/InvoiceOuts/filter" sort-by="issued DESC, id DESC" auto-load>
<template #body="{ rows }">
<q-card class="card" v-for="row of rows" :key="row.id">
<q-item class="q-pa-none items-start cursor-pointer q-hoverable" v-ripple clickable>
<q-item-section class="q-pa-md" @click="navigate(row.id)">
<div class="text-h6">{{ row.ref }}</div>
<q-item-label caption>#{{ row.id }}</q-item-label>
<q-list>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.list.issued') }}</q-item-label>
<q-item-label>{{ toDate(row.issued) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.list.amount') }}</q-item-label>
<q-item-label>{{ toCurrency(row.amount) }}</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.list.client') }}</q-item-label>
<q-item-label>{{ row.clientSocialName }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.list.created') }}</q-item-label>
<q-item-label>{{ toDate(row.created) }}</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.list.company') }}</q-item-label>
<q-item-label>{{ row.companyCode }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('invoiceOut.list.dued') }}</q-item-label>
<q-item-label>{{ toDate(row.dued) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-item-section>
<q-separator vertical />
<q-card-actions vertical class="justify-between">
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
</q-btn>
<q-btn flat round color="grey-7" icon="preview" @click="viewSummary(row.id)">
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
</q-btn>
</q-card-actions>
</q-item>
</q-card>
</template>
</paginate>
</q-page>
</template>

View File

@ -0,0 +1,17 @@
<script setup>
import { useState } from 'src/composables/useState';
import LeftMenu from 'src/components/LeftMenu.vue';
const state = useState();
</script>
<template>
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
<q-scroll-area class="fit text-grey-8">
<LeftMenu />
</q-scroll-area>
</q-drawer>
<q-page-container>
<router-view></router-view>
</q-page-container>
</template>

View File

@ -0,0 +1,553 @@
<script setup>
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 } from 'src/filters';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
import FetchData from 'components/FetchData.vue';
import FetchedTags from 'components/ui/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;
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
side
top
color="orange-11"
text-color="black"
: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 class="link">{{ 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 v-if="ticket.refFk" class="link">{{ 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-list>
<q-item-label header class="text-h6">
{{ t('ticket.summary.saleLines') }}
<router-link
:to="{ name: 'TicketBasicData', params: { id: entityId } }"
target="_blank"
>
<q-icon name="open_in_new" />
</router-link>
</q-item-label>
<q-table :rows="ticket.sales" flat>
<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">{{ props.row.itemFk }}</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>
<div class="fetched-tags">
<span>{{ props.row.item.name }}</span>
<span v-if="props.row.item.subName" class="subName">{{
props.row.item.subName
}}</span>
</div>
<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>
</q-list>
</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-list>
<q-item-label header class="text-h6">
{{ t('ticket.summary.packages') }}
<q-icon name="open_in_new" />
</q-item-label>
<q-table :rows="ticket.packagings" flat>
<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>
</q-list>
</div>
<div class="col" v-if="ticket.services.length > 0">
<q-list>
<q-item-label header class="text-h6">
{{ t('ticket.summary.services') }}
<q-icon name="open_in_new" />
</q-item-label>
<q-table :rows="ticket.services" flat>
<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>
<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>
</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>
</q-list>
</div>
</div>
<div class="row q-pa-md" v-if="ticket.requests.length > 0">
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('ticket.summary.request') }}
<q-icon name="open_in_new" />
</q-item-label>
<q-table :rows="ticket.requests" flat>
<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>
<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">{{ props.row.sale.itemFk }}</q-td>
<q-td><q-checkbox v-model="props.row.isOk" :disable="true" /></q-td>
</q-tr>
</template>
</q-table>
</q-list>
</div>
</div>
</template>
</q-card>
</div>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 100%;
height: 100%;
max-width: 1200px;
}
.summary {
.q-list {
.q-item__label--header {
display: flex;
justify-content: space-between;
a {
color: $primary;
}
}
}
.fetched-tags {
display: flex;
flex-wrap: wrap;
align-items: center;
& span {
flex-basis: 50%;
}
& span.subName {
flex-basis: 50%;
color: $secondary;
text-transform: uppercase;
font-size: 0.75rem;
}
}
.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%;
}
}
.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;
}
}
}
.q-dialog .summary {
max-width: 1200px;
}
</style>

View File

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

View File

@ -1,12 +1,13 @@
<script setup> <script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import Paginate from 'components/Paginate.vue'; import Paginate from 'components/Paginate.vue';
import { toDate, toCurrency } from 'src/filters/index'; import { toDate, toCurrency } from 'src/filters/index';
// import TicketSummary from './Card/TicketSummary.vue'; import TicketSummaryDialog from './Card/TicketSummaryDialog.vue';
const router = useRouter(); const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const filter = { const filter = {
@ -48,15 +49,13 @@ function navigate(id) {
router.push({ path: `/ticket/${id}` }); router.push({ path: `/ticket/${id}` });
} }
const preview = ref({ function viewSummary(id) {
shown: false, quasar.dialog({
}); component: TicketSummaryDialog,
componentProps: {
function showPreview(id) { id,
preview.value.shown = true; },
preview.value.data = { });
customerId: id,
};
} }
</script> </script>
@ -108,42 +107,16 @@ function showPreview(id) {
</q-item-section> </q-item-section>
<q-separator vertical /> <q-separator vertical />
<q-card-actions vertical class="justify-between"> <q-card-actions vertical class="justify-between">
<!-- <q-btn color="grey-7" round flat icon="more_vert">
<q-tooltip>{{ t('customer.list.moreOptions') }}</q-tooltip>
<q-menu cover auto-close>
<q-list>
<q-item clickable>
<q-item-section avatar>
<q-icon name="add" />
</q-item-section>
<q-item-section>Add a note</q-item-section>
</q-item>
<q-item clickable>
<q-item-section avatar>
<q-icon name="history" />
</q-item-section>
<q-item-section>Display customer history</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn> -->
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)"> <q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip> <q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
</q-btn> </q-btn>
<q-btn flat round color="grey-7" icon="preview" @click="showPreview(row.id)"> <q-btn flat round color="grey-7" icon="preview" @click="viewSummary(row.id)">
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip> <q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
</q-btn> </q-btn>
<!-- <q-btn flat round color="grey-7" icon="vn:ticket">
<q-tooltip>{{ t('customer.list.customerOrders') }}</q-tooltip>
</q-btn> -->
</q-card-actions> </q-card-actions>
</q-item> </q-item>
</q-card> </q-card>
</template> </template>
</paginate> </paginate>
</q-page> </q-page>
<!-- <q-dialog v-model="preview.shown">
<customer-summary :customer-id="preview.data.customerId" />
</q-dialog> -->
</template> </template>

View File

@ -5,6 +5,7 @@ import { i18n } from 'src/boot/i18n';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { useRole } from 'src/composables/useRole'; import { useRole } from 'src/composables/useRole';
import { toLowerCamel } from 'src/filters';
const state = useState(); const state = useState();
const session = useSession(); const session = useSession();
@ -24,8 +25,8 @@ export default route(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER const createHistory = process.env.SERVER
? createMemoryHistory ? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history' : process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory ? createWebHistory
: createWebHashHistory; : createWebHashHistory;
const Router = createRouter({ const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({ left: 0, top: 0 }),
@ -45,26 +46,14 @@ export default route(function (/* { store, ssrContext } */) {
} }
if (isLoggedIn()) { if (isLoggedIn()) {
// try {
const stateRoles = state.getRoles().value; const stateRoles = state.getRoles().value;
if (stateRoles.length === 0) { if (stateRoles.length === 0) {
await role.fetch(); await role.fetch();
} }
// } catch (error) {
// Notify.create({
// message: t('errors.statusUnauthorized'),
// type: 'negative',
// });
// session.destroy();
// return next({ path: '/login' });
// }
const matches = to.matched; const matches = to.matched;
const hasRequiredRoles = matches.every(route => { const hasRequiredRoles = matches.every((route) => {
const meta = route.meta; const meta = route.meta;
if (meta && meta.roles) if (meta && meta.roles) return role.hasAny(meta.roles);
return role.hasAny(meta.roles)
return true; return true;
}); });
@ -84,7 +73,7 @@ export default route(function (/* { store, ssrContext } */) {
if (matches && matches.length > 1) { if (matches && matches.length > 1) {
const module = matches[1]; const module = matches[1];
const moduleTitle = module.meta && module.meta.title; const moduleTitle = module.meta && module.meta.title;
moduleName = module.name.toLowerCase(); moduleName = toLowerCamel(module.name);
if (moduleTitle) { if (moduleTitle) {
title = t(`${moduleName}.pageTitles.${moduleTitle}`); title = t(`${moduleName}.pageTitles.${moduleTitle}`);
} }

View File

@ -1,9 +1,11 @@
import Customer from './customer'; import Customer from './customer';
import Ticket from './ticket'; import Ticket from './ticket';
import Claim from './claim'; import Claim from './claim';
import InvoiceOut from './invoiceOut';
export default [ export default [
Customer, Customer,
Ticket, Ticket,
Claim Claim,
InvoiceOut
] ]

View File

@ -0,0 +1,50 @@
import { RouterView } from 'vue-router';
export default {
path: '/invoiceOut',
name: 'InvoiceOut',
meta: {
title: 'invoiceOuts',
icon: 'vn:invoice-out'
},
component: RouterView,
redirect: { name: 'InvoiceOutMain' },
menus: {
main: ['InvoiceOutList']
},
children: [
{
path: '',
name: 'InvoiceOutMain',
component: () => import('src/pages/InvoiceOut/InvoiceOutMain.vue'),
redirect: { name: 'InvoiceOutList' },
children: [
{
path: 'list',
name: 'InvoiceOutList',
meta: {
title: 'list',
icon: 'view_list',
},
component: () => import('src/pages/InvoiceOut/InvoiceOutList.vue'),
}
]
},
{
name: 'InvoiceOutCard',
path: ':id',
component: () => import('src/pages/InvoiceOut/Card/InvoiceOutCard.vue'),
redirect: { name: 'InvoiceOutSummary' },
children: [
{
name: 'InvoiceOutSummary',
path: 'summary',
meta: {
title: 'summary'
},
component: () => import('src/pages/InvoiceOut/Card/InvoiceOutSummary.vue'),
}
]
},
]
};

View File

@ -1,6 +1,7 @@
import customer from './modules/customer'; import customer from './modules/customer';
import ticket from './modules/ticket'; import ticket from './modules/ticket';
import claim from './modules/claim'; import claim from './modules/claim';
import invoiceOut from './modules/invoiceOut';
const routes = [ const routes = [
{ {
@ -25,6 +26,7 @@ const routes = [
customer, customer,
ticket, ticket,
claim, claim,
invoiceOut,
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'NotFound', name: 'NotFound',
@ -34,4 +36,4 @@ const routes = [
} }
]; ];
export default routes; export default routes;

View File

@ -6,7 +6,7 @@ import { useRole } from 'src/composables/useRole';
import routes from 'src/router/modules'; import routes from 'src/router/modules';
export const useNavigationStore = defineStore('navigationStore', () => { export const useNavigationStore = defineStore('navigationStore', () => {
const modules = ['customer', 'claim', 'ticket']; const modules = ['customer', 'claim', 'ticket', 'invoiceOut'];
const pinnedModules = ref([]); const pinnedModules = ref([]);
const role = useRole(); const role = useRole();