#5186 create parking section #197

Merged
jorgep merged 29 commits from 5186-CreateParkingSection into dev 2024-03-27 08:42:33 +00:00
24 changed files with 667 additions and 75 deletions

View File

@ -2,7 +2,7 @@
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { t, te } = useI18n();
alexm marked this conversation as resolved
Review

la función "te" devuelve un booleano . Si existe true , si no false.

la función "te" devuelve un booleano . Si existe true , si no false.
Review

_Mientras que es común utilizar abreviaturas para ciertas palabras en nombres de variables, evita abreviaturas excesivas que puedan hacer que el código sea menos legible.

Evitar abreviaturas excesivas: Mientras que es común utilizar abreviaturas para ciertas palabras en nombres de variables, evita abreviaturas excesivas que puedan hacer que el código sea menos legible._

Jo en este cas no ho abreviaria. com a molt a tExist pq t si sabem q significa.
@juan com ho veus?

_Mientras que es común utilizar abreviaturas para ciertas palabras en nombres de variables, evita abreviaturas excesivas que puedan hacer que el código sea menos legible. Evitar abreviaturas excesivas: Mientras que es común utilizar abreviaturas para ciertas palabras en nombres de variables, evita abreviaturas excesivas que puedan hacer que el código sea menos legible._ Jo en este cas no ho abreviaria. com a molt a tExist pq t si sabem q significa. @juan com ho veus?
Review

No entiendo, la abreviatura es de la librería vue-i18n

No entiendo, la abreviatura es de la librería vue-i18n
Review

Es nativo de i18n

Es nativo de i18n
const props = defineProps({
item: {
@ -11,19 +11,30 @@ const props = defineProps({
},
});
const item = computed(() => props.item); // eslint-disable-line vue/no-dupe-keys
const itemComputed = computed(() => {
const item = JSON.parse(JSON.stringify(props.item));
Review

Es necesario hacerlo así para que no sea una referencia al mismo objeto.

Es necesario hacerlo así para que no sea una referencia al mismo objeto.
const [, , section] = item.title.split('.');
if (!te(item.title)) item.title = t(`globals.pageTitles.${section}`);
Review

Si no encuentra la traducción, coge la global.

Si no encuentra la traducción, coge la global.
return item;
});
jorgep marked this conversation as resolved Outdated
Outdated
Review

Si gastes una variable distinta a "item" ja no cal ficar esta linea.

Si gastes una variable distinta a "item" ja no cal ficar esta linea.
</script>
<template>
<QItem active-class="text-primary" :to="{ name: item.name }" clickable v-ripple>
<QItemSection avatar v-if="item.icon">
<QIcon :name="item.icon" />
<QItem
active-class="text-primary"
:to="{ name: itemComputed.name }"
clickable
v-ripple
>
<QItemSection avatar v-if="itemComputed.icon">
<QIcon :name="itemComputed.icon" />
</QItemSection>
<QItemSection avatar v-if="!item.icon">
<QItemSection avatar v-if="!itemComputed.icon">
<QIcon name="disabled_by_default" />
</QItemSection>
<QItemSection>{{ t(item.title) }}</QItemSection>
<QItemSection>{{ t(itemComputed.title) }}</QItemSection>
<QItemSection side>
<slot name="side" :item="item" />
<slot name="side" :item="itemComputed" />
</QItemSection>
</QItem>
</template>

View File

@ -5,16 +5,16 @@ import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useCamelCase } from 'src/composables/useCamelCase';
const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
const { currentRoute } = useRouter();
const { screen } = useQuasar();
const { t, te } = useI18n();
let matched = ref([]);
let breadcrumbs = ref([]);
let root = ref(null);
watchEffect(() => {
matched.value = router.currentRoute.value.matched.filter(
matched.value = currentRoute.value.matched.filter(
(matched) => Object.keys(matched.meta).length
);
breadcrumbs.value.length = 0;
@ -34,13 +34,17 @@ function getBreadcrumb(param) {
icon: param.meta.icon,
path: param.path,
root: root.value,
locale: t(`globals.pageTitles.${param.meta.title}`),
};
if (quasar.screen.gt.sm) {
if (screen.gt.sm) {
breadcrumb.name = param.name;
breadcrumb.title = useCamelCase(param.meta.title);
}
jorgep marked this conversation as resolved Outdated

Si existe la traducción específica , pone esta, si no la global

Si existe la traducción específica , pone esta, si no la global

La clave i18n de las lineas 44 y 45 son iguales no?
Podemos reducir a 1 ocurrencia?

La clave i18n de las lineas 44 y 45 son iguales no? Podemos reducir a 1 ocurrencia?

No, "te" devuelve un booleano, "t" traduce

No, "te" devuelve un booleano, "t" traduce

No, "te" devuelve un booleano, "t" traduce

Tras hablar con Javier, guardo el valor en una variable.

> No, "te" devuelve un booleano, "t" traduce Tras hablar con Javier, guardo el valor en una variable.

Bien, pero quizás para evitarte un ternario, te diría de usar un if con la operacion "te" y dentro le asignas el valor que quieras. Fuera del if resuelves la traducción asignandola a breadcumb.locale

Bien, pero quizás para evitarte un ternario, te diría de usar un if con la operacion "te" y dentro le asignas el valor que quieras. Fuera del if resuelves la traducción asignandola a breadcumb.locale
const moduleLocale = `${breadcrumb.root}.pageTitles.${breadcrumb.title}`;
if (te(moduleLocale)) breadcrumb.locale = t(moduleLocale);
return breadcrumb;
}
</script>
@ -50,7 +54,7 @@ function getBreadcrumb(param) {
v-for="(breadcrumb, index) of breadcrumbs"
:key="index"
:icon="breadcrumb.icon"
:label="t(`${breadcrumb.root}.pageTitles.${breadcrumb.title}`)"
:label="breadcrumb.locale"
:to="breadcrumb.path"
/>
</QBreadcrumbs>

View File

@ -403,7 +403,7 @@ setLogTree();
auto-load
/>
<div
class="column items-center logs origin-log"
class="column items-center logs origin-log q-mt-md"
v-for="(originLog, originLogIndex) in logTree"
Review

No se veía en algunos caso el primer registro.

No se veía en algunos caso el primer registro.
:key="originLogIndex"
>

View File

@ -1,5 +1,5 @@
<script setup>
import { onMounted, useSlots, watch, computed, ref } from 'vue';
import { onBeforeMount, useSlots, watch, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
import { useArrayData } from 'composables/useArrayData';
@ -41,29 +41,28 @@ const state = useState();
const slots = useSlots();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const entity = computed(() => useArrayData($props.dataKey).store.data);
const arrayData = useArrayData($props.dataKey || $props.module, {
url: $props.url,
filter: $props.filter,
skip: 0,
});
const { store } = arrayData;
const entity = computed(() => store.data);
const isLoading = ref(false);
defineExpose({
getData,
});
onMounted(async () => {
onBeforeMount(async () => {
await getData();
watch(
() => $props.url,
async (newUrl, lastUrl) => {
if (newUrl == lastUrl) return;

De momento, para que sea retrocompatible con el resto de secciones a la hora de abrir los descriptors, se hace la llamada siempre , pero si se hace refactor se puede evitar hacer la llamada. Pero ya usa los datos desde arrayData.

De momento, para que sea retrocompatible con el resto de secciones a la hora de abrir los descriptors, se hace la llamada siempre , pero si se hace refactor se puede evitar hacer la llamada. Pero ya usa los datos desde arrayData.
await getData();
}
async () => await getData()
);
});
async function getData() {
const arrayData = useArrayData($props.dataKey, {
url: $props.url,
filter: $props.filter,
skip: 0,
});
store.url = $props.url;
isLoading.value = true;
try {
const { data } = await arrayData.fetch({ append: false, updateRouter: false });

View File

@ -1,11 +1,10 @@
<script setup>
import { onMounted, ref, watch } from 'vue';
import { ref, computed, watch, onBeforeMount } from 'vue';
import { useRoute } from 'vue-router';
import axios from 'axios';
import SkeletonSummary from 'components/ui/SkeletonSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import { useArrayData } from 'src/composables/useArrayData';
const entity = ref();
const props = defineProps({
url: {
type: String,
@ -19,43 +18,48 @@ const props = defineProps({
type: Number,
default: null,
},
dataKey: {
type: String,
default: '',
},
});
const emit = defineEmits(['onFetch']);
const route = useRoute();
const isSummary = ref();
const arrayData = useArrayData(props.dataKey || route.meta.moduleName, {
url: props.url,
filter: props.filter,
skip: 0,
});
const { store } = arrayData;
const entity = computed(() => store.data);
const isLoading = ref(false);
defineExpose({
entity,
fetch,
});
onMounted(() => {
onBeforeMount(async () => {
isSummary.value = String(route.path).endsWith('/summary');
fetch();
await fetch();
watch(props, async () => await fetch());
Review

De momento, para que sea retrocompatible con el resto de secciones a la hora de abrir los summaryPopup, se hace la llamada siempre , pero si se hace refactor se puede evitar hacer la llamada. Pero ya usa los datos desde arrayData.

De momento, para que sea retrocompatible con el resto de secciones a la hora de abrir los summaryPopup, se hace la llamada siempre , pero si se hace refactor se puede evitar hacer la llamada. Pero ya usa los datos desde arrayData.
});
async function fetch() {
const params = {};
if (props.filter) params.filter = JSON.stringify(props.filter);
const { data } = await axios.get(props.url, { params });
entity.value = data;
store.url = props.url;
isLoading.value = true;
const { data } = await arrayData.fetch({ append: false, updateRouter: false });
emit('onFetch', data);
isLoading.value = false;
}
watch(props, async () => {
entity.value = null;
fetch();
});
</script>
<template>
<div class="summary container">
<QCard class="cardSummary">
<SkeletonSummary v-if="!entity" />
<template v-if="entity">
<SkeletonSummary v-if="!entity || isLoading" />
<template v-if="entity && !isLoading">
<div class="summaryHeader bg-primary q-pa-sm text-weight-bolder">
<slot name="header-left">
<router-link

View File

@ -85,6 +85,13 @@ export default {
selectFile: 'Select a file',
jorgep marked this conversation as resolved
Review

Traducciones duplicadas

Traducciones duplicadas
copyClipboard: 'Copy on clipboard',
salesPerson: 'SalesPerson',

Esto se repite mucho. Yo crearía tarea para sustituir en todos los sitios esto.

Esto se repite mucho. Yo crearía tarea para sustituir en todos los sitios esto.
code: 'Code',
jorgep marked this conversation as resolved
Review

Se usa 7 veces. Deberia estar global.

Se usa 7 veces. Deberia estar global.
pageTitles: {
summary: 'Summary',
Review

Se usa 17 veces, deberia estar global o crear tarea

Se usa 17 veces, deberia estar global o crear tarea
Review
https://redmine.verdnatura.es/issues/7132
basicData: 'Basic data',
jorgep marked this conversation as resolved
Review

Se usa 19 veces, debería estar global o crear tarea

Se usa 19 veces, debería estar global o crear tarea
log: 'Logs',
jorgep marked this conversation as resolved
Review

Se usa 2 veces, debería estar global

Se usa 2 veces, debería estar global
parkingList: 'Parkings list',
},
},
errors: {
statusUnauthorized: 'Access denied',
@ -686,6 +693,19 @@ export default {
recyclable: 'Recyclable',
},
},
parking: {
pickingOrder: 'Picking order',
sector: 'Sector',
row: 'Row',
column: 'Column',
pageTitles: {
parking: 'Parking',
},
searchBar: {
info: 'You can search by parking code',
label: 'Search parking...',
},
},
invoiceIn: {
pageTitles: {
invoiceIns: 'Invoices In',

View File

@ -85,6 +85,13 @@ export default {
selectFile: 'Seleccione un fichero',
jorgep marked this conversation as resolved
Review

Las mismas repetidas pero en este caso en español

Las mismas repetidas pero en este caso en español
copyClipboard: 'Copiar en portapapeles',
salesPerson: 'Comercial',
code: 'Código',
pageTitles: {
summary: 'Resumen',
basicData: 'Datos básicos',
log: 'Historial',
parkingList: 'Listado de parkings',
},
},
errors: {
statusUnauthorized: 'Acceso denegado',
@ -744,6 +751,18 @@ export default {
recyclable: 'Reciclable',
},
},
parking: {
pickingOrder: 'Orden de recogida',
row: 'Fila',
column: 'Columna',
pageTitles: {
parking: 'Parking',
},
searchBar: {
info: 'Puedes buscar por código de parking',
label: 'Buscar parking...',
},
},
invoiceIn: {
pageTitles: {
invoiceIns: 'Fact. recibidas',

View File

@ -199,7 +199,7 @@ function getLink(param) {
<template>
<CardSummary
ref="summary"
data-key="InvoiceInSummary"
Review

Como se hace uso de arrayData en InvoiceInCard, se le tiene que poner otra clave. Ya que no gasta el mismo origen de datos.

Como se hace uso de arrayData en InvoiceInCard, se le tiene que poner otra clave. Ya que no gasta el mismo origen de datos.
:url="`InvoiceIns/${entityId}/summary`"
@on-fetch="(data) => setData(data)"
>

View File

@ -0,0 +1,56 @@
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FormModel from 'components/FormModel.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
const { t } = useI18n();
const route = useRoute();
const parkingId = route.params?.id || null;
const sectors = ref([]);
const sectorFilter = { fields: ['id', 'description'] };
const filter = {
fields: ['sectorFk', 'code', 'pickingOrder', 'row', 'column'],
include: [{ relation: 'sector', scope: sectorFilter }],
};
</script>
<template>
<FetchData
url="Sectors"
jorgep marked this conversation as resolved
Review

Podemos usar los valores de filter.include.scope.fields?

Podemos usar los valores de filter.include.scope.fields?
Review

Me ha gustado como lo has enfocado al reves de lo que decia! 👌

Me ha gustado como lo has enfocado al reves de lo que decia! 👌
:filter="sectorFilter"
sort-by="description"
@on-fetch="(data) => (sectors = data)"
auto-load
/>
<VnSubToolbar />
<FormModel :url="`Parkings/${parkingId}`" model="parking" :filter="filter" auto-load>
<template #form="{ data }">
<VnRow>
<VnInput v-model="data.code" :label="t('globals.code')" />
<VnInput v-model="data.pickingOrder" :label="t('parking.pickingOrder')" />
</VnRow>
<VnRow>
<VnInput v-model="data.row" :label="t('parking.row')" />
<VnInput v-model="data.column" :label="t('parking.column')" />
</VnRow>
<VnRow>
<VnSelectFilter
v-model="data.sectorFk"
jorgep marked this conversation as resolved Outdated

El campo debería ser requerido si al enviar nulo da error.

O cambiar el mensaje de error en salix para que sea mas informativo al usuario y no de pie a pensar que algo está hecho mal.

El campo debería ser requerido si al enviar nulo da error. O cambiar el mensaje de error en salix para que sea mas informativo al usuario y no de pie a pensar que algo está hecho mal.

Esto habría que aplicarlo a nivel de FormModel, actualmente pasa en cualquier sección que inserte un campo que es obligatorio en bd, yo crearía tarea para hacer refactor de formModel. De momento, lo soluciono con la prop is-clearable="false". Pero que baraje @alexm y @jgallego si crear tarea.

Esto habría que aplicarlo a nivel de FormModel, actualmente pasa en cualquier sección que inserte un campo que es obligatorio en bd, yo crearía tarea para hacer refactor de formModel. De momento, lo soluciono con la prop is-clearable="false". Pero que baraje @alexm y @jgallego si crear tarea.
option-value="id"
jorgep marked this conversation as resolved Outdated
Outdated
Review

Al final un VnRow con un unico elemento se veia bien?

Al final un VnRow con un unico elemento se veia bien?

sí, te aplica el flex 1, ocupando todo el ancho

sí, te aplica el flex 1, ocupando todo el ancho
option-label="description"
:label="t('parking.sector')"
:options="sectors"
use-input
input-debounce="0"
:is-clearable="false"
/>
</VnRow>
</template>
</FormModel>
</template>

View File

@ -0,0 +1,57 @@
<script setup>
import { onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useArrayData } from 'src/composables/useArrayData';
import LeftMenu from 'components/LeftMenu.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import ParkingDescriptor from 'pages/Parking/Card/ParkingDescriptor.vue';
const { t } = useI18n();
const route = useRoute();
const stateStore = useStateStore();
const filter = {
fields: ['id', 'sectorFk', 'code', 'pickingOrder', 'row', 'column'],
include: [{ relation: 'sector', scope: { fields: ['id', 'description'] } }],
};
const arrayData = useArrayData('Parking', {
url: `Parkings/${route.params.id}`,
filter,
});
const { store } = arrayData;
onMounted(async () => await arrayData.fetch({ append: false }));
watch(
() => route.params.id,
async (newId) => {
if (newId) {
store.url = `Parkings/${newId}`;
store.filter = filter;
await arrayData.fetch({ append: false });
}
}
);
</script>
<template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<VnSearchbar
:info="t('parking.searchBar.info')"
:label="t('parking.searchBar.label')"
data-key="Parking"
/>
</Teleport>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<ParkingDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<RouterView></RouterView>
</QPage>
</QPageContainer>
</template>

View File

@ -0,0 +1,42 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'components/ui/VnLv.vue';
const props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const { t } = useI18n();
const { params } = useRoute();
const entityId = computed(() => props.id || params.id);
const { store } = useArrayData('Parking');
const card = computed(() => store.data);
const filter = {
Review

Yo valoraría crear un archivo general con los filtros de las cards de cada sección, porque si no hay que escribir el filtro. Si entramos en la sección summary no hay problema, se puede recoger el filtro desde la store, pero si abrimos el summaryProxy o popup, este no pasa por card y no tiene acceso al descriptor.

Yo valoraría crear un archivo general con los filtros de las cards de cada sección, porque si no hay que escribir el filtro. Si entramos en la sección summary no hay problema, se puede recoger el filtro desde la store, pero si abrimos el summaryProxy o popup, este no pasa por card y no tiene acceso al descriptor.
fields: ['id', 'sectorFk', 'code', 'pickingOrder', 'row', 'column'],
include: [{ relation: 'sector', scope: { fields: ['id', 'description'] } }],
};
</script>
<template>
<CardDescriptor
module="Parking"
data-key="Parking"
jorgep marked this conversation as resolved Outdated
Outdated
Review

No pot gastar les dades que hi han en la store? les seteges en parkingCard

No pot gastar les dades que hi han en la store? les seteges en parkingCard

Alex tiene razón, sin embargo, deberías comprobar que en parkingCard y este fichero solicitas los mismos

Alex tiene razón, sin embargo, deberías comprobar que en parkingCard y este fichero solicitas los mismos

Es lo mismo, tienen la misma url y filtro.

Es lo mismo, tienen la misma url y filtro.
:url="`Parkings/${entityId}`"
:title="card?.code"
:subtitle="card?.id"
:filter="filter"
>
<template #body="{ entity: parking }">
<VnLv :label="t('globals.code')" :value="parking.code" />
<VnLv :label="t('parking.pickingOrder')" :value="parking.pickingOrder" />
<VnLv :label="t('parking.sector')" :value="parking.sector?.description" />
</template>
</CardDescriptor>
</template>

View File

@ -0,0 +1,6 @@
<script setup>
import VnLog from 'src/components/common/VnLog.vue';
</script>
<template>
<VnLog model="Parking" url="/ParkingLogs" />
</template>

View File

@ -0,0 +1,54 @@
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'components/ui/VnLv.vue';
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const { params } = useRoute();
const { t } = useI18n();
const entityId = computed(() => $props.id || params.id);
const filter = {
Review

Yo valoraría crear un archivo general con los filtros de las cards de cada sección, porque si no hay que escribir el filtro. Si entramos en la sección summary no hay problema, se puede recoger el filtro desde la store, pero si abrimos el summaryProxy o popup, este no pasa por card y no tiene acceso al descriptor.

Yo valoraría crear un archivo general con los filtros de las cards de cada sección, porque si no hay que escribir el filtro. Si entramos en la sección summary no hay problema, se puede recoger el filtro desde la store, pero si abrimos el summaryProxy o popup, este no pasa por card y no tiene acceso al descriptor.
fields: ['id', 'sectorFk', 'code', 'pickingOrder', 'row', 'column'],
Outdated
Review

No pot gastar les dades que hi han en la store? les seteges en parkingCard

No pot gastar les dades que hi han en la store? les seteges en parkingCard

Sí pero hay que modificar la lógica de CardSummary, voy a dejarme esto en pendiente. Hoy subiré la rama con los cambios aplicados para que puedas ver como lo he hecho en el descriptor. No altera el funcionamiento del resto de la página.

Sé que te refieres a importar arrayData en parking summary y usarlo en CardSummary pero eso no tiene sentido porque sigues haciendo la llamada en CardSummary para usar los datos. Yo esto lo abordaría en una tarea. Avisame si quieres que lo miremos en llamada mañana.

Sí pero hay que modificar la lógica de CardSummary, voy a dejarme esto en pendiente. Hoy subiré la rama con los cambios aplicados para que puedas ver como lo he hecho en el descriptor. No altera el funcionamiento del resto de la página. Sé que te refieres a importar arrayData en parking summary y usarlo en CardSummary pero eso no tiene sentido porque sigues haciendo la llamada en CardSummary para usar los datos. Yo esto lo abordaría en una tarea. Avisame si quieres que lo miremos en llamada mañana.

Tras hablar con Alex , decidimos hacer un refactor en CardSummary para que use el arrayData e intentarlo con FormModel también.

Tras hablar con Alex , decidimos hacer un refactor en CardSummary para que use el arrayData e intentarlo con FormModel también.

El formModel lo paso a otra tarea https://redmine.verdnatura.es/issues/7135, que se me está alargando la tarea,

El formModel lo paso a otra tarea https://redmine.verdnatura.es/issues/7135, que se me está alargando la tarea,
include: [{ relation: 'sector', scope: { fields: ['id', 'description'] } }],
};
</script>
<template>
<div class="q-pa-md">
<CardSummary :url="`Parkings/${entityId}`" :filter="filter">
<template #header="{ entity: parking }">{{ parking.code }}</template>
<template #body="{ entity: parking }">
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<a
class="header header-link"
jorgep marked this conversation as resolved Outdated

Diria que no tiene el estilo correcto

Diria que no tiene el estilo correcto
:href="`#/parking/${entityId}/basic-data`"
>
{{ t('globals.pageTitles.basicData') }}
<QIcon name="open_in_new" />
</a>
</QCardSection>
<VnLv :label="t('globals.code')" :value="parking.code" />
<VnLv
:label="t('parking.pickingOrder')"
:value="parking.pickingOrder"
/>
<VnLv
:label="t('parking.sector')"
:value="parking.sector?.description"
/>
<VnLv :label="t('parking.row')" :value="parking.row" />
<VnLv :label="t('parking.column')" :value="parking.column" />
</QCard>
</template>
</CardSummary>
</div>
</template>

View File

@ -0,0 +1,77 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnInput from 'components/common/VnInput.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
const { t } = useI18n();
defineProps({
dataKey: {
type: String,
required: true,
},
});
const sectors = ref([]);
const emit = defineEmits(['search']);
</script>
<template>
<FetchData
url="Sectors"
:filter="{ fields: ['id', 'description'] }"
sort-by="description"
@on-fetch="(data) => (sectors = data)"
auto-load
/>
<VnFilterPanel :data-key="dataKey" :search-button="true" @search="emit('search')">
<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 }">
<QItem>
<QItemSection>
<VnInput
:label="t('params.code')"
v-model="params.code"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelectFilter
v-model="params.sectorFk"
option-value="id"
option-label="description"
:label="t('params.sectorFk')"
dense
outlined
rounded
:options="sectors"
use-input
input-debounce="0"
/>
</QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
code: Code
sectorFk: Sector
search: General Search
es:
params:
code: Código
search: Búsqueda general
</i18n>

View File

@ -0,0 +1,112 @@
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import CardList from 'components/ui/CardList.vue';
import VnLv from 'components/ui/VnLv.vue';
import ParkingFilter from './ParkingFilter.vue';
import ParkingSummary from './Card/ParkingSummary.vue';
const stateStore = useStateStore();
const { push } = useRouter();
const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false));
const filter = {
fields: ['id', 'sectorFk', 'code', 'pickingOrder'],
include: [{ relation: 'sector', scope: { fields: ['id', 'description'] } }],
};
function exprBuilder(param, value) {
switch (param) {
case 'code':
return { [param]: { like: `%${value}%` } };
case 'sectorFk':
return { [param]: value };
case 'search':
return { or: [{ code: { like: `%${value}%` } }, { id: value }] };
}
}
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
jorgep marked this conversation as resolved
Review

La opcion de PArking en el breadcrum no lleva a la parking, pero si monta bien la URL

La opcion de PArking en el breadcrum no lleva a la parking, pero si monta bien la URL
data-key="ParkingList"
:label="t('Search parking')"
:info="t('You can search by parking code')"
/>
</Teleport>
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<ParkingFilter data-key="ParkingList" />
</QScrollArea>
</QDrawer>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnPaginate
data-key="ParkingList"
url="Parkings"
:filter="filter"
:expr-builder="exprBuilder"
:limit="20"
auto-load
order="code"
>
<template #body="{ rows }">
<CardList
v-for="row of rows"
:key="row.id"
:id="row.id"
:title="row.code"
@click="push({ path: `/parking/${row.id}` })"
>
<template #list-items>
<VnLv label="Sector" :value="row.sector?.description" />
<VnLv
:label="t('parking.pickingOrder')"
:value="row.pickingOrder"
/>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, ParkingSummary)"
color="primary"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
</QPage>
</template>
<i18n>
es:
Search parking: Buscar parking
You can search by parking code: Puede buscar por el código del parking
</i18n>

View File

@ -15,7 +15,7 @@ import { toLowerCamel } from 'src/filters';
const state = useState();
const session = useSession();
const { t } = i18n.global;
const { t, te } = i18n.global;
const createHistory = process.env.SERVER
? createMemoryHistory
@ -90,7 +90,10 @@ export default route(function (/* { store, ssrContext } */) {
if (childPageTitle && matches.length > 2) {
if (title != '') title += ': ';
const pageTitle = t(`${moduleName}.pageTitles.${childPageTitle}`);
const moduleLocale = `${moduleName}.pageTitles.${childPageTitle}`;
const pageTitle = te(moduleLocale)
? t(moduleLocale)
: t(`globals.pageTitles.${childPageTitle}`);
const idParam = to.params && to.params.id;
const idPageTitle = `${idParam} - ${pageTitle}`;
const builtTitle = idParam ? idPageTitle : pageTitle;

View File

@ -13,7 +13,8 @@ import Travel from './travel';
import Order from './order';
import Department from './department';
import Entry from './entry';
import roadmap from "./roadmap";
import roadmap from './roadmap';
import Parking from './parking';
export default [
Item,
@ -31,5 +32,6 @@ export default [
invoiceIn,
Department,
Entry,
roadmap
roadmap,
Parking,
];

View File

@ -0,0 +1,54 @@
import { RouterView } from 'vue-router';
export default {
path: '/parking',
name: 'Parking',
meta: {
title: 'parking',
icon: 'garage_home',
moduleName: 'Parking',
},
component: RouterView,
redirect: { name: 'ParkingCard' },
menus: {
main: [],
card: ['ParkingBasicData', 'ParkingLog'],
},
children: [
{
path: '/parking/:id',
name: 'ParkingCard',
component: () => import('src/pages/Parking/Card/ParkingCard.vue'),
redirect: { name: 'ParkingSummary' },
children: [
{
name: 'ParkingSummary',
path: 'summary',
meta: {
title: 'summary',
icon: 'view_list',
},
component: () => import('src/pages/Parking/Card/ParkingSummary.vue'),
},
{
name: 'ParkingBasicData',
path: 'basic-data',
meta: {
title: 'basicData',
icon: 'vn:settings',
},
component: () => import('pages/Parking/Card/ParkingBasicData.vue'),
},
{
name: 'ParkingLog',
path: 'log',
meta: {
title: 'log',
icon: 'history',
},
component: () => import('src/pages/Parking/Card/ParkingLog.vue'),
},
],
},
],
};

View File

@ -11,7 +11,7 @@ export default {
component: RouterView,
redirect: { name: 'ShelvingMain' },
menus: {
main: ['ShelvingList'],
main: ['ShelvingList', 'ParkingList'],
card: ['ShelvingBasicData', 'ShelvingLog'],
},
children: [
@ -38,6 +38,21 @@ export default {
},
component: () => import('src/pages/Shelving/Card/ShelvingForm.vue'),
},
{
path: '/parking',
redirect: { name: 'ParkingList' },
children: [
{
path: 'list',
name: 'ParkingList',
meta: {
title: 'parkingList',
icon: 'view_list',
},
component: () => import('src/pages/Parking/ParkingList.vue'),
},
],
},
],
},
{

View File

@ -13,7 +13,8 @@ import department from './modules/department';
import shelving from 'src/router/modules/shelving';
import order from 'src/router/modules/order';
import entry from 'src/router/modules/entry';
import roadmap from "src/router/modules/roadmap";
import roadmap from 'src/router/modules/roadmap';
import parking from 'src/router/modules/parking';
const routes = [
{
@ -69,6 +70,7 @@ const routes = [
department,
roadmap,
entry,
parking,
{
path: '/:catchAll(.*)*',
name: 'NotFound',

View File

@ -0,0 +1,23 @@
/// <reference types="cypress" />
describe('ParkingBasicData', () => {
const codeInput = 'form .q-card .q-input input';
const sectorSelect = 'form .q-card .q-select input';
const sectorOpt = '.q-menu .q-item';
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/parking/1/basic-data`);
});
it('should edit the code and sector', () => {
cy.get(sectorSelect).type('Second');
cy.get(sectorOpt).click();
cy.get(codeInput).eq(0).clear();
cy.get(codeInput).eq(0).type(123);
cy.saveCard();
cy.get(sectorSelect).should('have.value', 'Second sector');
cy.get(codeInput).should('have.value', 123);
});
});

View File

@ -0,0 +1,30 @@
/// <reference types="cypress" />
describe('ParkingList', () => {
const firstCard = '.q-card:nth-child(1)';
const firstChipId =
':nth-child(1) > :nth-child(1) > .justify-between > .flex > .q-chip > .q-chip__content';
const firstDetailBtn =
':nth-child(1) > :nth-child(1) > .card-list-body > .actions > .q-btn';
const summaryHeader = '.summaryBody .header';
jorgep marked this conversation as resolved
Review

Ya hay un comando para ello. Busca por el valor de la variable en el archivo commands

Ya hay un comando para ello. Busca por el valor de la variable en el archivo commands
Review

Le cambio el nombre oir closeSideMenu, ya que cerraría cualquiera de los 2. También modifico el selector , porque directamente con .fullscreen no funciona en esa sección. Así funciona en cualquier sección.

Le cambio el nombre oir closeSideMenu, ya que cerraría cualquiera de los 2. También modifico el selector , porque directamente con .fullscreen no funciona en esa sección. Así funciona en cualquier sección.
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/parking/list`);
cy.closeSideMenu();
});
it('should redirect on clicking a parking', () => {
cy.get(firstChipId)
.invoke('text')
.then((content) => {
const id = content.substring(4);
cy.get(firstCard).click();
cy.url().should('include', `/parking/${id}/summary`);
});
});
it('should open the details', () => {
cy.get(firstDetailBtn).click();
cy.get(summaryHeader).contains('Basic data');
});
});

View File

@ -5,15 +5,15 @@ describe('VnSearchBar', () => {
cy.visit('/');
});
it('should redirect to new customer', () => {
cy.visit('#/customer/1112/basic-data')
cy.openLeftMenu();
cy.get('.q-item > .q-item__label').should('have.text',' #1112')
cy.closeLeftMenu();
cy.clearSearchbar();
cy.writeSearchbar('1{enter}');
cy.openLeftMenu();
cy.get('.q-item > .q-item__label').should('have.text',' #1')
cy.closeLeftMenu();
});
it('should redirect to new customer', () => {
cy.visit('#/customer/1112/basic-data');
cy.openLeftMenu();
cy.get('.q-item > .q-item__label').should('have.text', ' #1112');
cy.closeSideMenu();
cy.clearSearchbar();
cy.writeSearchbar('1{enter}');
cy.openLeftMenu();
cy.get('.q-item > .q-item__label').should('have.text', ' #1');
cy.closeSideMenu();
});
});

View File

@ -52,16 +52,18 @@ Cypress.Commands.add('getValue', (selector) => {
}
// Si es un QSelect
if ($el.find('.q-select__dropdown-icon').length) {
return cy.get(
selector +
'> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input'
).invoke('val')
return cy
.get(
selector +
'> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input'
)
.invoke('val');
}
// Si es un QSelect
if ($el.find('span').length) {
return cy.get(
selector + ' span'
).then(($span) => { return $span[0].innerText })
return cy.get(selector + ' span').then(($span) => {
return $span[0].innerText;
});
}
// Puedes añadir un log o lanzar un error si el elemento no es reconocido
cy.log('Elemento no soportado');
@ -132,13 +134,13 @@ Cypress.Commands.add('validateRow', (rowSelector, expectedValues) => {
cy.get(rowSelector).within(() => {
for (const [index, value] of expectedValues.entries()) {
cy.log('CHECKING ', index, value);
if(value === undefined) continue
if (value === undefined) continue;
if (typeof value == 'boolean') {
const prefix = value ? '' : 'not.';
cy.getValue(`:nth-child(${index + 1})`).should(`${prefix}be.checked`);
continue;
}
cy.getValue(`:nth-child(${index + 1})`).should('equal', value)
cy.getValue(`:nth-child(${index + 1})`).should('equal', value);
}
});
});
@ -174,9 +176,9 @@ Cypress.Commands.add('openLeftMenu', (element) => {
if (element) cy.waitForElement(element);
cy.get('.q-toolbar > .q-btn--round.q-btn--dense > .q-btn__content > .q-icon').click();
});
Cypress.Commands.add('closeLeftMenu', (element) => {
Cypress.Commands.add('closeSideMenu', (element) => {
if (element) cy.waitForElement(element);
cy.get('.fullscreen').click();
cy.get('.fullscreen.q-drawer__backdrop:not(.hidden)').click();
});
Cypress.Commands.add('clearSearchbar', (element) => {