forked from verdnatura/salix-front
Merge pull request '4802- Added searchbar & filter panel' (!39) from 4802-searchbar into dev
Reviewed-on: verdnatura/salix-front#39
This commit is contained in:
commit
82c433d800
|
@ -31,8 +31,7 @@ pipeline {
|
|||
NODE_ENV = ""
|
||||
}
|
||||
steps {
|
||||
nodejs('node-v14') {
|
||||
sh 'npm install -g @quasar/cli'
|
||||
nodejs('node-v18') {
|
||||
sh 'npm install --no-audit --prefer-offline'
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +47,7 @@ pipeline {
|
|||
parallel {
|
||||
stage('Frontend') {
|
||||
steps {
|
||||
nodejs('node-v14') {
|
||||
nodejs('node-v18') {
|
||||
sh 'npm run test:unit:ci'
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +63,7 @@ pipeline {
|
|||
CREDENTIALS = credentials('docker-registry')
|
||||
}
|
||||
steps {
|
||||
nodejs('node-v14') {
|
||||
nodejs('node-v18') {
|
||||
sh 'quasar build'
|
||||
}
|
||||
dockerBuild()
|
||||
|
|
|
@ -30,12 +30,12 @@ quasar.iconMapFn = (iconName) => {
|
|||
const name = iconName.substring(3);
|
||||
|
||||
return {
|
||||
cls: `icon-${name}`,
|
||||
cls: `icon-${name} notranslate`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
cls: 'material-symbols-outlined',
|
||||
cls: 'material-symbols-outlined notranslate',
|
||||
content: iconName,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -3,11 +3,12 @@ import { createI18n } from 'vue-i18n';
|
|||
import messages from 'src/i18n';
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'en',
|
||||
locale: navigator.language || navigator.userLanguage,
|
||||
fallbackLocale: 'en',
|
||||
globalInjection: true,
|
||||
messages,
|
||||
missingWarn: false,
|
||||
fallbackWarn: false,
|
||||
legacy: false,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,43 +1,49 @@
|
|||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useState } from 'src/composables/useState';
|
||||
import { useSession } from 'src/composables/useSession';
|
||||
import UserPanel from 'components/UserPanel.vue';
|
||||
import { useState } from 'src/composables/useState';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import PinnedModules from './PinnedModules.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const session = useSession();
|
||||
const stateStore = useStateStore();
|
||||
const state = useState();
|
||||
const user = state.getUser();
|
||||
const token = session.getToken();
|
||||
const appName = 'Lilium';
|
||||
|
||||
onMounted(() => (state.headerMounted.value = true));
|
||||
|
||||
function onToggleDrawer() {
|
||||
state.drawer.value = !state.drawer.value;
|
||||
}
|
||||
onMounted(() => stateStore.setMounted());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-header class="bg-dark" color="white" elevated>
|
||||
<q-toolbar class="q-py-sm q-px-md">
|
||||
<q-btn flat @click="onToggleDrawer()" round dense icon="menu">
|
||||
<q-btn flat @click="stateStore.toggleLeftDrawer()" round dense icon="menu">
|
||||
<q-tooltip bottom anchor="bottom right">
|
||||
{{ t('globals.collapseMenu') }}
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<router-link to="/">
|
||||
<q-btn flat round class="q-ml-xs" v-if="$q.screen.gt.xs">
|
||||
<q-btn class="q-ml-xs" color="primary" v-if="$q.screen.gt.xs" flat round>
|
||||
<q-avatar square size="md">
|
||||
<q-img src="~/assets/logo_icon.svg" alt="Logo" />
|
||||
<q-img
|
||||
src="~/assets/logo_icon.svg"
|
||||
:alt="appName"
|
||||
spinner-color="primary"
|
||||
/>
|
||||
</q-avatar>
|
||||
<q-tooltip bottom>
|
||||
{{ t('globals.backToDashboard') }}
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</router-link>
|
||||
<q-toolbar-title shrink class="text-weight-bold">Salix</q-toolbar-title>
|
||||
<q-toolbar-title shrink class="text-weight-bold" v-if="$q.screen.gt.xs">
|
||||
{{ appName }}
|
||||
<q-badge label="Beta" align="top" />
|
||||
</q-toolbar-title>
|
||||
<q-space></q-space>
|
||||
<div id="searchbar"></div>
|
||||
<q-space></q-space>
|
||||
|
@ -49,11 +55,22 @@ function onToggleDrawer() {
|
|||
</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
|
||||
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
|
||||
spinner-color="white"
|
||||
spinner-color="primary"
|
||||
>
|
||||
</q-img>
|
||||
</q-avatar>
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
<script setup>
|
||||
import axios from 'axios';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useArrayData } from 'composables/useArrayData';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const $props = defineProps({
|
||||
const props = defineProps({
|
||||
dataKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
autoLoad: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
@ -26,15 +30,11 @@ const $props = defineProps({
|
|||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
sortBy: {
|
||||
order: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
limit: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
rowsPerPage: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
|
@ -45,71 +45,55 @@ const $props = defineProps({
|
|||
});
|
||||
|
||||
const emit = defineEmits(['onFetch', 'onPaginate']);
|
||||
defineExpose({ refresh });
|
||||
const isLoading = ref(false);
|
||||
const pagination = ref({
|
||||
sortBy: props.order,
|
||||
rowsPerPage: props.limit,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
const arrayData = useArrayData(props.dataKey, {
|
||||
url: props.url,
|
||||
filter: props.filter,
|
||||
where: props.where,
|
||||
limit: props.limit,
|
||||
order: props.order,
|
||||
});
|
||||
const store = arrayData.store;
|
||||
|
||||
onMounted(() => {
|
||||
if ($props.autoLoad) paginate();
|
||||
if (props.autoLoad) fetch();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => $props.data,
|
||||
() => props.data,
|
||||
() => {
|
||||
rows.value = $props.data;
|
||||
store.data = props.data;
|
||||
}
|
||||
);
|
||||
|
||||
const isLoading = ref(false);
|
||||
const hasMoreData = ref(false);
|
||||
const pagination = ref({
|
||||
sortBy: $props.sortBy,
|
||||
rowsPerPage: $props.rowsPerPage,
|
||||
page: 1,
|
||||
});
|
||||
const rows = ref(null);
|
||||
|
||||
async function fetch() {
|
||||
const { page, rowsPerPage, sortBy } = pagination.value;
|
||||
await arrayData.fetch({ append: false });
|
||||
if (!arrayData.hasMoreData.value) {
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
if (!$props.url) return;
|
||||
|
||||
const filter = {
|
||||
limit: rowsPerPage,
|
||||
skip: rowsPerPage * (page - 1),
|
||||
};
|
||||
|
||||
Object.assign(filter, $props.filter);
|
||||
|
||||
if ($props.where) filter.where = $props.where;
|
||||
if ($props.sortBy) filter.order = $props.sortBy;
|
||||
if ($props.limit) filter.limit = $props.limit;
|
||||
|
||||
if (sortBy) filter.order = sortBy;
|
||||
|
||||
const { data } = await axios.get($props.url, {
|
||||
params: { filter },
|
||||
});
|
||||
|
||||
isLoading.value = false;
|
||||
|
||||
return data;
|
||||
emit('onFetch', store.data);
|
||||
}
|
||||
|
||||
async function paginate() {
|
||||
const { page, rowsPerPage, sortBy, descending } = pagination.value;
|
||||
|
||||
const data = await fetch();
|
||||
if (!props.url) return;
|
||||
|
||||
if (!data) {
|
||||
await arrayData.loadMore();
|
||||
|
||||
if (!arrayData.hasMoreData.value) {
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
hasMoreData.value = data.length === rowsPerPage;
|
||||
|
||||
if (!rows.value) rows.value = [];
|
||||
for (const row of data) rows.value.push(row);
|
||||
|
||||
pagination.value.rowsNumber = rows.value.length;
|
||||
pagination.value.rowsNumber = store.data.length;
|
||||
pagination.value.page = page;
|
||||
pagination.value.rowsPerPage = rowsPerPage;
|
||||
pagination.value.sortBy = sortBy;
|
||||
|
@ -117,58 +101,37 @@ async function paginate() {
|
|||
|
||||
isLoading.value = false;
|
||||
|
||||
emit('onFetch', rows);
|
||||
emit('onPaginate', data);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const { rowsPerPage } = pagination.value;
|
||||
|
||||
const data = await fetch();
|
||||
|
||||
if (!data) {
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
hasMoreData.value = data.length === rowsPerPage;
|
||||
|
||||
if (!rows.value) rows.value = [];
|
||||
rows.value = data;
|
||||
|
||||
isLoading.value = false;
|
||||
|
||||
emit('onFetch', rows);
|
||||
emit('onFetch', store.data);
|
||||
emit('onPaginate');
|
||||
}
|
||||
|
||||
async function onLoad(...params) {
|
||||
if (!store.data) return;
|
||||
|
||||
const done = params[1];
|
||||
if (!rows.value || rows.value.length === 0 || !$props.url) return done(false);
|
||||
if (store.data.length === 0 || !props.url) return done(false);
|
||||
|
||||
pagination.value.page = pagination.value.page + 1;
|
||||
|
||||
await paginate();
|
||||
|
||||
const endOfPages = !hasMoreData.value;
|
||||
const endOfPages = !arrayData.hasMoreData.value;
|
||||
done(endOfPages);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-infinite-scroll @load="onLoad" :offset="offset" class="column items-center">
|
||||
<div v-if="rows" class="card-list q-gutter-y-md">
|
||||
<slot name="body" :rows="rows"></slot>
|
||||
<div v-if="!rows.length && !isLoading" class="info-row q-pa-md text-center">
|
||||
<h5>
|
||||
{{ t('components.smartCard.noData') }}
|
||||
</h5>
|
||||
</div>
|
||||
<div v-if="isLoading" class="info-row q-pa-md text-center">
|
||||
<q-spinner color="orange" size="md" />
|
||||
</div>
|
||||
<div class="column items-center">
|
||||
<div
|
||||
v-if="store.data && store.data.length === 0 && !isLoading"
|
||||
class="info-row q-pa-md text-center"
|
||||
>
|
||||
<h5>
|
||||
{{ t('components.smartCard.noData') }}
|
||||
</h5>
|
||||
</div>
|
||||
<div v-if="!rows" class="card-list q-gutter-y-md">
|
||||
<q-card class="card" v-for="$index in $props.rowsPerPage" :key="$index">
|
||||
<div v-if="props.autoLoad && !store.data" class="card-list q-gutter-y-md">
|
||||
<q-card class="card" v-for="$index in $props.limit" :key="$index">
|
||||
<q-item v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
|
||||
<q-item-section class="q-pa-md">
|
||||
<q-skeleton type="rect" class="q-mb-md" square />
|
||||
|
@ -186,6 +149,19 @@ async function onLoad(...params) {
|
|||
</q-item>
|
||||
</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">
|
||||
<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>
|
||||
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
<script setup>
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useArrayData } from 'composables/useArrayData';
|
||||
import toDate from 'filters/toDate';
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps({
|
||||
dataKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
searchButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['refresh', 'clear']);
|
||||
|
||||
const arrayData = useArrayData(props.dataKey);
|
||||
const store = arrayData.store;
|
||||
const userParams = ref({});
|
||||
|
||||
onMounted(() => {
|
||||
const params = store.userParams;
|
||||
if (params) {
|
||||
userParams.value = Object.assign({}, params);
|
||||
}
|
||||
});
|
||||
|
||||
const isLoading = ref(false);
|
||||
async function search() {
|
||||
const params = userParams.value;
|
||||
for (const param in params) {
|
||||
if (params[param] === '' || params[param] === null) {
|
||||
delete userParams.value[param];
|
||||
delete store.userParams[param];
|
||||
}
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
await arrayData.addFilter({ params });
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
isLoading.value = true;
|
||||
await arrayData.fetch({ append: false });
|
||||
isLoading.value = false;
|
||||
emit('refresh');
|
||||
}
|
||||
|
||||
async function clearFilters() {
|
||||
userParams.value = {};
|
||||
isLoading.value = true;
|
||||
await arrayData.applyFilter({ params: {} });
|
||||
isLoading.value = false;
|
||||
|
||||
emit('clear');
|
||||
}
|
||||
|
||||
const tags = computed(() => {
|
||||
const params = [];
|
||||
|
||||
for (const param in store.userParams) {
|
||||
params.push({
|
||||
label: param,
|
||||
value: store.userParams[param],
|
||||
});
|
||||
}
|
||||
|
||||
return params;
|
||||
});
|
||||
|
||||
async function remove(key) {
|
||||
delete userParams.value[key];
|
||||
delete store.userParams[key];
|
||||
await search();
|
||||
}
|
||||
|
||||
function formatValue(value) {
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? t('Yes') : t('No');
|
||||
}
|
||||
|
||||
if (isNaN(value) && !isNaN(Date.parse(value))) {
|
||||
return toDate(value);
|
||||
}
|
||||
|
||||
return `"${value}"`;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<q-form @submit="search">
|
||||
<q-list dense>
|
||||
<q-item class="q-mt-xs">
|
||||
<q-item-section top>
|
||||
<q-item-label
|
||||
header
|
||||
lines="1"
|
||||
class="text-uppercase q-py-xs q-px-none"
|
||||
>
|
||||
{{ t('Applied filters') }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section top side>
|
||||
<div class="q-gutter-xs">
|
||||
<q-btn
|
||||
@click="clearFilters"
|
||||
icon="filter_list_off"
|
||||
color="primary"
|
||||
size="sm"
|
||||
padding="none"
|
||||
round
|
||||
flat
|
||||
dense
|
||||
>
|
||||
<q-tooltip>{{ t('Remove filters') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
@click="reload"
|
||||
icon="refresh"
|
||||
color="primary"
|
||||
size="sm"
|
||||
padding="none"
|
||||
round
|
||||
flat
|
||||
dense
|
||||
>
|
||||
<q-tooltip>{{ t('Refresh') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<div v-if="tags.length === 0" class="text-grey centered font-xs">
|
||||
{{ t(`You didn't enter any filter`) }}
|
||||
</div>
|
||||
<div>
|
||||
<q-chip
|
||||
v-for="chip of tags"
|
||||
:key="chip.label"
|
||||
@remove="remove(chip.label)"
|
||||
icon="label"
|
||||
color="primary"
|
||||
class="text-dark"
|
||||
size="sm"
|
||||
removable
|
||||
>
|
||||
<slot name="tags" :tag="chip" :format-fn="formatValue">
|
||||
<div class="q-gutter-x-xs">
|
||||
<strong>{{ chip.label }}:</strong>
|
||||
<span>"{{ chip.value }}"</span>
|
||||
</div>
|
||||
</slot>
|
||||
</q-chip>
|
||||
</div>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<template v-if="props.searchButton">
|
||||
<q-item>
|
||||
<q-item-section class="q-py-sm">
|
||||
<q-btn
|
||||
:label="t('Search')"
|
||||
type="submit"
|
||||
color="primary"
|
||||
class="full-width"
|
||||
icon="search"
|
||||
unelevated
|
||||
rounded
|
||||
dense
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
</template>
|
||||
</q-list>
|
||||
<slot name="body" :params="userParams" :search-fn="search"></slot>
|
||||
</q-form>
|
||||
<q-inner-loading
|
||||
:showing="isLoading"
|
||||
:label="t('globals.pleaseWait')"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
es:
|
||||
You didn't enter any filter: No has introducido ningún filtro
|
||||
Applied filters: Filtros aplicados
|
||||
Remove filters: Eliminar filtros
|
||||
Refresh: Refrescar
|
||||
Search: Buscar
|
||||
Yes: Si
|
||||
No: No
|
||||
</i18n>
|
|
@ -0,0 +1,105 @@
|
|||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useArrayData } from 'composables/useArrayData';
|
||||
|
||||
const props = defineProps({
|
||||
dataKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'Search',
|
||||
},
|
||||
info: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
redirect: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const arrayData = useArrayData(props.dataKey);
|
||||
const store = arrayData.store;
|
||||
const searchText = ref('');
|
||||
|
||||
onMounted(() => {
|
||||
const params = store.userParams;
|
||||
if (params && params.search) {
|
||||
searchText.value = params.search;
|
||||
}
|
||||
});
|
||||
|
||||
async function search() {
|
||||
await arrayData.applyFilter({
|
||||
params: {
|
||||
search: searchText.value,
|
||||
},
|
||||
});
|
||||
if (!props.redirect) return;
|
||||
|
||||
const rows = store.data;
|
||||
const module = route.matched[1];
|
||||
if (rows.length === 1) {
|
||||
const [firstRow] = rows;
|
||||
await router.push({ path: `/${module.name}/${firstRow.id}` });
|
||||
} else if (route.matched.length > 3) {
|
||||
await router.push({ path: `/${module.name}` });
|
||||
arrayData.updateStateParams();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-form @submit="search">
|
||||
<q-input
|
||||
id="searchbar"
|
||||
v-model="searchText"
|
||||
:placeholder="props.label"
|
||||
dense
|
||||
standout
|
||||
autofocus
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
<template #append>
|
||||
<q-icon
|
||||
v-if="searchText !== ''"
|
||||
name="close"
|
||||
@click="searchText = ''"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
|
||||
<q-icon v-if="props.info" name="info" class="cursor-info">
|
||||
<q-tooltip>{{ props.info }}</q-tooltip>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-form>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.cursor-info {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
#searchbar .q-field {
|
||||
min-width: 350px;
|
||||
}
|
||||
|
||||
.body--light #searchbar {
|
||||
.q-field--standout.q-field--highlighted .q-field__control {
|
||||
background-color: $grey-7;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,149 @@
|
|||
import { onMounted, ref, computed } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import { useArrayDataStore } from 'stores/useArrayDataStore';
|
||||
|
||||
const arrayDataStore = useArrayDataStore();
|
||||
|
||||
export function useArrayData(key, userOptions) {
|
||||
if (!key) throw new Error('ArrayData: A key is required to use this composable');
|
||||
|
||||
if (!arrayDataStore.get(key)) {
|
||||
arrayDataStore.set(key);
|
||||
}
|
||||
|
||||
const store = arrayDataStore.get(key);
|
||||
const hasMoreData = ref(false);
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
let canceller = null;
|
||||
|
||||
const page = ref(1);
|
||||
|
||||
if (typeof userOptions === 'object') {
|
||||
if (userOptions.filter) store.filter = userOptions.filter;
|
||||
if (userOptions.url) store.url = userOptions.url;
|
||||
if (userOptions.limit) store.limit = userOptions.limit;
|
||||
if (userOptions.order) store.order = userOptions.order;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const query = route.query;
|
||||
if (query.params) {
|
||||
store.userParams = JSON.parse(query.params);
|
||||
}
|
||||
});
|
||||
|
||||
async function fetch({ append = false }) {
|
||||
if (!store.url) return;
|
||||
|
||||
cancelRequest();
|
||||
canceller = new AbortController();
|
||||
|
||||
const filter = {
|
||||
order: store.order,
|
||||
limit: store.limit,
|
||||
skip: store.skip,
|
||||
};
|
||||
|
||||
Object.assign(filter, store.userFilter);
|
||||
Object.assign(store.filter, filter);
|
||||
|
||||
const params = {
|
||||
filter: JSON.stringify(filter),
|
||||
};
|
||||
|
||||
Object.assign(params, store.userParams);
|
||||
|
||||
const response = await axios.get(store.url, {
|
||||
signal: canceller.signal,
|
||||
params,
|
||||
});
|
||||
|
||||
const { limit } = filter;
|
||||
|
||||
hasMoreData.value = response.data.length === limit;
|
||||
|
||||
if (append === true) {
|
||||
if (!store.data) store.data = [];
|
||||
for (const row of response.data) store.data.push(row);
|
||||
}
|
||||
|
||||
if (append === false) {
|
||||
store.data = response.data;
|
||||
|
||||
updateStateParams();
|
||||
}
|
||||
|
||||
canceller = null;
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (arrayDataStore.get(key)) {
|
||||
arrayDataStore.clear(key);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelRequest() {
|
||||
if (canceller) {
|
||||
canceller.abort();
|
||||
canceller = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyFilter({ filter, params }) {
|
||||
if (filter) store.userFilter = filter;
|
||||
if (params) store.userParams = Object.assign({}, params);
|
||||
|
||||
await fetch({ append: false });
|
||||
}
|
||||
|
||||
async function addFilter({ filter, params }) {
|
||||
if (filter) store.userFilter = Object.assign(store.userFilter, filter);
|
||||
if (params) store.userParams = Object.assign(store.userParams, params);
|
||||
|
||||
await fetch({ append: false });
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (!hasMoreData.value) return;
|
||||
|
||||
store.skip = store.limit * page.value;
|
||||
page.value += 1;
|
||||
|
||||
await fetch({ append: true });
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await fetch({ append: false });
|
||||
}
|
||||
|
||||
function updateStateParams() {
|
||||
const query = {};
|
||||
if (store.order) query.order = store.order;
|
||||
if (store.limit) query.limit = store.limit;
|
||||
if (store.skip) query.skip = store.skip;
|
||||
if (store.userParams && Object.keys(store.userParams).length !== 0)
|
||||
query.params = JSON.stringify(store.userParams);
|
||||
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: query,
|
||||
});
|
||||
}
|
||||
|
||||
const totalRows = computed(() => store.data && store.data.length | 0);
|
||||
|
||||
return {
|
||||
fetch,
|
||||
applyFilter,
|
||||
addFilter,
|
||||
refresh,
|
||||
destroy,
|
||||
loadMore,
|
||||
store,
|
||||
hasMoreData,
|
||||
totalRows,
|
||||
updateStateParams,
|
||||
};
|
||||
}
|
|
@ -13,3 +13,20 @@ a {
|
|||
.link:hover {
|
||||
color: $orange-4;
|
||||
}
|
||||
|
||||
// Removes chrome autofill background
|
||||
input:-webkit-autofill,
|
||||
select:-webkit-autofill {
|
||||
color: $input-text-color !important;
|
||||
font-family: $typography-font-family;
|
||||
-webkit-text-fill-color: $input-text-color !important;
|
||||
-webkit-background-clip: text !important;
|
||||
background-clip: text !important;
|
||||
}
|
||||
|
||||
body.body--light {
|
||||
.q-header .q-toolbar {
|
||||
background-color: white;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@ $primary: #ff9800;
|
|||
$secondary: #26a69a;
|
||||
$accent: #9c27b0;
|
||||
|
||||
$dark: #1d1d1d;
|
||||
|
||||
$positive: #21ba45;
|
||||
$negative: #c10015;
|
||||
$info: #31ccec;
|
||||
|
@ -30,5 +28,4 @@ $border-thin-light: 1px solid $color-spacer-light;
|
|||
$dark-shadow-color: #000;
|
||||
$dark: #292929;
|
||||
$layout-shadow-dark: 0 0 10px 2px rgba(0, 0, 0, 0.2), 0 0px 10px rgba(0, 0, 0, 0.24);
|
||||
|
||||
$spacing-md: 16px;
|
||||
|
|
|
@ -3,10 +3,14 @@ import { useI18n } from 'vue-i18n';
|
|||
export default function (value, options = {}) {
|
||||
if (!value) return;
|
||||
|
||||
if (!options.dateStyle) options.dateStyle = 'short';
|
||||
if (!options.dateStyle && !options.timeStyle) {
|
||||
options.day = '2-digit';
|
||||
options.month = '2-digit';
|
||||
options.year = 'numeric';
|
||||
}
|
||||
|
||||
const { locale } = useI18n();
|
||||
const date = new Date(value);
|
||||
|
||||
return new Intl.DateTimeFormat(locale.value, options).format(date)
|
||||
}
|
||||
return new Intl.DateTimeFormat(locale.value, options).format(date);
|
||||
}
|
||||
|
|
|
@ -2,13 +2,26 @@
|
|||
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>
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
<script setup>
|
||||
import { useState } from 'composables/useState';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import ClaimDescriptor from './ClaimDescriptor.vue';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
import TeleportSlot from 'components/ui/TeleportSlot.vue';
|
||||
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
|
||||
|
||||
const state = useState();
|
||||
const stateStore = useStateStore();
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<teleport-slot to="#searchbar">
|
||||
<VnSearchbar
|
||||
data-key="ClaimList"
|
||||
:label="t('Search claim')"
|
||||
:info="t('You can search by claim id or customer name')"
|
||||
/>
|
||||
</teleport-slot>
|
||||
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
|
||||
<q-scroll-area class="fit">
|
||||
<claim-descriptor />
|
||||
<q-separator />
|
||||
|
@ -19,3 +30,9 @@ const state = useState();
|
|||
</q-page>
|
||||
</q-page-container>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
es:
|
||||
Search claim: Buscar reclamación
|
||||
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
|
||||
</i18n>
|
||||
|
|
|
@ -1,37 +1,49 @@
|
|||
<script setup>
|
||||
import axios from 'axios';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useRoute } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import { useArrayData } from 'src/composables/useArrayData';
|
||||
import Paginate from 'src/components/PaginateData.vue';
|
||||
import FetchData from 'components/FetchData.vue';
|
||||
import TeleportSlot from 'components/ui/TeleportSlot.vue';
|
||||
import VnConfirm from 'src/components/ui/VnConfirm.vue';
|
||||
|
||||
import { toDate } from 'src/filters';
|
||||
|
||||
const quasar = useQuasar();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const arrayData = useArrayData('ClaimRma');
|
||||
|
||||
const claim = ref([]);
|
||||
const claim = ref();
|
||||
const fetcher = ref();
|
||||
|
||||
const filter = {
|
||||
include: {
|
||||
relation: 'rmas',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'worker',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'user',
|
||||
},
|
||||
const claimFilter = {
|
||||
fields: ['rma'],
|
||||
};
|
||||
|
||||
async function onFetch(data) {
|
||||
claim.value = data;
|
||||
|
||||
const filter = {
|
||||
include: {
|
||||
relation: 'worker',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'user',
|
||||
},
|
||||
},
|
||||
order: 'created DESC',
|
||||
},
|
||||
},
|
||||
};
|
||||
order: 'created DESC',
|
||||
where: {
|
||||
code: claim.value.rma,
|
||||
},
|
||||
};
|
||||
|
||||
arrayData.applyFilter({ filter });
|
||||
}
|
||||
|
||||
async function addRow() {
|
||||
const formData = {
|
||||
|
@ -39,7 +51,7 @@ async function addRow() {
|
|||
};
|
||||
|
||||
await axios.post(`ClaimRmas`, formData);
|
||||
await fetcher.value.fetch();
|
||||
await arrayData.refresh();
|
||||
|
||||
quasar.notify({
|
||||
type: 'positive',
|
||||
|
@ -48,19 +60,17 @@ async function addRow() {
|
|||
});
|
||||
}
|
||||
|
||||
const confirmShown = ref(false);
|
||||
const rmaId = ref(null);
|
||||
function confirmRemove(id) {
|
||||
confirmShown.value = true;
|
||||
rmaId.value = id;
|
||||
quasar
|
||||
.dialog({
|
||||
component: VnConfirm,
|
||||
})
|
||||
.onOk(() => remove(id));
|
||||
}
|
||||
|
||||
async function remove() {
|
||||
const id = rmaId.value;
|
||||
|
||||
async function remove(id) {
|
||||
await axios.delete(`ClaimRmas/${id}`);
|
||||
await fetcher.value.fetch();
|
||||
confirmShown.value = false;
|
||||
await arrayData.refresh();
|
||||
|
||||
quasar.notify({
|
||||
type: 'positive',
|
||||
|
@ -68,41 +78,36 @@ async function remove() {
|
|||
icon: 'check',
|
||||
});
|
||||
}
|
||||
|
||||
function hide() {
|
||||
rmaId.value = null;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<fetch-data
|
||||
ref="fetcher"
|
||||
:url="`Claims/${route.params.id}`"
|
||||
:filter="filter"
|
||||
@on-fetch="(data) => (claim = data)"
|
||||
:filter="claimFilter"
|
||||
@on-fetch="onFetch"
|
||||
auto-load
|
||||
/>
|
||||
<paginate :data="claim.rmas">
|
||||
<paginate data-key="ClaimRma" url="ClaimRmas">
|
||||
<template #body="{ rows }">
|
||||
<q-card class="card">
|
||||
<template v-for="row of rows" :key="row.id">
|
||||
<template v-for="(row, index) of rows" :key="row.id">
|
||||
<q-item class="q-pa-none items-start">
|
||||
<q-item-section class="q-pa-md">
|
||||
<q-list>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{
|
||||
t('claim.rma.user')
|
||||
}}</q-item-label>
|
||||
<q-item-label>{{
|
||||
row.worker.user.name
|
||||
}}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ t('claim.rma.user') }}
|
||||
</q-item-label>
|
||||
<q-item-label>
|
||||
{{ row.worker.user.name }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{
|
||||
t('claim.rma.created')
|
||||
}}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ t('claim.rma.created') }}
|
||||
</q-item-label>
|
||||
<q-item-label>
|
||||
{{
|
||||
toDate(row.created, {
|
||||
|
@ -126,31 +131,12 @@ function hide() {
|
|||
</q-btn>
|
||||
</q-card-actions>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-separator v-if="index !== rows.length - 1" />
|
||||
</template>
|
||||
</q-card>
|
||||
</template>
|
||||
</paginate>
|
||||
|
||||
<q-dialog v-model="confirmShown" persistent @hide="hide">
|
||||
<q-card>
|
||||
<q-card-section class="row items-center">
|
||||
<q-avatar icon="warning" color="primary" text-color="white" />
|
||||
<span class="q-ml-sm">{{ t('globals.confirmRemove') }}</span>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
flat
|
||||
:label="t('globals.no')"
|
||||
color="primary"
|
||||
v-close-popup
|
||||
autofocus
|
||||
/>
|
||||
<q-btn flat :label="t('globals.yes')" color="primary" @click="remove()" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<teleport-slot v-if="!quasar.platform.is.mobile" to="#header-actions">
|
||||
<div class="row q-gutter-x-sm">
|
||||
<q-btn @click="addRow()" icon="add" color="primary" dense rounded>
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
<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();
|
||||
const states = ref();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fetch-data url="ClaimStates" @on-fetch="(data) => (states = data)" auto-load />
|
||||
<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-list dense>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
:label="t('Customer ID')"
|
||||
v-model="params.clientFk"
|
||||
lazy-rules
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="badge" size="sm"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
:label="t('Client Name')"
|
||||
v-model="params.clientName"
|
||||
lazy-rules
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<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('Salesperson')"
|
||||
v-model="params.salesPersonFk"
|
||||
@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-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('Attender')"
|
||||
v-model="params.attenderFk"
|
||||
@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-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('Responsible')"
|
||||
v-model="params.claimResponsibleFk"
|
||||
@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-item class="q-mb-md">
|
||||
<q-item-section v-if="!states">
|
||||
<q-skeleton type="QInput" class="full-width" />
|
||||
</q-item-section>
|
||||
<q-item-section v-if="states">
|
||||
<q-select
|
||||
:label="t('State')"
|
||||
v-model="params.claimStateFk"
|
||||
@update:model-value="searchFn()"
|
||||
:options="states"
|
||||
option-value="id"
|
||||
option-label="description"
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-expansion-item :label="t('More options')" expand-separator>
|
||||
<!-- <q-item>
|
||||
<q-item-section>
|
||||
<q-select
|
||||
:label="t('Item')"
|
||||
v-model="params.itemFk"
|
||||
:options="items"
|
||||
:loading="loading"
|
||||
@filter="filterFn"
|
||||
@virtual-scroll="onScroll"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item> -->
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
v-model="params.created"
|
||||
:label="t('Created')"
|
||||
autofocus
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy
|
||||
cover
|
||||
transition-show="scale"
|
||||
transition-hide="scale"
|
||||
>
|
||||
<q-date v-model="params.created">
|
||||
<div class="row items-center justify-end">
|
||||
<q-btn
|
||||
v-close-popup
|
||||
label="Close"
|
||||
color="primary"
|
||||
flat
|
||||
/>
|
||||
</div>
|
||||
</q-date>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</template>
|
||||
</VnFilterPanel>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
en:
|
||||
params:
|
||||
search: Contains
|
||||
clientFk: Customer
|
||||
clientName: Customer
|
||||
salesPersonFk: Salesperson
|
||||
attenderFk: Attender
|
||||
claimResponsibleFk: Responsible
|
||||
claimStateFk: State
|
||||
created: Created
|
||||
es:
|
||||
params:
|
||||
search: Contiene
|
||||
clientFk: Cliente
|
||||
clientName: Cliente
|
||||
salesPersonFk: Comercial
|
||||
attenderFk: Asistente
|
||||
claimResponsibleFk: Responsable
|
||||
claimStateFk: Estado
|
||||
created: Creada
|
||||
Customer ID: ID cliente
|
||||
Client Name: Nombre del cliente
|
||||
Salesperson: Comercial
|
||||
Attender: Asistente
|
||||
Responsible: Responsable
|
||||
State: Estado
|
||||
Item: Artículo
|
||||
Created: Creada
|
||||
More options: Más opciones
|
||||
</i18n>
|
|
@ -1,32 +1,24 @@
|
|||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useQuasar } from 'quasar';
|
||||
import Paginate from 'src/components/PaginateData.vue';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import { toDate } from 'src/filters/index';
|
||||
import Paginate from 'src/components/PaginateData.vue';
|
||||
import ClaimSummaryDialog from './Card/ClaimSummaryDialog.vue';
|
||||
import CustomerDescriptorPopover from 'src/pages/Customer/Card/CustomerDescriptorPopover.vue';
|
||||
import TeleportSlot from 'components/ui/TeleportSlot.vue';
|
||||
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
|
||||
import ClaimFilter from './ClaimFilter.vue';
|
||||
|
||||
const stateStore = useStateStore();
|
||||
const router = useRouter();
|
||||
const quasar = useQuasar();
|
||||
const { t } = useI18n();
|
||||
|
||||
const filter = {
|
||||
include: [
|
||||
{
|
||||
relation: 'client',
|
||||
},
|
||||
{
|
||||
relation: 'claimState',
|
||||
},
|
||||
{
|
||||
relation: 'worker',
|
||||
scope: {
|
||||
include: { relation: 'user' },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
onMounted(() => (stateStore.rightDrawer = true));
|
||||
onUnmounted(() => (stateStore.rightDrawer = false));
|
||||
|
||||
function stateColor(code) {
|
||||
if (code === 'pending') return 'green';
|
||||
|
@ -49,41 +41,66 @@ function viewSummary(id) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<teleport-slot to="#searchbar">
|
||||
<VnSearchbar
|
||||
data-key="ClaimList"
|
||||
:label="t('Search claim')"
|
||||
:info="t('You can search by claim id or customer name')"
|
||||
/>
|
||||
</teleport-slot>
|
||||
<teleport-slot to="#rightPanel">
|
||||
<ClaimFilter data-key="ClaimList" />
|
||||
</teleport-slot>
|
||||
<q-page class="q-pa-md">
|
||||
<paginate url="/Claims" :filter="filter" sort-by="id DESC" auto-load>
|
||||
<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-item class="q-pa-none items-start cursor-pointer q-hoverable" v-ripple clickable>
|
||||
<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 link">
|
||||
{{ row.client.name }}
|
||||
<q-popup-proxy>
|
||||
<customer-descriptor-popover :customer="row.client" />
|
||||
</q-popup-proxy>
|
||||
{{ row.clientName }}
|
||||
</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('claim.list.customer') }}</q-item-label>
|
||||
<q-item-label>{{ row.client.name }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ t('claim.list.customer') }}
|
||||
</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.worker.user.name }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ t('claim.list.assignedTo') }}
|
||||
</q-item-label>
|
||||
<q-item-label>{{ row.workerName }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.list.created') }}</q-item-label>
|
||||
<q-item-label>{{ toDate(row.created) }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ t('claim.list.created') }}
|
||||
</q-item-label>
|
||||
<q-item-label>
|
||||
{{ toDate(row.created) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.list.state') }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ t('claim.list.state') }}
|
||||
</q-item-label>
|
||||
<q-item-label>
|
||||
<q-chip :color="stateColor(row.claimState.code)" dense>
|
||||
{{ row.claimState.description }}
|
||||
</q-chip>
|
||||
<q-badge
|
||||
:color="stateColor(row.stateCode)"
|
||||
class="q-ma-none"
|
||||
dense
|
||||
>
|
||||
{{ row.stateDescription }}
|
||||
</q-badge>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
@ -111,16 +128,34 @@ function viewSummary(id) {
|
|||
</q-menu>
|
||||
</q-btn> -->
|
||||
|
||||
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
|
||||
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
|
||||
<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
|
||||
flat
|
||||
round
|
||||
color="grey-7"
|
||||
icon="preview"
|
||||
@click="viewSummary(row.id)"
|
||||
>
|
||||
<q-tooltip>
|
||||
{{ t('components.smartCard.openSummary') }}
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn flat round color="grey-7" icon="vn:client">
|
||||
<q-tooltip>{{ t('components.smartCard.viewDescription') }}</q-tooltip>
|
||||
<q-tooltip>
|
||||
{{ t('components.smartCard.viewDescription') }}
|
||||
</q-tooltip>
|
||||
<q-popup-proxy>
|
||||
<customer-descriptor-popover :customer="row.client" />
|
||||
<CustomerDescriptorPopover :id="row.clientFk" />
|
||||
</q-popup-proxy>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
|
@ -130,3 +165,9 @@ function viewSummary(id) {
|
|||
</paginate>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
es:
|
||||
Search claim: Buscar reclamación
|
||||
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
|
||||
</i18n>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script setup>
|
||||
import { useState } from 'src/composables/useState';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
|
||||
const state = useState();
|
||||
const stateStore = useStateStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
|
||||
<q-scroll-area class="fit text-grey-8">
|
||||
<LeftMenu />
|
||||
</q-scroll-area>
|
||||
|
|
|
@ -4,16 +4,15 @@ import { useI18n } from 'vue-i18n';
|
|||
import { useQuasar } from 'quasar';
|
||||
import axios from 'axios';
|
||||
import Paginate from 'src/components/PaginateData.vue';
|
||||
import { useArrayData } from 'src/composables/useArrayData';
|
||||
import VnConfirm from 'src/components/ui/VnConfirm.vue';
|
||||
|
||||
const quasar = useQuasar();
|
||||
const { t } = useI18n();
|
||||
|
||||
const rmas = ref([]);
|
||||
const card = ref(null);
|
||||
|
||||
function onFetch(data) {
|
||||
rmas.value = data.value;
|
||||
}
|
||||
const arrayData = useArrayData('ClaimRmaList');
|
||||
const isLoading = ref(false);
|
||||
const input = ref();
|
||||
|
||||
const newRma = ref({
|
||||
code: '',
|
||||
|
@ -24,46 +23,38 @@ function onInputUpdate(value) {
|
|||
newRma.value.code = value.toUpperCase();
|
||||
}
|
||||
|
||||
function submit() {
|
||||
async function submit() {
|
||||
const formData = newRma.value;
|
||||
if (formData.code === '') return;
|
||||
|
||||
axios
|
||||
.post('ClaimRmas', formData)
|
||||
.then(() => {
|
||||
newRma.value = {
|
||||
code: '',
|
||||
crated: new Date(),
|
||||
};
|
||||
})
|
||||
.then(() => card.value.refresh());
|
||||
isLoading.value = true;
|
||||
await axios.post('ClaimRmas', formData);
|
||||
await arrayData.refresh();
|
||||
isLoading.value = false;
|
||||
input.value.$el.focus();
|
||||
|
||||
newRma.value = {
|
||||
code: '',
|
||||
created: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
const confirmShown = ref(false);
|
||||
const rmaId = ref(null);
|
||||
function confirm(id) {
|
||||
confirmShown.value = true;
|
||||
rmaId.value = id;
|
||||
}
|
||||
|
||||
function remove() {
|
||||
const id = rmaId.value;
|
||||
axios
|
||||
.delete(`ClaimRmas/${id}`)
|
||||
.then(() => {
|
||||
confirmShown.value = false;
|
||||
|
||||
quasar.notify({
|
||||
type: 'positive',
|
||||
message: 'Entry deleted',
|
||||
icon: 'check',
|
||||
});
|
||||
quasar
|
||||
.dialog({
|
||||
component: VnConfirm,
|
||||
})
|
||||
.then(() => card.value.refresh());
|
||||
.onOk(() => remove(id));
|
||||
}
|
||||
|
||||
function hide() {
|
||||
rmaId.value = null;
|
||||
async function remove(id) {
|
||||
await axios.delete(`ClaimRmas/${id}`);
|
||||
await arrayData.refresh();
|
||||
quasar.notify({
|
||||
type: 'positive',
|
||||
message: t('globals.rowRemoved'),
|
||||
icon: 'check',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -73,34 +64,74 @@ function hide() {
|
|||
<q-card class="card q-pa-md">
|
||||
<q-form @submit="submit">
|
||||
<q-input
|
||||
ref="input"
|
||||
v-model="newRma.code"
|
||||
:label="t('claim.rmaList.code')"
|
||||
@update:model-value="onInputUpdate"
|
||||
class="q-mb-md"
|
||||
:readonly="isLoading"
|
||||
:loading="isLoading"
|
||||
autofocus
|
||||
/>
|
||||
<div class="text-caption">{{ rmas.length }} {{ t('claim.rmaList.records') }}</div>
|
||||
<div class="text-caption">
|
||||
{{ arrayData.totalRows }} {{ t('claim.rmaList.records') }}
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-page-sticky>
|
||||
|
||||
<paginate ref="card" url="/ClaimRmas" @on-fetch="onFetch" sort-by="id DESC" auto-load>
|
||||
<paginate
|
||||
data-key="ClaimRmaList"
|
||||
url="ClaimRmas"
|
||||
order="id DESC"
|
||||
:offset="50"
|
||||
auto-load
|
||||
>
|
||||
<template #body="{ rows }">
|
||||
<q-card class="card">
|
||||
<template v-for="row of rows" :key="row.code">
|
||||
<template v-if="isLoading">
|
||||
<q-item class="q-pa-none items-start">
|
||||
<q-item-section class="q-pa-md">
|
||||
<q-list>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('claim.rmaList.code') }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
<q-skeleton />
|
||||
</q-item-label>
|
||||
<q-item-label
|
||||
><q-skeleton type="text"
|
||||
/></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-item-section>
|
||||
<q-card-actions vertical class="justify-between">
|
||||
<q-skeleton type="circle" class="q-mb-md" size="40px" />
|
||||
</q-card-actions>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
</template>
|
||||
<template v-for="row of rows" :key="row.id">
|
||||
<q-item class="q-pa-none items-start">
|
||||
<q-item-section class="q-pa-md">
|
||||
<q-list>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{
|
||||
t('claim.rmaList.code')
|
||||
}}</q-item-label>
|
||||
<q-item-label>{{ row.code }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-item-section>
|
||||
<q-card-actions vertical class="justify-between">
|
||||
<q-btn flat round color="primary" icon="vn:bin" @click="confirm(row.id)">
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
color="primary"
|
||||
icon="vn:bin"
|
||||
@click="confirm(row.id)"
|
||||
>
|
||||
<q-tooltip>{{ t('globals.remove') }}</q-tooltip>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
|
@ -111,20 +142,6 @@ function hide() {
|
|||
</template>
|
||||
</paginate>
|
||||
</q-page>
|
||||
|
||||
<q-dialog v-model="confirmShown" persistent @hide="hide">
|
||||
<q-card>
|
||||
<q-card-section class="row items-center">
|
||||
<q-avatar icon="warning" color="primary" text-color="white" />
|
||||
<span class="q-ml-sm">{{ t('globals.confirmRemove') }}</span>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat :label="t('globals.no')" color="primary" v-close-popup autofocus />
|
||||
<q-btn flat :label="t('globals.yes')" color="primary" @click="remove()" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -48,8 +48,16 @@ const filterOptions = {
|
|||
@on-fetch="setWorkers"
|
||||
auto-load
|
||||
/>
|
||||
<fetch-data url="ContactChannels" @on-fetch="(data) => contactChannels = data" auto-load />
|
||||
<fetch-data url="BusinessTypes" @on-fetch="(data) => businessTypes = data" auto-load />
|
||||
<fetch-data
|
||||
url="ContactChannels"
|
||||
@on-fetch="(data) => (contactChannels = data)"
|
||||
auto-load
|
||||
/>
|
||||
<fetch-data
|
||||
url="BusinessTypes"
|
||||
@on-fetch="(data) => (businessTypes = data)"
|
||||
auto-load
|
||||
/>
|
||||
<div class="container">
|
||||
<q-card>
|
||||
<form-model :url="`Clients/${route.params.id}`" model="customer">
|
||||
|
@ -125,11 +133,14 @@ const filterOptions = {
|
|||
:label="t('customer.basicData.salesPerson')"
|
||||
map-options
|
||||
use-input
|
||||
@filter="(value, update) => filter(value, update, filterOptions)"
|
||||
@filter="
|
||||
(value, update) =>
|
||||
filter(value, update, filterOptions)
|
||||
"
|
||||
:rules="validate('client.salesPersonFk')"
|
||||
:input-debounce="0"
|
||||
>
|
||||
<template #before>
|
||||
<template #prepend>
|
||||
<q-avatar color="orange">
|
||||
<q-img
|
||||
v-if="data.salesPersonFk"
|
||||
|
|
|
@ -1,14 +1,25 @@
|
|||
<script setup>
|
||||
import { useState } from 'src/composables/useState';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import CustomerDescriptor from './CustomerDescriptor.vue';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
import TeleportSlot from 'components/ui/TeleportSlot.vue';
|
||||
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
|
||||
|
||||
const state = useState();
|
||||
const stateStore = useStateStore();
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<teleport-slot to="#searchbar">
|
||||
<VnSearchbar
|
||||
data-key="CustomerList"
|
||||
:label="t('Search customer')"
|
||||
:info="t('You can search by customer id or name')"
|
||||
/>
|
||||
</teleport-slot>
|
||||
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
|
||||
<q-scroll-area class="fit">
|
||||
<customer-descriptor />
|
||||
<CustomerDescriptor />
|
||||
<q-separator />
|
||||
<left-menu source="card" />
|
||||
</q-scroll-area>
|
||||
|
@ -19,3 +30,9 @@ const state = useState();
|
|||
</q-page>
|
||||
</q-page-container>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
es:
|
||||
Search customer: Buscar cliente
|
||||
You can search by customer id or name: Puedes buscar por id o nombre del cliente
|
||||
</i18n>
|
||||
|
|
|
@ -24,50 +24,46 @@ const entityId = computed(() => {
|
|||
<template>
|
||||
<card-descriptor module="Customer" :url="`Clients/${entityId}/getCard`">
|
||||
<template #body="{ entity }">
|
||||
<q-list>
|
||||
<q-item v-if="entity.salesPersonUser">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{
|
||||
t('customer.card.salesPerson')
|
||||
}}</q-item-label>
|
||||
<q-item-label>
|
||||
{{ entity.salesPersonUser.name }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-list dense>
|
||||
<q-item v-if="entity.salesPersonUser" class="row">
|
||||
<q-item-label class="col" caption>
|
||||
{{ t('customer.card.salesPerson') }}
|
||||
</q-item-label>
|
||||
<q-item-label class="col q-ma-none">
|
||||
{{ entity.salesPersonUser.name }}
|
||||
</q-item-label>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>
|
||||
{{ t('customer.card.credit') }}
|
||||
</q-item-label>
|
||||
<q-item-label>
|
||||
{{ toCurrency(entity.credit) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{
|
||||
t('customer.card.securedCredit')
|
||||
}}</q-item-label>
|
||||
<q-item-label>
|
||||
{{ toCurrency(entity.creditInsurance) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item class="row">
|
||||
<q-item-label class="col" caption>
|
||||
{{ t('customer.card.credit') }}
|
||||
</q-item-label>
|
||||
<q-item-label class="col q-ma-none">
|
||||
{{ toCurrency(entity.credit) }}
|
||||
</q-item-label>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section v-if="entity.payMethod">
|
||||
<q-item-label caption>{{
|
||||
t('customer.card.payMethod')
|
||||
}}</q-item-label>
|
||||
<q-item-label>
|
||||
{{ entity.payMethod.name }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.card.debt') }}</q-item-label>
|
||||
<q-item-label>
|
||||
{{ toCurrency(entity.debt) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item class="row">
|
||||
<q-item-label class="col" caption>
|
||||
{{ t('customer.card.securedCredit') }}
|
||||
</q-item-label>
|
||||
<q-item-label class="col q-ma-none">
|
||||
{{ toCurrency(entity.creditInsurance) }}
|
||||
</q-item-label>
|
||||
</q-item>
|
||||
<q-item v-if="entity.payMethod" class="row">
|
||||
<q-item-label class="col" caption>
|
||||
{{ t('customer.card.payMethod') }}
|
||||
</q-item-label>
|
||||
<q-item-label class="col q-ma-none">
|
||||
{{ entity.payMethod.name }}
|
||||
</q-item-label>
|
||||
</q-item>
|
||||
<q-item class="row">
|
||||
<q-item-label class="col" caption>
|
||||
{{ t('customer.card.debt') }}
|
||||
</q-item-label>
|
||||
<q-item-label class="col q-ma-none">
|
||||
{{ toCurrency(entity.debt) }}
|
||||
</q-item-label>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-card-actions class="q-gutter-md">
|
||||
|
@ -112,27 +108,55 @@ const entityId = computed(() => {
|
|||
<q-tooltip>{{ t('customer.card.noWebAccess') }}</q-tooltip>
|
||||
</q-icon>
|
||||
</q-card-actions>
|
||||
<!-- <q-card-actions>
|
||||
<q-btn size="md" icon="vn:ticket" color="primary">
|
||||
<q-tooltip>Ticket list</q-tooltip>
|
||||
</q-btn>
|
||||
<q-card-actions>
|
||||
<q-btn
|
||||
:to="{
|
||||
name: 'TicketList',
|
||||
query: { params: JSON.stringify({ clientFk: entity.id }) },
|
||||
}"
|
||||
size="md"
|
||||
icon="vn:ticket"
|
||||
color="primary"
|
||||
>
|
||||
<q-tooltip>{{ t('ticketList') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
:to="{
|
||||
name: 'InvoiceOutList',
|
||||
query: { params: JSON.stringify({ clientFk: entity.id }) },
|
||||
}"
|
||||
size="md"
|
||||
icon="vn:invoice-out"
|
||||
color="primary"
|
||||
>
|
||||
<q-tooltip>{{ t('invoiceOutList') }}</q-tooltip>
|
||||
</q-btn>
|
||||
<!--
|
||||
<q-btn size="md" icon="vn:basketadd" color="primary">
|
||||
<q-tooltip>Order list</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn size="md" icon="vn:invoice-out" color="primary">
|
||||
<q-tooltip>Invoice Out list</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn size="md" icon="face" color="primary">
|
||||
<q-tooltip>View user</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn size="md" icon="vn:basketadd" color="primary">
|
||||
<q-tooltip>Order list</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn size="md" icon="face" color="primary">
|
||||
<q-tooltip>View user</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn size="md" icon="expand_more" color="primary">
|
||||
<q-tooltip>More options</q-tooltip>
|
||||
</q-btn>
|
||||
</q-card-actions> -->
|
||||
<q-btn size="md" icon="expand_more" color="primary">
|
||||
<q-tooltip>More options</q-tooltip>
|
||||
</q-btn> -->
|
||||
</q-card-actions>
|
||||
</template>
|
||||
</card-descriptor>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
{
|
||||
"en": {
|
||||
"ticketList": "Customer ticket list",
|
||||
"invoiceOutList": "Customer invoice out list"
|
||||
},
|
||||
"es": {
|
||||
"ticketList": "Listado de tickets del cliente",
|
||||
"invoiceOutList": "Listado de facturas del cliente"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
|
|
|
@ -55,7 +55,7 @@ const creditWarning = computed(() => {
|
|||
<template #body="{ entity }">
|
||||
<q-card-section class="row q-pa-none q-col-gutter-md">
|
||||
<div class="col">
|
||||
<q-list>
|
||||
<q-list dense>
|
||||
<q-item-label header class="text-h6">
|
||||
{{ t('customer.summary.basicData') }}
|
||||
<router-link
|
||||
|
@ -68,57 +68,69 @@ const creditWarning = computed(() => {
|
|||
<q-icon name="open_in_new" />
|
||||
</router-link>
|
||||
</q-item-label>
|
||||
<q-separator class="q-mb-md" />
|
||||
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>
|
||||
{{ t('customer.summary.customerId') }}
|
||||
</q-item-label>
|
||||
<q-item-label>{{ entity.id }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item class="row col">
|
||||
<q-item-label class="col" caption>
|
||||
{{ t('customer.summary.customerId') }}
|
||||
</q-item-label>
|
||||
<q-item-label class="col q-ma-none">
|
||||
{{ entity.id }}
|
||||
</q-item-label>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>
|
||||
{{ t('customer.summary.name') }}
|
||||
</q-item-label>
|
||||
<q-item-label>{{ entity.name }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item class="row col">
|
||||
<q-item-label class="col" caption>
|
||||
{{ t('customer.summary.name') }}
|
||||
</q-item-label>
|
||||
<q-item-label class="col q-ma-none">{{
|
||||
entity.name
|
||||
}}</q-item-label>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>
|
||||
{{ t('customer.summary.contact') }}
|
||||
</q-item-label>
|
||||
<q-item-label>{{ entity.contact }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item class="row col">
|
||||
<q-item-label class="col" caption>
|
||||
{{ t('customer.summary.contact') }}
|
||||
</q-item-label>
|
||||
<q-item-label class="col q-ma-none">{{
|
||||
entity.contact
|
||||
}}</q-item-label>
|
||||
</q-item>
|
||||
<q-item v-if="entity.salesPersonUser">
|
||||
<q-item-section>
|
||||
<q-item-label caption>
|
||||
{{ t('customer.summary.salesPerson') }}
|
||||
</q-item-label>
|
||||
<q-item-label>{{
|
||||
entity.salesPersonUser.name
|
||||
}}</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item v-if="entity.salesPersonUser" class="row col">
|
||||
<q-item-label class="col" caption>
|
||||
{{ t('customer.summary.salesPerson') }}
|
||||
</q-item-label>
|
||||
<q-item-label class="col q-ma-none">
|
||||
{{ entity.salesPersonUser.name }}
|
||||
</q-item-label>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>
|
||||
{{ t('customer.summary.phone') }}
|
||||
</q-item-label>
|
||||
<q-item-label>{{ entity.phone }}</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item class="row col">
|
||||
<q-item-label class="col" caption>
|
||||
{{ t('customer.summary.phone') }}
|
||||
</q-item-label>
|
||||
<q-item-label class="col q-ma-none">{{
|
||||
entity.phone
|
||||
}}</q-item-label>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>
|
||||
{{ t('customer.summary.mobile') }}
|
||||
</q-item-label>
|
||||
<q-item-label>{{ entity.mobile }}</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item class="row col">
|
||||
<q-item-label class="col" caption>
|
||||
{{ t('customer.summary.mobile') }}
|
||||
</q-item-label>
|
||||
<q-item-label class="col q-ma-none">{{
|
||||
entity.mobile
|
||||
}}</q-item-label>
|
||||
</q-item>
|
||||
|
||||
<q-item v-if="entity.contactChannel" class="row col">
|
||||
<q-item-label class="col" caption>
|
||||
{{ t('customer.summary.contactChannel') }}
|
||||
</q-item-label>
|
||||
<q-item-label class="col q-ma-none">
|
||||
{{ entity.contactChannel.name }}
|
||||
</q-item-label>
|
||||
</q-item>
|
||||
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-item-label caption>
|
||||
|
@ -127,16 +139,6 @@ const creditWarning = computed(() => {
|
|||
<q-item-label>{{ entity.email }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="entity.contactChannel">
|
||||
<q-item-section>
|
||||
<q-item-label caption>
|
||||
{{ t('customer.summary.contactChannel') }}
|
||||
</q-item-label>
|
||||
<q-item-label>{{
|
||||
entity.contactChannel.name
|
||||
}}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
@ -522,3 +524,9 @@ const creditWarning = computed(() => {
|
|||
</template>
|
||||
</card-summary>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.q-item__label + .q-item__label {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
<script setup>
|
||||
import { reactive, watch } from 'vue'
|
||||
import { reactive, watch } from 'vue';
|
||||
|
||||
const customer = reactive({
|
||||
name: '',
|
||||
});
|
||||
|
||||
watch(() => customer.name, () => {
|
||||
console.log('customer.name changed');
|
||||
});
|
||||
watch(
|
||||
() => customer.name,
|
||||
() => {
|
||||
console.log('customer.name changed');
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -20,7 +23,7 @@ watch(() => customer.name, () => {
|
|||
label="Your name *"
|
||||
hint="Name and surname"
|
||||
lazy-rules
|
||||
:rules="[val => val && val.length > 0 || 'Please type something']"
|
||||
:rules="[(val) => (val && val.length > 0) || 'Please type something']"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
|
@ -30,8 +33,8 @@ watch(() => customer.name, () => {
|
|||
label="Your age *"
|
||||
lazy-rules
|
||||
:rules="[
|
||||
val => val !== null && val !== '' || 'Please type your age',
|
||||
val => val > 0 && val < 100 || 'Please type a real age'
|
||||
(val) => (val !== null && val !== '') || 'Please type your age',
|
||||
(val) => (val > 0 && val < 100) || 'Please type a real age',
|
||||
]"
|
||||
/>
|
||||
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
<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 provinces = ref();
|
||||
const workers = ref();
|
||||
const zones = ref();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fetch-data url="Provinces" @on-fetch="(data) => (provinces = data)" auto-load />
|
||||
<fetch-data url="Zones" @on-fetch="(data) => (zones = data)" auto-load />
|
||||
<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-list dense>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input :label="t('FI')" v-model="params.fi" lazy-rules>
|
||||
<template #prepend>
|
||||
<q-icon name="badge" size="sm"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input :label="t('Name')" v-model="params.name" lazy-rules />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
:label="t('Social Name')"
|
||||
v-model="params.socialName"
|
||||
lazy-rules
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<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('Salesperson')"
|
||||
v-model="params.salesPersonFk"
|
||||
@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-item>
|
||||
<q-item-section v-if="!provinces">
|
||||
<q-skeleton type="QInput" class="full-width" />
|
||||
</q-item-section>
|
||||
<q-item-section v-if="provinces">
|
||||
<q-select
|
||||
:label="t('Province')"
|
||||
v-model="params.provinceFk"
|
||||
@update:model-value="searchFn()"
|
||||
:options="provinces"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item class="q-mb-md">
|
||||
<q-item-section>
|
||||
<q-input :label="t('City')" v-model="params.city" lazy-rules />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-expansion-item :label="t('More options')" expand-separator>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
:label="t('Phone')"
|
||||
v-model="params.phone"
|
||||
lazy-rules
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="phone" size="sm"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
:label="t('Email')"
|
||||
v-model="params.email"
|
||||
lazy-rules
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="email" size="sm"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section v-if="!zones">
|
||||
<q-skeleton type="QInput" class="full-width" />
|
||||
</q-item-section>
|
||||
<q-item-section v-if="zones">
|
||||
<q-select
|
||||
:label="t('Zone')"
|
||||
v-model="params.zoneFk"
|
||||
@update:model-value="searchFn()"
|
||||
:options="zones"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
:label="t('Postcode')"
|
||||
v-model="params.postcode"
|
||||
lazy-rules
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</template>
|
||||
</VnFilterPanel>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
en:
|
||||
params:
|
||||
search: Contains
|
||||
fi: FI
|
||||
name: Name
|
||||
socialName: Social Name
|
||||
salesPersonFk: Salesperson
|
||||
provinceFk: Province
|
||||
city: City
|
||||
phone: Phone
|
||||
email: Email
|
||||
zoneFk: Zone
|
||||
postcode: Postcode
|
||||
es:
|
||||
params:
|
||||
search: Contiene
|
||||
fi: NIF
|
||||
name: Nombre
|
||||
socialName: Razón Social
|
||||
salesPersonFk: Comercial
|
||||
provinceFk: Provincia
|
||||
city: Ciudad
|
||||
phone: Teléfono
|
||||
email: Email
|
||||
zoneFk: Zona
|
||||
postcode: CP
|
||||
FI: NIF
|
||||
Name: Nombre
|
||||
Social Name: Razón social
|
||||
Salesperson: Comercial
|
||||
Province: Provincia
|
||||
City: Ciudad
|
||||
More options: Más opciones
|
||||
Phone: Teléfono
|
||||
Email: Email
|
||||
Zone: Zona
|
||||
Postcode: Código postal
|
||||
</i18n>
|
|
@ -1,14 +1,23 @@
|
|||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import Paginate from 'src/components/PaginateData.vue';
|
||||
import CustomerSummaryDialog from './Card/CustomerSummaryDialog.vue';
|
||||
import TeleportSlot from 'components/ui/TeleportSlot.vue';
|
||||
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
|
||||
import CustomerFilter from './CustomerFilter.vue';
|
||||
|
||||
const stateStore = useStateStore();
|
||||
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}` });
|
||||
}
|
||||
|
@ -24,24 +33,43 @@ function viewSummary(id) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<teleport-slot to="#searchbar">
|
||||
<VnSearchbar
|
||||
data-key="CustomerList"
|
||||
:label="t('Search customer')"
|
||||
:info="t('You can search by customer id or name')"
|
||||
/>
|
||||
</teleport-slot>
|
||||
<teleport-slot to="#rightPanel">
|
||||
<CustomerFilter data-key="CustomerList" />
|
||||
</teleport-slot>
|
||||
<q-page class="q-pa-md">
|
||||
<paginate url="/Clients" sort-by="id DESC" auto-load>
|
||||
<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-item class="q-pa-none items-start cursor-pointer q-hoverable" v-ripple clickable>
|
||||
<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.name }}</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('customer.list.email') }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ t('customer.list.email') }}
|
||||
</q-item-label>
|
||||
<q-item-label>{{ row.email }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('customer.list.phone') }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ t('customer.list.phone') }}
|
||||
</q-item-label>
|
||||
<q-item-label>{{ row.phone }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
@ -69,11 +97,27 @@ function viewSummary(id) {
|
|||
</q-menu>
|
||||
</q-btn> -->
|
||||
|
||||
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
|
||||
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
|
||||
<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
|
||||
flat
|
||||
round
|
||||
color="grey-7"
|
||||
icon="preview"
|
||||
@click="viewSummary(row.id)"
|
||||
>
|
||||
<q-tooltip>
|
||||
{{ t('components.smartCard.openSummary') }}
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<!-- <q-btn flat round color="grey-7" icon="vn:ticket">
|
||||
<q-tooltip>{{ t('customer.list.customerOrders') }}</q-tooltip>
|
||||
|
@ -85,3 +129,9 @@ function viewSummary(id) {
|
|||
</paginate>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
es:
|
||||
Search customer: Buscar cliente
|
||||
You can search by customer id or name: Puedes buscar por id o nombre del cliente
|
||||
</i18n>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script setup>
|
||||
import { useState } from 'src/composables/useState';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
|
||||
const state = useState();
|
||||
const stateStore = useStateStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
|
||||
<q-scroll-area class="fit text-grey-8">
|
||||
<LeftMenu />
|
||||
</q-scroll-area>
|
||||
|
@ -15,3 +15,10 @@ const state = useState();
|
|||
<router-view></router-view>
|
||||
</q-page-container>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#searchbar,
|
||||
.search-panel {
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
<script setup>
|
||||
import { onMounted, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useState } from 'src/composables/useState';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import { useNavigationStore } from 'src/stores/useNavigationStore';
|
||||
|
||||
const state = useState();
|
||||
const stateStore = useStateStore();
|
||||
const navigation = useNavigationStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
onMounted(() => {
|
||||
navigation.fetchPinned();
|
||||
});
|
||||
onMounted(() => navigation.fetchPinned());
|
||||
|
||||
const pinnedModules = computed(() => navigation.getPinnedModules());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<q-drawer
|
||||
v-model="stateStore.leftDrawer"
|
||||
show-if-above
|
||||
:width="256"
|
||||
:breakpoint="1000"
|
||||
>
|
||||
<q-scroll-area class="fit text-grey-8">
|
||||
<LeftMenu />
|
||||
</q-scroll-area>
|
||||
|
@ -26,13 +29,19 @@ const pinnedModules = computed(() => navigation.getPinnedModules());
|
|||
<q-page class="q-pa-md">
|
||||
<div class="row items-start wrap q-col-gutter-md q-mb-lg">
|
||||
<div class="col-12 col-md">
|
||||
<div class="text-h6 text-grey-8 q-mb-sm">{{ t('globals.pinnedModules') }}</div>
|
||||
<div class="text-h6 text-grey-8 q-mb-sm">
|
||||
{{ t('globals.pinnedModules') }}
|
||||
</div>
|
||||
<q-card class="row flex-container q-pa-md">
|
||||
<div class="text-grey-5" v-if="pinnedModules.length === 0">
|
||||
{{ t('pinnedInfo') }}
|
||||
</div>
|
||||
<template v-if="pinnedModules.length">
|
||||
<div v-for="item of pinnedModules" :key="item.title" class="row no-wrap q-pa-xs flex-item">
|
||||
<div
|
||||
v-for="item of pinnedModules"
|
||||
:key="item.title"
|
||||
class="row no-wrap q-pa-xs flex-item"
|
||||
>
|
||||
<q-btn
|
||||
align="evenly"
|
||||
padding="16px"
|
||||
|
|
|
@ -1,13 +1,27 @@
|
|||
<script setup>
|
||||
import { useState } from 'src/composables/useState';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import InvoiceOutDescriptor from './InvoiceOutDescriptor.vue';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
import TeleportSlot from 'components/ui/TeleportSlot.vue';
|
||||
import VnSearchbar from 'components/ui/VnSearchbar.vue';
|
||||
|
||||
const state = useState();
|
||||
const stateStore = useStateStore();
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<teleport-slot to="#searchbar">
|
||||
<VnSearchbar
|
||||
data-key="InvoiceOutList"
|
||||
:label="t('Search invoice')"
|
||||
:info="t('You can search by invoice reference')"
|
||||
/>
|
||||
</teleport-slot>
|
||||
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
|
||||
<q-scroll-area class="fit">
|
||||
<InvoiceOutDescriptor />
|
||||
<q-separator />
|
||||
<left-menu source="card" />
|
||||
</q-scroll-area>
|
||||
</q-drawer>
|
||||
<q-page-container>
|
||||
|
@ -16,3 +30,9 @@ const state = useState();
|
|||
</q-page>
|
||||
</q-page-container>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
es:
|
||||
Search invoice: Buscar factura emitida
|
||||
You can search by invoice reference: Puedes buscar por referencia de la factura
|
||||
</i18n>
|
||||
|
|
|
@ -0,0 +1,263 @@
|
|||
<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();
|
||||
const workersCopy = ref();
|
||||
const states = ref();
|
||||
|
||||
function setWorkers(data) {
|
||||
workers.value = data;
|
||||
workersCopy.value = data;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fetch-data url="ClaimStates" @on-fetch="(data) => (states = data)" auto-load />
|
||||
<fetch-data
|
||||
url="Workers/activeWithInheritedRole"
|
||||
:filter="{ where: { role: 'salesPerson' } }"
|
||||
@on-fetch="setWorkers"
|
||||
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-list dense>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
:label="t('Customer ID')"
|
||||
v-model="params.clientFk"
|
||||
lazy-rules
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="vn:client" size="sm"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input :label="t('FI')" v-model="params.fi" lazy-rules>
|
||||
<template #prepend>
|
||||
<q-icon name="badge" size="sm"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input :label="t('Amount')" v-model="params.amount" lazy-rules>
|
||||
<template #prepend>
|
||||
<q-icon name="euro" size="sm"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
:label="t('Min')"
|
||||
type="number"
|
||||
v-model.number="params.min"
|
||||
lazy-rules
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="euro" size="sm"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
:label="t('Max')"
|
||||
type="number"
|
||||
v-model.number="params.max"
|
||||
lazy-rules
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="euro" size="sm"></q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item class="q-mb-md">
|
||||
<q-item-section>
|
||||
<q-checkbox
|
||||
v-model="params.hasPdf"
|
||||
@update:model-value="searchFn()"
|
||||
:label="t('Has PDF')"
|
||||
toggle-indeterminate
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-expansion-item :label="t('More options')" expand-separator>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
:label="t('Issued')"
|
||||
v-model="params.issued"
|
||||
mask="date"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy
|
||||
cover
|
||||
transition-show="scale"
|
||||
transition-hide="scale"
|
||||
>
|
||||
<q-date v-model="params.issued" landscape>
|
||||
<div
|
||||
class="row items-center justify-end q-gutter-sm"
|
||||
>
|
||||
<q-btn
|
||||
:label="t('globals.cancel')"
|
||||
color="primary"
|
||||
flat
|
||||
v-close-popup
|
||||
/>
|
||||
<q-btn
|
||||
:label="t('globals.confirm')"
|
||||
color="primary"
|
||||
flat
|
||||
@click="save"
|
||||
v-close-popup
|
||||
/>
|
||||
</div>
|
||||
</q-date>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
:label="t('Created')"
|
||||
v-model="params.created"
|
||||
mask="date"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy
|
||||
cover
|
||||
transition-show="scale"
|
||||
transition-hide="scale"
|
||||
>
|
||||
<q-date v-model="params.created" landscape>
|
||||
<div
|
||||
class="row items-center justify-end q-gutter-sm"
|
||||
>
|
||||
<q-btn
|
||||
:label="t('globals.cancel')"
|
||||
color="primary"
|
||||
flat
|
||||
v-close-popup
|
||||
/>
|
||||
<q-btn
|
||||
:label="t('globals.confirm')"
|
||||
color="primary"
|
||||
flat
|
||||
@click="save"
|
||||
v-close-popup
|
||||
/>
|
||||
</div>
|
||||
</q-date>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input :label="t('Dued')" v-model="params.dued" mask="date">
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy
|
||||
cover
|
||||
transition-show="scale"
|
||||
transition-hide="scale"
|
||||
>
|
||||
<q-date v-model="params.dued" landscape>
|
||||
<div
|
||||
class="row items-center justify-end q-gutter-sm"
|
||||
>
|
||||
<q-btn
|
||||
:label="t('globals.cancel')"
|
||||
color="primary"
|
||||
flat
|
||||
v-close-popup
|
||||
/>
|
||||
<q-btn
|
||||
:label="t('globals.confirm')"
|
||||
color="primary"
|
||||
flat
|
||||
@click="save"
|
||||
v-close-popup
|
||||
/>
|
||||
</div>
|
||||
</q-date>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</template>
|
||||
</VnFilterPanel>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
en:
|
||||
params:
|
||||
search: Contains
|
||||
clientFk: Customer
|
||||
fi: FI
|
||||
amount: Amount
|
||||
min: Min
|
||||
max: Max
|
||||
hasPdf: Has PDF
|
||||
issued: Issued
|
||||
created: Created
|
||||
dued: Dued
|
||||
es:
|
||||
params:
|
||||
search: Contiene
|
||||
clientFk: Cliente
|
||||
fi: CIF
|
||||
amount: Importe
|
||||
min: Min
|
||||
max: Max
|
||||
hasPdf: Tiene PDF
|
||||
issued: Emitida
|
||||
created: Creada
|
||||
dued: Vencida
|
||||
Customer ID: ID cliente
|
||||
FI: CIF
|
||||
Amount: Importe
|
||||
Has PDF: Tiene PDF
|
||||
Issued: Fecha emisión
|
||||
Created: Fecha creación
|
||||
Dued: Fecha vencimiento
|
||||
More options: Más opciones
|
||||
</i18n>
|
|
@ -1,15 +1,24 @@
|
|||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import Paginate from 'src/components/PaginateData.vue';
|
||||
import InvoiceOutSummaryDialog from './Card/InvoiceOutSummaryDialog.vue';
|
||||
import { toDate, toCurrency } from 'src/filters/index';
|
||||
import TeleportSlot from 'components/ui/TeleportSlot.vue';
|
||||
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
|
||||
import InvoiceOutFilter from './InvoiceOutFilter.vue';
|
||||
|
||||
const stateStore = useStateStore();
|
||||
const router = useRouter();
|
||||
const quasar = useQuasar();
|
||||
const { t } = useI18n();
|
||||
|
||||
onMounted(() => (stateStore.rightDrawer = true));
|
||||
onUnmounted(() => (stateStore.rightDrawer = false));
|
||||
|
||||
function navigate(id) {
|
||||
router.push({ path: `/invoiceOut/${id}` });
|
||||
}
|
||||
|
@ -25,54 +34,111 @@ function viewSummary(id) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<teleport-slot to="#searchbar">
|
||||
<VnSearchbar
|
||||
data-key="InvoiceOutList"
|
||||
:label="t('Search invoice')"
|
||||
:info="t('You can search by invoice reference')"
|
||||
/>
|
||||
</teleport-slot>
|
||||
<teleport-slot to="#rightPanel">
|
||||
<InvoiceOutFilter data-key="InvoiceOutList" />
|
||||
</teleport-slot>
|
||||
<q-page class="q-pa-md">
|
||||
<paginate url="/InvoiceOuts/filter" sort-by="issued DESC, id DESC" auto-load>
|
||||
<paginate
|
||||
data-key="InvoiceOutList"
|
||||
url="InvoiceOuts/filter"
|
||||
order="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
|
||||
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-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-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-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-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 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-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
|
||||
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
|
||||
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>
|
||||
|
@ -81,3 +147,9 @@ function viewSummary(id) {
|
|||
</paginate>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
es:
|
||||
Search invoice: Buscar factura emitida
|
||||
You can search by invoice reference: Puedes buscar por referencia de la factura
|
||||
</i18n>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script setup>
|
||||
import { useState } from 'src/composables/useState';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import LeftMenu from 'src/components/LeftMenu.vue';
|
||||
|
||||
const state = useState();
|
||||
const stateStore = useStateStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
|
||||
<q-scroll-area class="fit text-grey-8">
|
||||
<LeftMenu />
|
||||
</q-scroll-area>
|
||||
|
|
|
@ -138,6 +138,7 @@ async function onSubmit() {
|
|||
color="primary"
|
||||
class="full-width"
|
||||
rounded
|
||||
unelevated
|
||||
/>
|
||||
</div>
|
||||
</q-form>
|
||||
|
|
|
@ -1,14 +1,25 @@
|
|||
<script setup>
|
||||
import { useState } from 'src/composables/useState';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import TicketDescriptor from './TicketDescriptor.vue';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
import TeleportSlot from 'components/ui/TeleportSlot.vue';
|
||||
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
|
||||
|
||||
const state = useState();
|
||||
const stateStore = useStateStore();
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<teleport-slot to="#searchbar">
|
||||
<VnSearchbar
|
||||
data-key="TicketList"
|
||||
:label="t('Search ticket')"
|
||||
:info="t('You can search by ticket id or alias')"
|
||||
/>
|
||||
</teleport-slot>
|
||||
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
|
||||
<q-scroll-area class="fit">
|
||||
<ticket-descriptor />
|
||||
<TicketDescriptor />
|
||||
<q-separator />
|
||||
<left-menu source="card" />
|
||||
</q-scroll-area>
|
||||
|
@ -48,3 +59,9 @@ const state = useState();
|
|||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<i18n>
|
||||
es:
|
||||
Search ticket: Buscar ticket
|
||||
You can search by ticket id or alias: Puedes buscar por id o alias del ticket
|
||||
</i18n>
|
||||
|
|
|
@ -0,0 +1,325 @@
|
|||
<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();
|
||||
const provinces = ref();
|
||||
const states = ref();
|
||||
const agencies = ref();
|
||||
const warehouses = ref();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fetch-data url="Provinces" @on-fetch="(data) => (provinces = data)" auto-load />
|
||||
<fetch-data url="States" @on-fetch="(data) => (states = data)" auto-load />
|
||||
<fetch-data url="AgencyModes" @on-fetch="(data) => (agencies = data)" auto-load />
|
||||
<fetch-data url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load />
|
||||
<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-list dense>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
v-model="params.clientFk"
|
||||
:label="t('Customer ID')"
|
||||
lazy-rules
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
v-model="params.orderFk"
|
||||
:label="t('Order ID')"
|
||||
lazy-rules
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input v-model="params.dateFrom" :label="t('From')" mask="date">
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy
|
||||
cover
|
||||
transition-show="scale"
|
||||
transition-hide="scale"
|
||||
>
|
||||
<q-date v-model="params.dateFrom" landscape>
|
||||
<div
|
||||
class="row items-center justify-end q-gutter-sm"
|
||||
>
|
||||
<q-btn
|
||||
:label="t('globals.cancel')"
|
||||
color="primary"
|
||||
flat
|
||||
v-close-popup
|
||||
/>
|
||||
<q-btn
|
||||
:label="t('globals.confirm')"
|
||||
color="primary"
|
||||
flat
|
||||
@click="save"
|
||||
v-close-popup
|
||||
/>
|
||||
</div>
|
||||
</q-date>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input v-model="params.dateTo" :label="t('To')" mask="date">
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy
|
||||
cover
|
||||
transition-show="scale"
|
||||
transition-hide="scale"
|
||||
>
|
||||
<q-date v-model="params.dateTo" landscape>
|
||||
<div
|
||||
class="row items-center justify-end q-gutter-sm"
|
||||
>
|
||||
<q-btn
|
||||
:label="t('globals.cancel')"
|
||||
color="primary"
|
||||
flat
|
||||
v-close-popup
|
||||
/>
|
||||
<q-btn
|
||||
:label="t('globals.confirm')"
|
||||
color="primary"
|
||||
flat
|
||||
@click="save"
|
||||
v-close-popup
|
||||
/>
|
||||
</div>
|
||||
</q-date>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<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('Salesperson')"
|
||||
v-model="params.salesPersonFk"
|
||||
:options="workers"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
emit-value
|
||||
map-options
|
||||
use-input
|
||||
:input-debounce="0"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section v-if="!states">
|
||||
<q-skeleton type="QInput" class="full-width" />
|
||||
</q-item-section>
|
||||
<q-item-section v-if="states">
|
||||
<q-select
|
||||
:label="t('State')"
|
||||
v-model="params.stateFk"
|
||||
@update:model-value="searchFn()"
|
||||
:options="states"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
v-model="params.refFk"
|
||||
:label="t('Invoice Ref.')"
|
||||
lazy-rules
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-checkbox
|
||||
v-model="params.myTeam"
|
||||
@update:model-value="searchFn()"
|
||||
:label="t('My team')"
|
||||
toggle-indeterminate
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-checkbox
|
||||
v-model="params.pending"
|
||||
@update:model-value="searchFn()"
|
||||
:label="t('Pending')"
|
||||
toggle-indeterminate
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-checkbox
|
||||
v-model="params.hasInvoice"
|
||||
@update:model-value="searchFn()"
|
||||
:label="t('Invoiced')"
|
||||
toggle-indeterminate
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-checkbox
|
||||
v-model="params.hasRoute"
|
||||
@update:model-value="searchFn()"
|
||||
:label="t('Routed')"
|
||||
toggle-indeterminate
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-checkbox
|
||||
v-model="params.problems"
|
||||
@update:model-value="searchFn()"
|
||||
:label="t('With problems')"
|
||||
toggle-indeterminate
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-expansion-item :label="t('More options')" expand-separator>
|
||||
<q-item>
|
||||
<q-item-section v-if="!provinces">
|
||||
<q-skeleton type="QInput" class="full-width" />
|
||||
</q-item-section>
|
||||
<q-item-section v-if="provinces">
|
||||
<q-select
|
||||
:label="t('Province')"
|
||||
v-model="params.provinceFk"
|
||||
@update:model-value="searchFn()"
|
||||
:options="provinces"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section v-if="!states">
|
||||
<q-skeleton type="QInput" class="full-width" />
|
||||
</q-item-section>
|
||||
<q-item-section v-if="!states">
|
||||
<q-select
|
||||
:label="t('Agency')"
|
||||
v-model="params.agencyModeFk"
|
||||
@update:model-value="searchFn()"
|
||||
:options="agencies"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section v-if="!warehouses">
|
||||
<q-skeleton type="QInput" class="full-width" />
|
||||
</q-item-section>
|
||||
<q-item-section v-if="warehouses">
|
||||
<q-select
|
||||
:label="t('Warehouse')"
|
||||
v-model="params.warehouseFk"
|
||||
@update:model-value="searchFn()"
|
||||
:options="warehouses"
|
||||
option-value="id"
|
||||
option-label="name"
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</template>
|
||||
</VnFilterPanel>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
en:
|
||||
params:
|
||||
search: Contains
|
||||
clientFk: Customer
|
||||
orderFK: Order
|
||||
dateFrom: From
|
||||
dateTo: To
|
||||
salesPersonFk: Salesperson
|
||||
stateFk: State
|
||||
refFk: Invoice Ref.
|
||||
myTeam: My team
|
||||
pending: Pending
|
||||
hasInvoice: Invoiced
|
||||
hasRoute: Routed
|
||||
provinceFk: Province
|
||||
agencyModeFk: Agency
|
||||
warehouseFk: Warehouse
|
||||
es:
|
||||
params:
|
||||
search: Contiene
|
||||
clientFk: Cliente
|
||||
orderFK: Pedido
|
||||
dateFrom: Desde
|
||||
dateTo: Hasta
|
||||
salesPersonFk: Comercial
|
||||
stateFk: Estado
|
||||
refFk: Ref. Factura
|
||||
myTeam: Mi equipo
|
||||
pending: Pendiente
|
||||
hasInvoice: Facturado
|
||||
hasRoute: Enrutado
|
||||
Customer ID: ID Cliente
|
||||
Order ID: ID Pedido
|
||||
From: Desde
|
||||
To: Hasta
|
||||
Salesperson: Comercial
|
||||
State: Estado
|
||||
Invoice Ref.: Ref. Factura
|
||||
My team: Mi equipo
|
||||
Pending: Pendiente
|
||||
With problems: Con problemas
|
||||
Invoiced: Facturado
|
||||
Routed: Enrutado
|
||||
More options: Más opciones
|
||||
Province: Provincia
|
||||
Agency: Agencia
|
||||
Warehouse: Almacén
|
||||
Yes: Si
|
||||
No: No
|
||||
</i18n>
|
|
@ -1,14 +1,24 @@
|
|||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import Paginate from 'src/components/PaginateData.vue';
|
||||
import { toDate, toCurrency } from 'src/filters/index';
|
||||
import TicketSummaryDialog from './Card/TicketSummaryDialog.vue';
|
||||
|
||||
import TeleportSlot from 'components/ui/TeleportSlot.vue';
|
||||
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
|
||||
import TicketFilter from './TicketFilter.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const quasar = useQuasar();
|
||||
const { t } = useI18n();
|
||||
const stateStore = useStateStore();
|
||||
|
||||
onMounted(() => (stateStore.rightDrawer = true));
|
||||
onUnmounted(() => (stateStore.rightDrawer = false));
|
||||
|
||||
const filter = {
|
||||
include: [
|
||||
|
@ -38,11 +48,12 @@ const filter = {
|
|||
],
|
||||
};
|
||||
|
||||
function stateColor(state) {
|
||||
if (state.code === 'OK') return 'green';
|
||||
if (state.code === 'FREE') return 'blue-3';
|
||||
if (state.alertLevel === 1) return 'orange';
|
||||
if (state.alertLevel === 0) return 'red';
|
||||
function stateColor(row) {
|
||||
if (row.alertLevelCode === 'OK') return 'green';
|
||||
if (row.alertLevelCode === 'FREE') return 'blue-3';
|
||||
if (row.alertLevel === 1) return 'orange';
|
||||
if (row.alertLevel === 0) return 'red';
|
||||
return 'red';
|
||||
}
|
||||
|
||||
function navigate(id) {
|
||||
|
@ -60,58 +71,118 @@ function viewSummary(id) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<teleport-slot to="#searchbar">
|
||||
<VnSearchbar
|
||||
data-key="TicketList"
|
||||
:label="t('Search ticket')"
|
||||
:info="t('You can search by ticket id or alias')"
|
||||
/>
|
||||
</teleport-slot>
|
||||
<teleport-slot to="#rightPanel">
|
||||
<TicketFilter data-key="TicketList" />
|
||||
</teleport-slot>
|
||||
<q-page class="q-pa-md">
|
||||
<paginate url="/Tickets" :filter="filter" sort-by="id DESC" auto-load>
|
||||
<paginate
|
||||
data-key="TicketList"
|
||||
url="Tickets/filter"
|
||||
:filter="filter"
|
||||
order="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
|
||||
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.name }}</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('ticket.list.nickname') }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ t('ticket.list.nickname') }}
|
||||
</q-item-label>
|
||||
<q-item-label>{{ row.nickname }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.list.state') }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ t('ticket.list.state') }}
|
||||
</q-item-label>
|
||||
<q-item-label>
|
||||
<q-chip :color="stateColor(row.ticketState)" dense>
|
||||
{{ row.ticketState.state.name }}
|
||||
</q-chip>
|
||||
<q-badge
|
||||
:color="stateColor(row)"
|
||||
class="q-ma-none"
|
||||
dense
|
||||
>
|
||||
{{ row.state }}
|
||||
</q-badge>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.list.shipped') }}</q-item-label>
|
||||
<q-item-label>{{ toDate(row.shipped) }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ t('ticket.list.shipped') }}
|
||||
</q-item-label>
|
||||
<q-item-label>
|
||||
{{ toDate(row.shipped) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.list.landed') }}</q-item-label>
|
||||
<q-item-label>{{ toDate(row.landed) }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ t('Zone') }}
|
||||
</q-item-label>
|
||||
<q-item-label>
|
||||
{{ row.zoneName }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item class="q-pa-none">
|
||||
<q-item-section v-if="row.client.salesPersonUser">
|
||||
<q-item-label caption>{{ t('ticket.list.salesPerson') }}</q-item-label>
|
||||
<q-item-label>{{ row.client.salesPersonUser.name }}</q-item-label>
|
||||
<q-item-section>
|
||||
<q-item-label caption>
|
||||
{{ t('ticket.list.salesPerson') }}
|
||||
</q-item-label>
|
||||
<q-item-label>
|
||||
{{ row.salesPerson }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ t('ticket.list.total') }}</q-item-label>
|
||||
<q-item-label>{{ toCurrency(row.totalWithVat) }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ t('ticket.list.total') }}
|
||||
</q-item-label>
|
||||
<q-item-label>
|
||||
{{ toCurrency(row.totalWithVat) }}
|
||||
</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
|
||||
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
|
||||
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>
|
||||
|
@ -120,3 +191,10 @@ function viewSummary(id) {
|
|||
</paginate>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<i18n>
|
||||
es:
|
||||
Search ticket: Buscar ticket
|
||||
You can search by ticket id or alias: Puedes buscar por id o alias del ticket
|
||||
Zone: Zona
|
||||
</i18n>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script setup>
|
||||
import { useState } from 'src/composables/useState';
|
||||
import { useStateStore } from 'stores/useStateStore';
|
||||
import LeftMenu from 'components/LeftMenu.vue';
|
||||
|
||||
const state = useState();
|
||||
const stateStore = useStateStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
|
||||
<q-drawer v-model="stateStore.leftDrawer" show-if-above :width="256">
|
||||
<q-scroll-area class="fit text-grey-8">
|
||||
<LeftMenu />
|
||||
</q-scroll-area>
|
||||
|
|
|
@ -27,7 +27,7 @@ export default {
|
|||
title: 'list',
|
||||
icon: 'view_list',
|
||||
},
|
||||
component: () => import('src/pages/Customer/CustomerList.vue'),
|
||||
component: () => import('src/pages/Customer/CustomerList.vue')
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useArrayDataStore = defineStore('arrayDataStore', () => {
|
||||
const state = ref({});
|
||||
|
||||
function get(key) {
|
||||
return state.value[key];
|
||||
}
|
||||
|
||||
function set(key) {
|
||||
state.value[key] = {
|
||||
filter: {},
|
||||
userFilter: {},
|
||||
userParams: {},
|
||||
url: '',
|
||||
limit: 10,
|
||||
skip: 0,
|
||||
order: '',
|
||||
data: ref(),
|
||||
};
|
||||
}
|
||||
|
||||
function clear(key) {
|
||||
delete state.value[key];
|
||||
}
|
||||
|
||||
return {
|
||||
get,
|
||||
set,
|
||||
clear,
|
||||
};
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
import { ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useStateStore = defineStore('stateStore', () => {
|
||||
const isMounted = ref(false);
|
||||
const leftDrawer = ref(false);
|
||||
const rightDrawer = ref(false);
|
||||
|
||||
function toggleLeftDrawer() {
|
||||
leftDrawer.value = !leftDrawer.value;
|
||||
}
|
||||
|
||||
function toggleRightDrawer() {
|
||||
rightDrawer.value = !rightDrawer.value;
|
||||
}
|
||||
|
||||
function setMounted() {
|
||||
isMounted.value = true;
|
||||
}
|
||||
|
||||
function isHeaderMounted() {
|
||||
return isMounted.value;
|
||||
}
|
||||
|
||||
function isLeftDrawerShown() {
|
||||
return leftDrawer.value;
|
||||
}
|
||||
|
||||
function isRightDrawerShown() {
|
||||
return rightDrawer.value;
|
||||
}
|
||||
|
||||
return {
|
||||
leftDrawer,
|
||||
rightDrawer,
|
||||
setMounted,
|
||||
isHeaderMounted,
|
||||
toggleLeftDrawer,
|
||||
toggleRightDrawer,
|
||||
isLeftDrawerShown,
|
||||
isRightDrawerShown,
|
||||
};
|
||||
});
|
|
@ -4,82 +4,66 @@ import Paginate from 'components/PaginateData.vue';
|
|||
|
||||
describe('Paginate', () => {
|
||||
const expectedUrl = '/api/customers';
|
||||
|
||||
let vm;
|
||||
beforeAll(() => {
|
||||
const options = {
|
||||
attrs: {
|
||||
url: expectedUrl,
|
||||
sortBy: 'id DESC',
|
||||
rowsPerPage: 3,
|
||||
dataKey: 'CustomerList',
|
||||
order: 'id DESC',
|
||||
limit: 3,
|
||||
},
|
||||
};
|
||||
vm = createWrapper(Paginate, options).vm;
|
||||
|
||||
vi.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: [
|
||||
{ id: 1, name: 'Tony Stark' },
|
||||
{ id: 2, name: 'Jessica Jones' },
|
||||
{ id: 3, name: 'Bruce Wayne' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.rows = [];
|
||||
vm.store.data = [];
|
||||
vm.store.skip = 0;
|
||||
vm.pagination.page = 1;
|
||||
vm.hasMoreData = true;
|
||||
});
|
||||
|
||||
describe('paginate()', () => {
|
||||
it('should call to the paginate() method and set the data on the rows property', async () => {
|
||||
const expectedOptions = {
|
||||
params: {
|
||||
filter: {
|
||||
order: 'id DESC',
|
||||
limit: 3,
|
||||
skip: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
vi.spyOn(vm.arrayData, 'loadMore');
|
||||
vm.store.data = [
|
||||
{ id: 1, name: 'Tony Stark' },
|
||||
{ id: 2, name: 'Jessica Jones' },
|
||||
{ id: 3, name: 'Bruce Wayne' },
|
||||
];
|
||||
|
||||
await vm.paginate();
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(vm.rows.length).toEqual(3);
|
||||
expect(vm.arrayData.loadMore).toHaveBeenCalledWith();
|
||||
expect(vm.store.data.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should call to the paginate() method and then call it again to paginate', async () => {
|
||||
const expectedOptions = {
|
||||
params: {
|
||||
filter: {
|
||||
order: 'id DESC',
|
||||
limit: 3,
|
||||
skip: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
vi.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: [
|
||||
{ id: 1, name: 'Tony Stark' },
|
||||
{ id: 2, name: 'Jessica Jones' },
|
||||
{ id: 3, name: 'Bruce Wayne' },
|
||||
],
|
||||
});
|
||||
vm.arrayData.hasMoreData.value = true;
|
||||
vm.store.data = [
|
||||
{ id: 1, name: 'Tony Stark' },
|
||||
{ id: 2, name: 'Jessica Jones' },
|
||||
{ id: 3, name: 'Bruce Wayne' },
|
||||
];
|
||||
|
||||
await vm.paginate();
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptions);
|
||||
expect(vm.rows.length).toEqual(3);
|
||||
|
||||
const expectedOptionsPaginated = {
|
||||
params: {
|
||||
filter: {
|
||||
order: 'id DESC',
|
||||
limit: 3,
|
||||
skip: 3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
vm.pagination.page = 2;
|
||||
expect(vm.store.skip).toEqual(3);
|
||||
expect(vm.store.data.length).toEqual(6);
|
||||
|
||||
await vm.paginate();
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptionsPaginated);
|
||||
expect(vm.rows.length).toEqual(6);
|
||||
expect(vm.store.skip).toEqual(6);
|
||||
expect(vm.store.data.length).toEqual(9);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -95,16 +79,15 @@ describe('Paginate', () => {
|
|||
});
|
||||
|
||||
it('should increment the pagination and then call to the done() callback', async () => {
|
||||
vm.rows = [
|
||||
{ id: 1, name: 'Tony Stark' },
|
||||
{ id: 2, name: 'Jessica Jones' },
|
||||
{ id: 3, name: 'Bruce Wayne' },
|
||||
];
|
||||
|
||||
expect(vm.pagination.page).toEqual(1);
|
||||
|
||||
const index = 1;
|
||||
const done = vi.fn();
|
||||
vm.store.data = [
|
||||
{ id: 1, name: 'Tony Stark' },
|
||||
{ id: 2, name: 'Jessica Jones' },
|
||||
{ id: 3, name: 'Bruce Wayne' },
|
||||
];
|
||||
|
||||
await vm.onLoad(index, done);
|
||||
|
||||
|
@ -120,7 +103,7 @@ describe('Paginate', () => {
|
|||
],
|
||||
});
|
||||
|
||||
vm.rows = [
|
||||
vm.store.data = [
|
||||
{ id: 1, name: 'Tony Stark' },
|
||||
{ id: 2, name: 'Jessica Jones' },
|
||||
{ id: 3, name: 'Bruce Wayne' },
|
||||
|
|
|
@ -13,7 +13,9 @@ installQuasar({
|
|||
},
|
||||
});
|
||||
|
||||
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false });
|
||||
const mockPush = vi.fn();
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
|
@ -27,6 +29,8 @@ vi.mock('vue-router', () => ({
|
|||
}),
|
||||
useRoute: () => ({
|
||||
matched: [],
|
||||
query: {},
|
||||
params: {},
|
||||
}),
|
||||
}));
|
||||
|
||||
|
@ -56,8 +60,6 @@ class FormDataMock {
|
|||
global.FormData = FormDataMock;
|
||||
|
||||
export function createWrapper(component, options) {
|
||||
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false });
|
||||
|
||||
const defaultOptions = {
|
||||
global: {
|
||||
plugins: [i18n, pinia],
|
||||
|
|
Loading…
Reference in New Issue