0
0
Fork 0

feat(claimLog): added claim log section

Refs: #5340
This commit is contained in:
Joan Sanchez 2023-03-06 14:29:21 +01:00
parent 818356465b
commit 34e5814b06
16 changed files with 816 additions and 581 deletions

View File

@ -48,24 +48,13 @@ onMounted(() => stateStore.setMounted());
<div id="searchbar"></div>
<q-space></q-space>
<div class="q-pl-sm q-gutter-sm row items-center no-wrap">
<div id="header-actions"></div>
<div id="actions-prepend"></div>
<q-btn id="pinnedModules" icon="apps" flat dense rounded>
<q-tooltip bottom>
{{ t('globals.pinnedModules') }}
</q-tooltip>
<PinnedModules />
</q-btn>
<q-btn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
<q-btn rounded dense flat no-wrap id="user">
<q-avatar size="lg">
<q-img
@ -79,6 +68,7 @@ onMounted(() => stateStore.setMounted());
</q-tooltip>
<UserPanel />
</q-btn>
<div id="actions-append"></div>
</div>
</q-toolbar>
</q-header>

View File

@ -86,6 +86,7 @@ async function paginate() {
if (!props.url) return;
isLoading.value = true;
await arrayData.loadMore();
if (!arrayData.hasMoreData.value) {
@ -121,7 +122,7 @@ async function onLoad(...params) {
</script>
<template>
<div class="column items-center">
<div>
<div
v-if="store.data && store.data.length === 0 && !isLoading"
class="info-row q-pa-md text-center"
@ -150,27 +151,18 @@ async function onLoad(...params) {
</q-card>
</div>
</div>
<q-infinite-scroll
v-if="store.data"
@load="onLoad"
:offset="offset"
class="column items-center"
>
<div v-if="store" class="card-list q-gutter-y-md">
<q-infinite-scroll v-if="store.data" @load="onLoad" :offset="offset">
<slot name="body" :rows="store.data"></slot>
<div v-if="isLoading" class="info-row q-pa-md text-center">
<q-spinner color="orange" size="md" />
</div>
</div>
</q-infinite-scroll>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
// .q-infinite-scroll {
// width: 100%;
// }
.info-row {
width: 100%;

View File

@ -50,7 +50,7 @@ export function useArrayData(key, userOptions) {
Object.assign(store.filter, filter);
const params = {
filter: JSON.stringify(filter),
filter: JSON.stringify(store.filter),
};
Object.assign(params, store.userParams);

View File

@ -242,6 +242,7 @@ export default {
basicData: 'Basic Data',
rma: 'RMA',
photos: 'Photos',
log: 'Audit logs',
},
list: {
customer: 'Customer',

View File

@ -241,6 +241,7 @@ export default {
basicData: 'Datos básicos',
rma: 'RMA',
photos: 'Fotos',
log: 'Registros de auditoría',
},
list: {
customer: 'Cliente',

View File

@ -2,26 +2,13 @@
import { useQuasar } from 'quasar';
import Navbar from 'src/components/NavBar.vue';
import { useStateStore } from 'stores/useStateStore';
const quasar = useQuasar();
const stateStore = useStateStore();
</script>
<template>
<q-layout view="hHh LpR fFf">
<Navbar />
<router-view></router-view>
<q-drawer
v-model="stateStore.rightDrawer"
side="right"
:width="256"
:persistent="false"
>
<q-scroll-area class="fit text-grey-8">
<div id="rightPanel"></div>
</q-scroll-area>
</q-drawer>
<q-footer v-if="quasar.platform.is.mobile"></q-footer>
</q-layout>
</template>

View File

@ -81,22 +81,47 @@ const statesFilter = {
/>
<fetch-data url="ClaimStates" @on-fetch="setClaimStates" auto-load />
<div class="container">
<div class="column items-center">
<q-card>
<form-model :url="`Claims/${route.params.id}`" :filter="claimFilter" model="claim">
<form-model
:url="`Claims/${route.params.id}`"
:filter="claimFilter"
model="claim"
>
<template #form="{ data, validate, filter }">
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-input v-model="data.client.name" :label="t('claim.basicData.customer')" disable />
<q-input
v-model="data.client.name"
:label="t('claim.basicData.customer')"
disable
/>
</div>
<div class="col">
<q-input v-model="data.created" mask="####-##-##" fill-mask="_" autofocus>
<q-input
v-model="data.created"
mask="####-##-##"
fill-mask="_"
autofocus
>
<template #append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="data.created" mask="YYYY-MM-DD">
<q-popup-proxy
cover
transition-show="scale"
transition-hide="scale"
>
<q-date
v-model="data.created"
mask="YYYY-MM-DD"
>
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
<q-btn
v-close-popup
label="Close"
color="primary"
flat
/>
</div>
</q-date>
</q-popup-proxy>
@ -116,7 +141,9 @@ const statesFilter = {
:label="t('claim.basicData.assignedTo')"
map-options
use-input
@filter="(value, update) => filter(value, update, workerFilter)"
@filter="
(value, update) => filter(value, update, workerFilter)
"
:rules="validate('claim.claimStateFk')"
:input-debounce="0"
>
@ -141,7 +168,9 @@ const statesFilter = {
:label="t('claim.basicData.state')"
map-options
use-input
@filter="(value, update) => filter(value, update, statesFilter)"
@filter="
(value, update) => filter(value, update, statesFilter)
"
:rules="validate('claim.claimStateFk')"
:input-debounce="0"
>
@ -166,7 +195,10 @@ const statesFilter = {
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-checkbox v-model="data.hasToPickUp" :label="t('claim.basicData.picked')" />
<q-checkbox
v-model="data.hasToPickUp"
:label="t('claim.basicData.picked')"
/>
</div>
</div>
</template>
@ -176,12 +208,8 @@ const statesFilter = {
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 800px;
width: 100%;
max-width: 60em;
}
</style>

View File

@ -1,120 +1,190 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router';
import { useSession } from 'src/composables/useSession';
import FetchData from 'components/FetchData.vue';
import { useStateStore } from 'stores/useStateStore';
import TeleportSlot from 'components/ui/TeleportSlot.vue';
import Paginate from 'src/components/PaginateData.vue';
import ClaimLogFilter from './ClaimLogFilter.vue';
import { toDate } from 'src/filters';
// const quasar = useQuasar();
const stateStore = useStateStore();
const route = useRoute();
const session = useSession();
const token = session.getToken();
const { t } = useI18n();
const filter = {
include: [
const columns = [
{
relation: 'user',
scope: {
fields: ['name'],
include: { relation: 'worker', scope: { fields: ['id'] } },
name: 'property',
label: 'Property',
field: (row) => t(`properties.${row.property}`),
align: 'left',
},
{
name: 'before',
label: 'Before',
field: (row) => formatValue(row.before),
},
],
where: {
originFk: route.params.id,
{
name: 'after',
label: 'After',
field: (row) => formatValue(row.after),
},
};
];
const logs = ref();
function onFetch(data) {
//rows.value = data;
logs.value = [];
for (const row of data) {
const changes = [];
const oldInstance = row.oldInstance;
const newInstance = row.newInstance;
for (const property in oldInstance) {
let oldValue = oldInstance[property];
let newValue = newInstance[property];
// if (isNaN(oldValue) && !isNaN(Date.parse(oldValue))) {
// oldValue = toDate(oldValue);
// }
// if (isNaN(newValue) && !isNaN(Date.parse(newValue))) {
// newValue = toDate(newValue);
// }
const change = {
property: property,
value: `${oldValue} -> ${newValue}`,
};
changes.push(change);
function formatValue(value) {
if (typeof value === 'boolean') {
return value ? t('Yes') : t('No');
}
logs.value.push({
model: row.changedModel,
created: row.creationDate,
userFk: row.userFk,
changes: changes,
});
if (isNaN(value) && !isNaN(Date.parse(value))) {
return toDate(value);
}
if (value === undefined) {
return t('Nothing');
}
return `"${value}"`;
}
function actionColor(action) {
if (action === 'insert') return 'positive';
if (action === 'update') return 'positive';
if (action === 'delete') return 'negative';
}
</script>
<template>
<fetch-data url="ClaimLogs" :filter="filter" @on-fetch="onFetch" auto-load />
<div class="q-px-lg">
<q-timeline>
<q-timeline-entry heading> Logs </q-timeline-entry>
<template v-for="log of logs" :key="log.id">
<div class="column items-center">
<q-timeline class="q-pa-md">
<q-timeline-entry heading tag="h4"> {{ t('Audit logs') }} </q-timeline-entry>
<Paginate
data-key="ClaimLogs"
:url="`Claims/${route.params.id}/logs`"
order="id DESC"
:offset="100"
:limit="5"
auto-load
>
<template #body="{ rows }">
<template v-for="log of rows" :key="log.id">
<q-timeline-entry
:title="t(`models.${log.model}`)"
:subtitle="toDate(log.created)"
:avatar="`/api/Images/user/160x160/${log.userFk}/download?access_token=${token}`"
>
<q-list
v-for="change of log.changes"
:key="change.property"
<template #subtitle>
{{ log.userName }} -
{{
toDate(log.created, {
dateStyle: 'medium',
timeStyle: 'short',
})
}}
</template>
<template #title>
<q-chip :color="actionColor(log.action)">
{{ t(`actions.${log.action}`) }}
</q-chip>
{{ t(`models.${log.model}`) }}
</template>
<q-table
:rows="log.changes"
:columns="columns"
row-key="property"
hide-pagination
dense
style="width: 500px"
flat
>
<q-item>
<q-item-section>
<q-item-label caption>
{{ t(`properties.${change.property}`) }}
</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label>{{ change.value }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<!-- <div v-for="change of log.changes" :key="change.property">
{{ change.property }}: {{ change.value }}
</div> -->
<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-timeline-entry>
</template>
</template>
</Paginate>
</q-timeline>
</div>
<TeleportSlot to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</TeleportSlot>
<q-drawer v-model="stateStore.rightDrawer" show-if-above side="right" :width="300">
<q-scroll-area class="fit text-grey-8">
<ClaimLogFilter data-key="ClaimLogs" />
</q-scroll-area>
</q-drawer>
</template>
<style lang="scss" scoped>
.q-timeline {
width: 100%;
max-width: 80em;
}
</style>
<i18n>
en:
actions:
insert: Creates
update: Updates
delete: Deletes
models:
Claim: Claim
ClaimDms: Document
ClaimBeginning: Claimed Sales
ClaimObservation: Observation
properties:
id: ID
claimFk: Claim ID
saleFk: Sale ID
quantity: Quantity
observation: Observation
ticketCreated: Created
created: Created
isChargedToMana: Charged to mana
hasToPickUp: Has to pick Up
dmsFk: Document ID
text: Description
es:
Audit logs: Registros de auditoría
Property: Propiedad
Before: Antes
After: Después
Yes: Si
Nothing: Nada
actions:
insert: Crea
update: Actualiza
delete: Elimina
models:
Claim: Reclamación
ClaimDms: Documento
ClaimBeginning: Línea reclamada
ClaimObservation: Observación
properties:
id: ID
claimFk: ID reclamación
saleFk: ID linea de venta
quantity: Cantidad
observation: Observación
ticketCreated: Creado
created: Creado
isChargedToMana: Cargado a maná
hasToPickUp: Se debe recoger
dmsFk: ID documento
text: Descripción
</i18n>

View File

@ -0,0 +1,79 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const workers = ref();
</script>
<template>
<fetch-data
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<q-date
v-model="params.created"
@update:model-value="searchFn()"
dense
flat
minimal
>
</q-date>
<q-list dense>
<q-separator />
<q-item>
<q-item-section v-if="!workers">
<q-skeleton type="QInput" class="full-width" />
</q-item-section>
<q-item-section v-if="workers">
<q-select
:label="t('User')"
v-model="params.userFk"
@update:model-value="searchFn()"
:options="workers"
option-value="id"
option-label="name"
emit-value
map-options
use-input
:input-debounce="0"
/>
</q-item-section>
</q-item>
</q-list>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
search: Contains
userFk: User
created: Created
es:
params:
search: Contiene
userFk: Usuario
created: Creada
User: Usuario
</i18n>

View File

@ -239,7 +239,7 @@ function onDrag() {
</div>
</div>
<teleport-slot v-if="!quasar.platform.is.mobile" to="#header-actions">
<teleport-slot v-if="!quasar.platform.is.mobile" to="#actions-prepend">
<div class="row q-gutter-x-sm">
<label for="fileInput">
<q-btn

View File

@ -84,6 +84,8 @@ async function remove(id) {
@on-fetch="onFetch"
auto-load
/>
<div class="column items-center">
<div class="list">
<paginate data-key="ClaimRma" url="ClaimRmas">
<template #body="{ rows }">
<q-card class="card">
@ -134,8 +136,9 @@ async function remove(id) {
</q-card>
</template>
</paginate>
<teleport-slot v-if="!quasar.platform.is.mobile" to="#header-actions">
</div>
</div>
<teleport-slot v-if="!quasar.platform.is.mobile" to="#actions-prepend">
<div class="row q-gutter-x-sm">
<q-btn @click="addRow()" icon="add" color="primary" dense rounded>
<q-tooltip bottom> {{ t('globals.add') }} </q-tooltip>
@ -152,6 +155,10 @@ async function remove(id) {
</template>
<style lang="scss" scoped>
.list {
width: 100%;
max-width: 60em;
}
.q-toolbar {
background-color: $grey-9;
}

View File

@ -1,5 +1,4 @@
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
@ -17,9 +16,6 @@ const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false));
function stateColor(code) {
if (code === 'pending') return 'green';
if (code === 'managed') return 'orange';
@ -48,13 +44,25 @@ function viewSummary(id) {
:info="t('You can search by claim id or customer name')"
/>
</teleport-slot>
<teleport-slot to="#rightPanel">
<ClaimFilter data-key="ClaimList" />
<teleport-slot to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</teleport-slot>
<q-page class="q-pa-md">
<q-drawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<q-scroll-area class="fit text-grey-8">
<ClaimFilter data-key="ClaimList" />
</q-scroll-area>
</q-drawer>
<q-page class="column items-center q-pa-md">
<div class="card-list">
<paginate data-key="ClaimList" url="Claims/filter" order="id DESC" auto-load>
<template #body="{ rows }">
<q-card class="card" v-for="row of rows" :key="row.id">
<q-card class="card q-mb-md" v-for="row of rows" :key="row.id">
<q-item
class="q-pa-none items-start cursor-pointer q-hoverable"
v-ripple
@ -71,13 +79,17 @@ function viewSummary(id) {
<q-item-label caption>
{{ t('claim.list.customer') }}
</q-item-label>
<q-item-label>{{ row.clientName }}</q-item-label>
<q-item-label>
{{ row.clientName }}
</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>
{{ t('claim.list.assignedTo') }}
</q-item-label>
<q-item-label>{{ row.workerName }}</q-item-label>
<q-item-label>
{{ row.workerName }}
</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
@ -163,9 +175,17 @@ function viewSummary(id) {
</q-card>
</template>
</paginate>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
Search claim: Buscar reclamación

View File

@ -58,7 +58,7 @@ const filterOptions = {
@on-fetch="(data) => (businessTypes = data)"
auto-load
/>
<div class="container">
<div class="column items-center">
<q-card>
<form-model :url="`Clients/${route.params.id}`" model="customer">
<template #form="{ data, validate, filter }">
@ -172,11 +172,6 @@ const filterOptions = {
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 800px;
}

View File

@ -1,5 +1,4 @@
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
@ -15,9 +14,6 @@ const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false));
function navigate(id) {
router.push({ path: `/customer/${id}` });
}
@ -40,13 +36,30 @@ function viewSummary(id) {
:info="t('You can search by customer id or name')"
/>
</teleport-slot>
<teleport-slot to="#rightPanel">
<CustomerFilter data-key="CustomerList" />
<teleport-slot to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</teleport-slot>
<q-page class="q-pa-md">
<paginate data-key="CustomerList" url="/Clients/filter" order="id DESC" auto-load>
<q-drawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<q-scroll-area class="fit text-grey-8">
<CustomerFilter data-key="CustomerList" />
</q-scroll-area>
</q-drawer>
<q-page class="column items-center q-pa-md">
<div class="card-list">
<paginate
data-key="CustomerList"
url="/Clients/filter"
order="id DESC"
auto-load
>
<template #body="{ rows }">
<q-card class="card" v-for="row of rows" :key="row.id">
<q-card class="card q-mb-md" v-for="row of rows" :key="row.id">
<q-item
class="q-pa-none items-start cursor-pointer q-hoverable"
v-ripple
@ -127,9 +140,17 @@ function viewSummary(id) {
</q-card>
</template>
</paginate>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
Search customer: Buscar cliente

View File

@ -41,10 +41,22 @@ function viewSummary(id) {
:info="t('You can search by invoice reference')"
/>
</teleport-slot>
<teleport-slot to="#rightPanel">
<InvoiceOutFilter data-key="InvoiceOutList" />
<teleport-slot to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</teleport-slot>
<q-page class="q-pa-md">
<q-drawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<q-scroll-area class="fit text-grey-8">
<InvoiceOutFilter data-key="InvoiceOutList" />
</q-scroll-area>
</q-drawer>
<q-page class="column items-center q-pa-md">
<div class="card-list">
<paginate
data-key="InvoiceOutList"
url="InvoiceOuts/filter"
@ -52,7 +64,7 @@ function viewSummary(id) {
auto-load
>
<template #body="{ rows }">
<q-card class="card" v-for="row of rows" :key="row.id">
<q-card class="card q-mb-md" v-for="row of rows" :key="row.id">
<q-item
class="q-pa-none items-start cursor-pointer q-hoverable"
v-ripple
@ -103,7 +115,9 @@ function viewSummary(id) {
<q-item-label caption>
{{ t('invoiceOut.list.company') }}
</q-item-label>
<q-item-label>{{ row.companyCode }}</q-item-label>
<q-item-label>{{
row.companyCode
}}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>
@ -145,9 +159,17 @@ function viewSummary(id) {
</q-card>
</template>
</paginate>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
Search invoice: Buscar factura emitida

View File

@ -78,10 +78,22 @@ function viewSummary(id) {
:info="t('You can search by ticket id or alias')"
/>
</teleport-slot>
<teleport-slot to="#rightPanel">
<TicketFilter data-key="TicketList" />
<teleport-slot to="#actions-append">
<div class="row q-gutter-x-sm">
<q-btn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu">
<q-tooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</q-tooltip>
</q-btn>
</div>
</teleport-slot>
<q-page class="q-pa-md">
<q-drawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<q-scroll-area class="fit text-grey-8">
<TicketFilter data-key="TicketList" />
</q-scroll-area>
</q-drawer>
<q-page class="column items-center q-pa-md">
<div class="card-list">
<paginate
data-key="TicketList"
url="Tickets/filter"
@ -90,7 +102,7 @@ function viewSummary(id) {
auto-load
>
<template #body="{ rows }">
<q-card class="card" v-for="row of rows" :key="row.id">
<q-card class="card q-mb-md" v-for="row of rows" :key="row.id">
<q-item
class="q-pa-none items-start cursor-pointer q-hoverable"
v-ripple
@ -105,7 +117,9 @@ function viewSummary(id) {
<q-item-label caption>
{{ t('ticket.list.nickname') }}
</q-item-label>
<q-item-label>{{ row.nickname }}</q-item-label>
<q-item-label>{{
row.nickname
}}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>
@ -189,9 +203,17 @@ function viewSummary(id) {
</q-card>
</template>
</paginate>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>
<i18n>
es:
Search ticket: Buscar ticket