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

This commit is contained in:
Joan Sanchez 2022-11-02 12:34:49 +01:00
commit 0c9f35e493
26 changed files with 1279 additions and 1197 deletions

View File

@ -61,7 +61,7 @@ function responseError(error) {
router.push({ path: '/login' }); router.push({ path: '/login' });
} }
return Promise.resolve(error); return Promise.reject(error);
} }
axios.interceptors.response.use((response) => { axios.interceptors.response.use((response) => {

View File

@ -52,8 +52,7 @@ describe('App', () => {
} }
}; };
await vm.responseError(response); expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response));
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining( expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
{ {
type: 'negative', type: 'negative',
@ -73,8 +72,7 @@ describe('App', () => {
} }
}; };
await vm.responseError(response); expect(vm.responseError(response)).rejects.toEqual(expect.objectContaining(response));
expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining( expect(vm.quasar.notify).toHaveBeenCalledWith(expect.objectContaining(
{ {
type: 'negative', type: 'negative',

View File

@ -0,0 +1,60 @@
<script setup>
import { h, onMounted } from 'vue';
import axios from 'axios';
const $props = defineProps({
autoLoad: {
type: Boolean,
default: false,
},
url: {
type: String,
default: '',
},
filter: {
type: Object,
default: null,
},
where: {
type: Object,
default: null,
},
sortBy: {
type: String,
default: '',
},
limit: {
type: String,
default: '',
},
});
defineExpose({ fetch });
const emit = defineEmits(['onFetch']);
onMounted(async () => {
if ($props.autoLoad) {
await fetch();
}
});
async function fetch() {
const filter = Object.assign({}, $props.filter);
if ($props.where) filter.where = $props.where;
if ($props.sortBy) filter.order = $props.sortBy;
if ($props.limit) filter.limit = $props.limit;
const { data } = await axios.get($props.url, {
params: { filter },
});
emit('onFetch', data);
}
const render = () => {
return h('div', []);
};
</script>
<template>
<render />
</template>

View File

@ -0,0 +1,118 @@
<script setup>
import { onMounted, onUnmounted, computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
import { useState } from 'src/composables/useState';
import { useValidator } from 'src/composables/useValidator';
import SkeletonForm from 'src/components/SkeletonForm.vue';
const quasar = useQuasar();
const { t } = useI18n();
const state = useState();
const { validate } = useValidator();
const $props = defineProps({
url: {
type: String,
default: '',
},
model: {
type: String,
default: '',
},
filter: {
type: Object,
default: null,
},
});
const emit = defineEmits(['onFetch']);
defineExpose({
save,
});
onMounted(async () => await fetch());
onUnmounted(() => {
state.unset($props.model);
});
const isLoading = ref(false);
const hasChanges = ref(false);
const formData = computed(() => state.get($props.model));
const originalData = ref();
async function fetch() {
const { data } = await axios.get($props.url, {
params: { filter: $props.filter },
});
state.set($props.model, data);
originalData.value = Object.assign({}, data);
watch(formData.value, () => (hasChanges.value = true));
emit('onFetch', state.get($props.model));
}
async function save() {
if (!hasChanges.value) {
return quasar.notify({
type: 'negative',
message: t('globals.noChanges'),
});
}
isLoading.value = true;
await axios.patch($props.url, formData.value);
originalData.value = formData.value;
hasChanges.value = false;
isLoading.value = false;
}
function reset() {
state.set($props.model, originalData.value);
hasChanges.value = false;
}
function filter(value, update, filterOptions) {
update(
() => {
const { options, filterFn } = filterOptions;
options.value = filterFn(options, value);
},
(ref) => {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
);
}
</script>
<template>
<q-banner v-if="hasChanges" class="text-white bg-warning">
<q-icon name="warning" size="md" class="q-mr-md" />
<span>{{ t('globals.changesToSave') }}</span>
</q-banner>
<q-form v-if="formData" @submit="save" @reset="reset" class="q-pa-md">
<slot name="form" :data="formData" :validate="validate" :filter="filter"></slot>
<div class="q-mt-lg">
<slot name="actions">
<q-btn :label="t('globals.save')" type="submit" color="primary" />
<q-btn
:label="t('globals.reset')"
type="reset"
class="q-ml-sm"
color="primary"
flat
:disable="!hasChanges"
/>
</slot>
</div>
</q-form>
<skeleton-form v-if="!formData" />
<q-inner-loading :showing="isLoading" :label="t('globals.pleaseWait')" color="primary" />
</template>

View File

@ -1,27 +1,39 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { onMounted, ref } from 'vue'; import { onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t } = useI18n();
const $props = defineProps({ const $props = defineProps({
autoLoad: {
type: Boolean,
default: false,
},
url: { url: {
type: String, type: String,
default: '', default: '',
}, },
data: {
type: Array,
default: null,
},
filter: { filter: {
type: Object, type: Object,
default: null, default: null,
}, },
autoLoad: { where: {
type: Boolean, type: Object,
default: false, default: null,
}, },
sortBy: { sortBy: {
type: String, type: String,
default: '', default: '',
}, },
limit: {
type: String,
default: '',
},
rowsPerPage: { rowsPerPage: {
type: Number, type: Number,
default: 10, default: 10,
@ -33,7 +45,18 @@ const $props = defineProps({
}); });
defineEmits(['onNavigate']); defineEmits(['onNavigate']);
defineExpose({ fetch }); defineExpose({ refresh });
onMounted(() => {
if ($props.autoLoad) paginate();
});
watch(
() => $props.data,
() => {
rows.value = $props.data;
}
);
const isLoading = ref(false); const isLoading = ref(false);
const hasMoreData = ref(false); const hasMoreData = ref(false);
@ -42,18 +65,12 @@ const pagination = ref({
rowsPerPage: $props.rowsPerPage, rowsPerPage: $props.rowsPerPage,
page: 1, page: 1,
}); });
const rows = ref(null); const rows = ref(null);
onMounted(() => {
if ($props.autoLoad) fetch();
else rows.value = [];
});
async function fetch() { async function fetch() {
const { page, rowsPerPage, sortBy, descending } = pagination.value; const { page, rowsPerPage, sortBy } = pagination.value;
isLoading.value = true; if (!$props.url) return;
const filter = { const filter = {
limit: rowsPerPage, limit: rowsPerPage,
@ -62,12 +79,26 @@ async function fetch() {
Object.assign(filter, $props.filter); 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; if (sortBy) filter.order = sortBy;
const { data } = await axios.get($props.url, { const { data } = await axios.get($props.url, {
params: { filter }, params: { filter },
}); });
isLoading.value = false;
return data;
}
async function paginate() {
const { page, rowsPerPage, sortBy, descending } = pagination.value;
const data = await fetch();
if (!data) { if (!data) {
isLoading.value = false; isLoading.value = false;
return; return;
@ -87,13 +118,31 @@ async function fetch() {
isLoading.value = false; isLoading.value = false;
} }
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;
}
async function onLoad(...params) { async function onLoad(...params) {
const done = params[1]; const done = params[1];
if (!rows.value || rows.value.length === 0) return done(false); if (!rows.value || rows.value.length === 0 || !$props.url) return done(false);
pagination.value.page = pagination.value.page + 1; pagination.value.page = pagination.value.page + 1;
await fetch(); await paginate();
const endOfPages = !hasMoreData.value; const endOfPages = !hasMoreData.value;
done(endOfPages); done(endOfPages);
@ -103,34 +152,7 @@ async function onLoad(...params) {
<template> <template>
<q-infinite-scroll @load="onLoad" :offset="offset" class="column items-center"> <q-infinite-scroll @load="onLoad" :offset="offset" class="column items-center">
<div v-if="rows" class="card-list q-gutter-y-md"> <div v-if="rows" class="card-list q-gutter-y-md">
<q-card class="card" v-for="row of rows" :key="row.id"> <slot name="body" :rows="rows"></slot>
<q-item v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
<q-item-section class="q-pa-md" @click="$emit('onNavigate', row.id)">
<slot name="header" :row="row">
<div class="text-h6">{{ row.name }}</div>
<q-item-label caption>#{{ row.id }}</q-item-label>
</slot>
<slot name="labels" :row="row"></slot>
</q-item-section>
<q-separator vertical />
<q-card-actions vertical class="justify-between">
<slot name="actions" :row="row">
<q-btn
flat
round
color="orange"
icon="arrow_circle_right"
@click="$emit('onNavigate', row.id)"
>
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
</q-btn>
<q-btn flat round color="grey-7" icon="preview">
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
</q-btn>
</slot>
</q-card-actions>
</q-item>
</q-card>
<div v-if="!rows.length && !isLoading" class="info-row q-pa-md text-center"> <div v-if="!rows.length && !isLoading" class="info-row q-pa-md text-center">
<h5> <h5>
{{ t('components.smartCard.noData') }} {{ t('components.smartCard.noData') }}

View File

@ -1,30 +1,32 @@
<template> <template>
<div class="row q-gutter-md q-mb-md"> <div class="q-pa-md">
<div class="col"> <div class="row q-gutter-md q-mb-md">
<q-skeleton type="QInput" square /> <div class="col">
<q-skeleton type="QInput" square />
</div>
<div class="col">
<q-skeleton type="QInput" square />
</div>
</div> </div>
<div class="col"> <div class="row q-gutter-md q-mb-md">
<q-skeleton type="QInput" square /> <div class="col">
<q-skeleton type="QInput" square />
</div>
<div class="col">
<q-skeleton type="QInput" square />
</div>
</div> </div>
</div> <div class="row q-gutter-md q-mb-md">
<div class="row q-gutter-md q-mb-md"> <div class="col">
<div class="col"> <q-skeleton type="QInput" square />
<q-skeleton type="QInput" square /> </div>
<div class="col">
<q-skeleton type="QInput" square />
</div>
</div> </div>
<div class="col"> <div class="row q-gutter-md">
<q-skeleton type="QInput" square /> <q-skeleton type="QBtn" />
<q-skeleton type="QBtn" />
</div> </div>
</div> </div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-skeleton type="QInput" square />
</div>
<div class="col">
<q-skeleton type="QInput" square />
</div>
</div>
<div class="row q-gutter-md">
<q-skeleton type="QBtn" />
<q-skeleton type="QBtn" />
</div>
</template> </template>

View File

@ -1,6 +1,6 @@
import { jest, describe, expect, it, beforeAll } from '@jest/globals'; import { jest, describe, expect, it, beforeAll } from '@jest/globals';
import { createWrapper, axios } from 'app/tests/jest/jestHelpers'; import { createWrapper, axios } from 'app/tests/jest/jestHelpers';
import SmartCard from '../SmartCard.vue'; import Paginate from '../Paginate.vue';
const mockPush = jest.fn(); const mockPush = jest.fn();
@ -11,7 +11,7 @@ jest.mock('vue-router', () => ({
}), }),
})); }));
describe('SmartCard', () => { describe('Paginate', () => {
const expectedUrl = '/api/customers'; const expectedUrl = '/api/customers';
let vm; let vm;
beforeAll(() => { beforeAll(() => {
@ -22,7 +22,7 @@ describe('SmartCard', () => {
rowsPerPage: 3 rowsPerPage: 3
} }
}; };
vm = createWrapper(SmartCard, options).vm; vm = createWrapper(Paginate, options).vm;
jest.spyOn(axios, 'get').mockResolvedValue({ jest.spyOn(axios, 'get').mockResolvedValue({
data: [ data: [
@ -39,8 +39,8 @@ describe('SmartCard', () => {
vm.hasMoreData = true; vm.hasMoreData = true;
}) })
describe('fetch()', () => { describe('paginate()', () => {
it('should call to the fetch() method and set the data on the rows property', async () => { it('should call to the paginate() method and set the data on the rows property', async () => {
const expectedOptions = { const expectedOptions = {
params: { params: {
filter: { filter: {
@ -51,13 +51,13 @@ describe('SmartCard', () => {
} }
}; };
await vm.fetch(); await vm.paginate();
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptions); expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptions);
expect(vm.rows.length).toEqual(3); expect(vm.rows.length).toEqual(3);
}); });
it('should call to the fetch() method and then call it again to paginate', async () => { it('should call to the paginate() method and then call it again to paginate', async () => {
const expectedOptions = { const expectedOptions = {
params: { params: {
filter: { filter: {
@ -68,7 +68,7 @@ describe('SmartCard', () => {
} }
}; };
await vm.fetch(); await vm.paginate();
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptions); expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptions);
expect(vm.rows.length).toEqual(3); expect(vm.rows.length).toEqual(3);
@ -85,7 +85,7 @@ describe('SmartCard', () => {
vm.pagination.page = 2; vm.pagination.page = 2;
await vm.fetch(); await vm.paginate();
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptionsPaginated); expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedOptionsPaginated);
expect(vm.rows.length).toEqual(6); expect(vm.rows.length).toEqual(6);

View File

@ -1,5 +1,7 @@
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
const state = ref({});
const user = ref({ const user = ref({
id: 0, id: 0,
name: '', name: '',
@ -44,11 +46,27 @@ export function useState() {
roles.value = data; roles.value = data;
} }
function set(name, data) {
state.value[name] = ref(data);
}
function get(name) {
return state.value[name];
}
function unset(name) {
delete state.value[name];
}
return { return {
getUser, getUser,
setUser, setUser,
getRoles, getRoles,
setRoles, setRoles,
set,
get,
unset,
drawer drawer
}; };
} }

View File

@ -33,10 +33,6 @@ export function useValidator() {
return validations(validation)[validation.validation]; return validations(validation)[validation.validation];
}); });
if (property === 'socialName') {
console.log(modelValidations[property])
}
return rules; return rules;
} }

View File

@ -21,9 +21,11 @@ export default {
yes: 'Yes', yes: 'Yes',
no: 'No', no: 'No',
noChanges: 'No changes to save', noChanges: 'No changes to save',
changesToSave: 'You have changes pending to save',
confirmRemove: 'You are about to delete this row. Are you sure?', confirmRemove: 'You are about to delete this row. Are you sure?',
rowAdded: 'Row added', rowAdded: 'Row added',
rowRemoved: 'Row removed' rowRemoved: 'Row removed',
pleaseWait: 'Please wait...'
}, },
moduleIndex: { moduleIndex: {
allModules: 'All modules' allModules: 'All modules'
@ -150,7 +152,8 @@ export default {
list: 'List', list: 'List',
createTicket: 'Create ticket', createTicket: 'Create ticket',
summary: 'Summary', summary: 'Summary',
basicData: 'Basic Data' basicData: 'Basic Data',
boxing: 'Boxing'
}, },
list: { list: {
nickname: 'Nickname', nickname: 'Nickname',
@ -197,8 +200,7 @@ export default {
state: 'State' state: 'State'
}, },
rmaList: { rmaList: {
code: 'Code', code: 'Code'
newRma: 'New RMA...'
}, },
rma: { rma: {
user: 'User', user: 'User',

View File

@ -21,9 +21,11 @@ export default {
yes: 'Si', yes: 'Si',
no: 'No', no: 'No',
noChanges: 'Sin cambios que guardar', noChanges: 'Sin cambios que guardar',
changesToSave: 'Tienes cambios pendientes de guardar',
confirmRemove: 'Vas a eliminar este registro. ¿Continuar?', confirmRemove: 'Vas a eliminar este registro. ¿Continuar?',
rowAdded: 'Fila añadida', rowAdded: 'Fila añadida',
rowRemoved: 'Fila eliminada' rowRemoved: 'Fila eliminada',
pleaseWait: 'Por favor, espera...'
}, },
moduleIndex: { moduleIndex: {
allModules: 'Todos los módulos' allModules: 'Todos los módulos'
@ -149,7 +151,8 @@ export default {
list: 'Listado', list: 'Listado',
createTicket: 'Crear ticket', createTicket: 'Crear ticket',
summary: 'Resumen', summary: 'Resumen',
basicData: 'Datos básicos' basicData: 'Datos básicos',
boxing: 'Encajado'
}, },
list: { list: {
nickname: 'Alias', nickname: 'Alias',
@ -196,8 +199,7 @@ export default {
state: 'Estado' state: 'Estado'
}, },
rmaList: { rmaList: {
code: 'Código', code: 'Código'
newRma: 'Nuevo RMA...'
}, },
rma: { rma: {
user: 'Usuario', user: 'Usuario',

View File

@ -1,152 +1,100 @@
<script setup> <script setup>
import { ref, onMounted, watch } from 'vue'; import { ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
import { useSession } from 'src/composables/useSession';
import { useValidator } from 'src/composables/useValidator';
import SkeletonForm from 'src/components/SkeletonForm.vue';
onMounted(() => { import { useSession } from 'src/composables/useSession';
fetch(); import FetchData from 'src/components/FetchData.vue';
fetchWorkers(); import FormModel from 'src/components/FormModel.vue';
fetchClaimStates();
});
const route = useRoute(); const route = useRoute();
const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const { validate } = useValidator();
const session = useSession(); const session = useSession();
const token = session.getToken(); const token = session.getToken();
const claim = ref(null); const claimFilter = {
const claimCopy = ref(null); include: [
const hasChanges = ref(false); {
relation: 'client',
function fetch() { scope: {
const id = route.params.id; fields: ['name'],
const filter = {
include: [
{
relation: 'client',
scope: {
fields: ['name'],
},
}, },
], },
}; ],
const options = { params: { filter } }; };
axios.get(`Claims/${id}`, options).then(({ data }) => {
claim.value = data;
claimCopy.value = Object.assign({}, data);
watch(claim.value, () => (hasChanges.value = true));
});
}
const workers = ref([]); const workers = ref([]);
const workersCopy = ref([]); const workersCopy = ref([]);
function fetchWorkers() {
const filter = {
where: {
role: 'salesPerson',
},
};
const options = { params: { filter } };
axios.get(`Workers/activeWithRole`, options).then(({ data }) => {
workers.value = data;
workersCopy.value = data;
});
}
const claimStates = ref([]); const claimStates = ref([]);
const claimStatesCopy = ref([]); const claimStatesCopy = ref([]);
function fetchClaimStates() {
axios.get(`ClaimStates`).then(({ data }) => { function setWorkers(data) {
claimStates.value = data; workers.value = data;
claimStatesCopy.value = data; workersCopy.value = data;
});
} }
function filter(value, update, options, originalOptions, filter) { function setClaimStates(data) {
update( claimStates.value = data;
() => { claimStatesCopy.value = data;
if (value === '') {
options.value = originalOptions.value;
return;
}
options.value = options.value.filter(filter);
},
(ref) => {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
);
} }
function filterWorkers(value, update) { const workerFilter = {
const search = value.toLowerCase(); options: workers,
filterFn: (options, value) => {
const search = value.toLowerCase();
filter(value, update, workers, workersCopy, (row) => { if (value === '') return workersCopy.value;
const id = row.id;
const name = row.name.toLowerCase();
const idMatch = id == search; return options.value.filter((row) => {
const nameMatch = name.indexOf(search) > -1; const id = row.id;
const name = row.name.toLowerCase();
return idMatch || nameMatch; const idMatches = id == search;
}); const nameMatches = name.indexOf(search) > -1;
}
function filterStates(value, update) { return idMatches || nameMatches;
const search = value.toLowerCase();
filter(value, update, claimStates, claimStatesCopy, (row) => {
const description = row.description.toLowerCase();
return description.indexOf(search) > -1;
});
}
function save() {
const id = route.params.id;
const formData = claim.value;
if (!hasChanges.value) {
return quasar.notify({
type: 'negative',
message: t('globals.noChanges'),
}); });
} },
};
axios.patch(`Claims/${id}`, formData).then((hasChanges.value = false)); const statesFilter = {
} options: claimStates,
filterFn: (options, value) => {
const search = value.toLowerCase();
function onReset() { if (value === '') return claimStatesCopy.value;
claim.value = claimCopy.value;
hasChanges.value = false; return options.value.filter((row) => {
} const description = row.description.toLowerCase();
return description.indexOf(search) > -1;
});
},
};
</script> </script>
<template> <template>
<q-page class="q-pa-md"> <fetch-data
<div class="container"> url="Workers/activeWithInheritedRole"
<q-card class="q-pa-md"> :filter="{ where: { role: 'salesPerson' } }"
<skeleton-form v-if="!claim" /> @on-fetch="setWorkers"
<q-form v-if="claim" @submit="save" @reset="onReset" greedy> auto-load
/>
<fetch-data url="ClaimStates" @on-fetch="setClaimStates" auto-load />
<div class="container">
<q-card>
<form-model :url="`Claims/${route.params.id}`" :filter="claimFilter" model="claim">
<template #form="{ data, validate, filter }">
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<q-input v-model="claim.client.name" :label="t('claim.basicData.customer')" disable /> <q-input v-model="data.client.name" :label="t('claim.basicData.customer')" disable />
</div> </div>
<div class="col"> <div class="col">
<q-input v-model="claim.created" mask="####-##-##" fill-mask="_" autofocus> <q-input v-model="data.created" mask="####-##-##" fill-mask="_" autofocus>
<template #append> <template #append>
<q-icon name="event" class="cursor-pointer"> <q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale"> <q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="claim.created" mask="YYYY-MM-DD"> <q-date v-model="data.created" mask="YYYY-MM-DD">
<div class="row items-center justify-end"> <div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat /> <q-btn v-close-popup label="Close" color="primary" flat />
</div> </div>
@ -160,7 +108,7 @@ function onReset() {
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<q-select <q-select
v-model="claim.workerFk" v-model="data.workerFk"
:options="workers" :options="workers"
option-value="id" option-value="id"
option-label="name" option-label="name"
@ -168,15 +116,15 @@ function onReset() {
:label="t('claim.basicData.assignedTo')" :label="t('claim.basicData.assignedTo')"
map-options map-options
use-input use-input
@filter="filterWorkers" @filter="(value, update) => filter(value, update, workerFilter)"
:rules="validate('claim.claimStateFk')" :rules="validate('claim.claimStateFk')"
:input-debounce="0" :input-debounce="0"
> >
<template #before> <template #before>
<q-avatar color="orange"> <q-avatar color="orange">
<q-img <q-img
v-if="claim.workerFk" v-if="data.workerFk"
:src="`/api/Images/user/160x160/${claim.workerFk}/download?access_token=${token}`" :src="`/api/Images/user/160x160/${data.workerFk}/download?access_token=${token}`"
spinner-color="white" spinner-color="white"
/> />
</q-avatar> </q-avatar>
@ -185,7 +133,7 @@ function onReset() {
</div> </div>
<div class="col"> <div class="col">
<q-select <q-select
v-model="claim.claimStateFk" v-model="data.claimStateFk"
:options="claimStates" :options="claimStates"
option-value="id" option-value="id"
option-label="description" option-label="description"
@ -193,7 +141,7 @@ function onReset() {
:label="t('claim.basicData.state')" :label="t('claim.basicData.state')"
map-options map-options
use-input use-input
@filter="filterStates" @filter="(value, update) => filter(value, update, statesFilter)"
:rules="validate('claim.claimStateFk')" :rules="validate('claim.claimStateFk')"
:input-debounce="0" :input-debounce="0"
> >
@ -203,14 +151,14 @@ function onReset() {
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<q-input <q-input
v-model="claim.packages" v-model="data.packages"
:label="t('claim.basicData.packages')" :label="t('claim.basicData.packages')"
:rules="validate('claim.packages')" :rules="validate('claim.packages')"
/> />
</div> </div>
<div class="col"> <div class="col">
<q-input <q-input
v-model="claim.rma" v-model="data.rma"
:label="t('claim.basicData.returnOfMaterial')" :label="t('claim.basicData.returnOfMaterial')"
:rules="validate('claim.rma')" :rules="validate('claim.rma')"
/> />
@ -218,17 +166,13 @@ function onReset() {
</div> </div>
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<q-checkbox v-model="claim.hasToPickUp" :label="t('claim.basicData.picked')" /> <q-checkbox v-model="data.hasToPickUp" :label="t('claim.basicData.picked')" />
</div> </div>
</div> </div>
<div> </template>
<q-btn :label="t('globals.save')" type="submit" color="primary" /> </form-model>
<q-btn :label="t('globals.reset')" type="reset" class="q-ml-sm" color="primary" flat /> </q-card>
</div> </div>
</q-form>
</q-card>
</div>
</q-page>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -163,7 +163,9 @@ function stateColor(code) {
</q-scroll-area> </q-scroll-area>
</q-drawer> </q-drawer>
<q-page-container> <q-page-container>
<router-view v-if="claim.id" :claim="claim"></router-view> <q-page class="q-pa-md">
<router-view></router-view>
</q-page>
</q-page-container> </q-page-container>
</template> </template>

View File

@ -1,52 +1,49 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import SmartCard from 'src/components/SmartCard.vue'; import Paginate from 'src/components/Paginate.vue';
import FetchData from 'src/components/FetchData.vue';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
onMounted(() => fetch());
const $props = defineProps({
claim: {
type: Object,
required: true,
},
});
const quasar = useQuasar(); const quasar = useQuasar();
const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const claim = ref([]);
const fetcher = ref();
const filter = { const filter = {
include: { include: {
relation: 'worker', relation: 'rmas',
scope: { scope: {
include: { include: {
relation: 'user', relation: 'worker',
scope: {
include: {
relation: 'user',
},
},
}, },
order: 'created DESC',
}, },
}, },
where: {
code: $props.claim.rma,
},
}; };
function fetch() { async function addRow() {
//console.log($props.claim);
}
function addRow() {
const formData = { const formData = {
code: $props.claim.rma, code: claim.value.rma,
}; };
axios.post(`ClaimRmas`, formData).then(() => { await axios.post(`ClaimRmas`, formData);
quasar.notify({ await fetcher.value.fetch();
type: 'positive',
message: t('globals.rowAdded'), quasar.notify({
icon: 'check', type: 'positive',
}); message: t('globals.rowAdded'),
icon: 'check',
}); });
} }
@ -57,16 +54,17 @@ function confirmRemove(id) {
rmaId.value = id; rmaId.value = id;
} }
function remove() { async function remove() {
const id = rmaId.value; const id = rmaId.value;
axios.delete(`ClaimRmas/${id}`).then(() => {
confirmShown.value = false;
quasar.notify({ await axios.delete(`ClaimRmas/${id}`);
type: 'positive', await fetcher.value.fetch();
message: t('globals.rowRemoved'), confirmShown.value = false;
icon: 'check',
}); quasar.notify({
type: 'positive',
message: t('globals.rowRemoved'),
icon: 'check',
}); });
} }
@ -75,7 +73,14 @@ function hide() {
} }
</script> </script>
<template> <template>
<q-page class="q-pa-md sticky"> <fetch-data
ref="fetcher"
:url="`Claims/${route.params.id}`"
:filter="filter"
@on-fetch="($data) => (claim = $data)"
auto-load
/>
<div class="sticky-page">
<q-page-sticky expand position="top"> <q-page-sticky expand position="top">
<q-toolbar class="bg-grey-9"> <q-toolbar class="bg-grey-9">
<q-space /> <q-space />
@ -85,28 +90,41 @@ function hide() {
</q-toolbar> </q-toolbar>
</q-page-sticky> </q-page-sticky>
<smart-card ref="card" url="/ClaimRmas" :filter="filter" sort-by="id DESC" auto-load> <paginate :data="claim.rmas">
<template #header="{ row }"> <template #body="{ rows }">
<q-item-label caption>{{ t('claim.rma.user') }}</q-item-label> <q-card class="card">
<q-item-label>{{ row.worker.user.name }}</q-item-label> <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.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>
{{ toDate(row.created, { timeStyle: 'medium' }) }}
</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="orange" icon="vn:bin" @click="confirmRemove(row.id)">
<q-tooltip>{{ t('globals.remove') }}</q-tooltip>
</q-btn>
</q-card-actions>
</q-item>
<q-separator />
</template>
</q-card>
</template> </template>
<template #labels="{ row }"> </paginate>
<q-list> </div>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('claim.rma.created') }}</q-item-label>
<q-item-label>{{ toDate(row.created, { timeStyle: 'medium' }) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</template>
<template #actions="{ row }">
<q-btn flat round color="orange" icon="vn:bin" @click="confirmRemove(row.id)">
<q-tooltip>{{ t('globals.remove') }}</q-tooltip>
</q-btn>
</template>
</smart-card>
</q-page>
<q-dialog v-model="confirmShown" persistent @hide="hide"> <q-dialog v-model="confirmShown" persistent @hide="hide">
<q-card> <q-card>
<q-card-section class="row items-center"> <q-card-section class="row items-center">
@ -126,7 +144,7 @@ function hide() {
.q-toolbar { .q-toolbar {
background-color: $grey-9; background-color: $grey-9;
} }
.sticky { .sticky-page {
padding-top: 66px; padding-top: 66px;
} }

View File

@ -90,75 +90,73 @@ function stateColor(code) {
</script> </script>
<template> <template>
<q-page class="q-pa-md"> <div class="summary container">
<div class="summary container"> <q-card>
<q-card> <skeleton-summary v-if="!claim" />
<skeleton-summary v-if="!claim" /> <template v-if="claim">
<template v-if="claim"> <div class="header bg-primary q-pa-sm q-mb-md">{{ claim.id }} - {{ claim.client.name }}</div>
<div class="header bg-primary q-pa-sm q-mb-md">{{ claim.id }} - {{ claim.client.name }}</div> <q-list>
<q-list> <q-item>
<q-item> <q-item-section>
<q-item-section> <q-item-label caption>{{ t('claim.summary.created') }}</q-item-label>
<q-item-label caption>{{ t('claim.summary.created') }}</q-item-label> <q-item-label>{{ toDate(claim.created) }}</q-item-label>
<q-item-label>{{ toDate(claim.created) }}</q-item-label> </q-item-section>
</q-item-section> <q-item-section>
<q-item-section> <q-item-label caption>{{ t('claim.summary.state') }}</q-item-label>
<q-item-label caption>{{ t('claim.summary.state') }}</q-item-label> <q-item-label>
<q-item-label> <q-chip :color="stateColor(claim.claimState.code)" dense>
<q-chip :color="stateColor(claim.claimState.code)" dense> {{ claim.claimState.description }}
{{ claim.claimState.description }} </q-chip>
</q-chip> </q-item-label>
</q-item-label> </q-item-section>
</q-item-section> </q-item>
</q-item> <q-item>
<q-item> <q-item-section>
<q-item-section> <q-item-label caption>{{ t('claim.summary.assignedTo') }}</q-item-label>
<q-item-label caption>{{ t('claim.summary.assignedTo') }}</q-item-label> <q-item-label>{{ claim.worker.user.nickname }}</q-item-label>
<q-item-label>{{ claim.worker.user.nickname }}</q-item-label> </q-item-section>
</q-item-section> <q-item-section>
<q-item-section> <q-item-label caption>{{ t('claim.summary.attendedBy') }}</q-item-label>
<q-item-label caption>{{ t('claim.summary.attendedBy') }}</q-item-label> <q-item-label>{{ claim.client.salesPersonUser.name }}</q-item-label>
<q-item-label>{{ claim.client.salesPersonUser.name }}</q-item-label> </q-item-section>
</q-item-section> </q-item>
</q-item> </q-list>
</q-list> <q-card-section class="q-pa-md">
<q-card-section class="q-pa-md"> <h6>{{ t('claim.summary.details') }}</h6>
<h6>{{ t('claim.summary.details') }}</h6> <q-table :columns="detailsColumns" :rows="salesClaimed" flat>
<q-table :columns="detailsColumns" :rows="salesClaimed" flat> <template #header="props">
<template #header="props"> <q-tr :props="props">
<q-tr :props="props"> <q-th v-for="col in props.cols" :key="col.name" :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props"> {{ t(col.label) }}
{{ t(col.label) }} </q-th>
</q-th> </q-tr>
</q-tr> </template>
</template> </q-table>
</q-table> </q-card-section>
</q-card-section> <q-card-section class="q-pa-md">
<q-card-section class="q-pa-md"> <h6>{{ t('claim.summary.actions') }}</h6>
<h6>{{ t('claim.summary.actions') }}</h6> <q-separator />
<q-separator /> <div id="slider-container">
<div id="slider-container"> <q-slider
<q-slider v-model="claim.responsibility"
v-model="claim.responsibility" label
label :label-value="t('claim.summary.responsibility')"
:label-value="t('claim.summary.responsibility')" label-always
label-always color="primary"
color="primary" markers
markers :marker-labels="[
:marker-labels="[ { value: 1, label: t('claim.summary.company') },
{ value: 1, label: t('claim.summary.company') }, { value: 5, label: t('claim.summary.person') },
{ value: 5, label: t('claim.summary.person') }, ]"
]" :min="1"
:min="1" :max="5"
:max="5" readonly
readonly />
/> </div>
</div> </q-card-section>
</q-card-section> </template>
</template> </q-card>
</q-card> </div>
</div>
</q-page>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -194,4 +192,8 @@ function stateColor(code) {
} }
} }
} }
.q-dialog .summary {
max-width: 1200px;
}
</style> </style>

View File

@ -1,52 +0,0 @@
<script setup>
import { reactive, watch } from 'vue'
const customer = reactive({
name: '',
});
watch(() => customer.name, () => {
console.log('customer.name changed');
});
</script>
<template>
<q-page class="q-pa-md">
<q-card class="q-pa-md">
<q-form @submit="onSubmit" @reset="onReset" class="q-gutter-md">
<q-input
filled
v-model="customer.name"
label="Your name *"
hint="Name and surname"
lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']"
/>
<q-input
filled
type="number"
v-model="age"
label="Your age *"
lazy-rules
:rules="[
val => val !== null && val !== '' || 'Please type your age',
val => val > 0 && val < 100 || 'Please type a real age'
]"
/>
<div>
<q-btn label="Submit" type="submit" color="primary" />
<q-btn label="Reset" type="reset" color="primary" flat class="q-ml-sm" />
</div>
</q-form>
</q-card>
</q-page>
</template>
<style lang="scss" scoped>
.card {
width: 100%;
max-width: 60em;
}
</style>

View File

@ -2,7 +2,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import SmartCard from 'src/components/SmartCard.vue'; import Paginate from 'src/components/Paginate.vue';
import { toDate } from 'src/filters/index'; import { toDate } from 'src/filters/index';
import ClaimSummary from './Card/ClaimSummary.vue'; import ClaimSummary from './Card/ClaimSummary.vue';
@ -50,64 +50,73 @@ function showPreview(id) {
<template> <template>
<q-page class="q-pa-md"> <q-page class="q-pa-md">
<smart-card url="/Claims" :filter="filter" sort-by="id DESC" @on-navigate="navigate" auto-load> <paginate url="/Claims" :filter="filter" sort-by="id DESC" auto-load>
<template #labels="{ row }"> <template #body="{ rows }">
<q-list> <q-card class="card" v-for="row of rows" :key="row.id">
<q-item class="q-pa-none"> <q-item class="q-pa-none items-start cursor-pointer q-hoverable" v-ripple clickable>
<q-item-section> <q-item-section class="q-pa-md" @click="navigate(row.id)">
<q-item-label caption>{{ t('claim.list.customer') }}</q-item-label> <div class="text-h6">{{ row.name }}</div>
<q-item-label>{{ row.client.name }}</q-item-label> <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-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-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-section>
<q-item-section>
<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-item-label>
</q-item-section>
</q-item>
</q-list>
</q-item-section> </q-item-section>
<q-item-section> <q-separator vertical />
<q-item-label caption>{{ t('claim.list.assignedTo') }}</q-item-label> <q-card-actions vertical class="justify-between">
<q-item-label>{{ row.worker.user.name }}</q-item-label> <q-btn color="grey-7" round flat icon="more_vert">
</q-item-section> <q-tooltip>{{ t('customer.list.moreOptions') }}</q-tooltip>
</q-item> <q-menu cover auto-close>
<q-item class="q-pa-none"> <q-list>
<q-item-section> <q-item clickable>
<q-item-label caption>{{ t('claim.list.created') }}</q-item-label> <q-item-section avatar>
<q-item-label>{{ toDate(row.created) }}</q-item-label> <q-icon name="add" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>Add a note</q-item-section>
<q-item-label caption>{{ t('claim.list.state') }}</q-item-label> </q-item>
<q-item-label> <q-item clickable>
<q-chip :color="stateColor(row.claimState.code)" dense> <q-item-section avatar>
{{ row.claimState.description }} <q-icon name="logs" />
</q-chip> </q-item-section>
</q-item-label> <q-item-section>Display claim logs</q-item-section>
</q-item-section> </q-item>
</q-item> </q-list>
</q-list> </q-menu>
</template> </q-btn>
<template #actions="{ row }">
<q-btn color="grey-7" round flat icon="more_vert">
<q-tooltip>{{ t('customer.list.moreOptions') }}</q-tooltip>
<q-menu cover auto-close>
<q-list>
<q-item clickable>
<q-item-section avatar>
<q-icon name="add" />
</q-item-section>
<q-item-section>Add a note</q-item-section>
</q-item>
<q-item clickable>
<q-item-section avatar>
<q-icon name="logs" />
</q-item-section>
<q-item-section>Display claim logs</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)"> <q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip> <q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
</q-btn> </q-btn>
<q-btn flat round color="grey-7" icon="preview" @click="showPreview(row.id)"> <q-btn flat round color="grey-7" icon="preview" @click="showPreview(row.id)">
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip> <q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
</q-btn> </q-btn>
</q-card-actions>
</q-item>
</q-card>
</template> </template>
</smart-card> </paginate>
</q-page> </q-page>
<q-dialog v-model="preview.shown"> <q-dialog v-model="preview.shown">
<claim-summary :claim-id="preview.data.claimId" /> <claim-summary :claim-id="preview.data.claimId" />

View File

@ -3,7 +3,7 @@ import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import axios from 'axios'; import axios from 'axios';
import SmartCard from 'src/components/SmartCard.vue'; import Paginate from 'src/components/Paginate.vue';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
@ -26,7 +26,7 @@ function submit() {
crated: new Date(), crated: new Date(),
}; };
}) })
.then(() => card.value.fetch()); .then(() => card.value.refresh());
} }
const confirmShown = ref(false); const confirmShown = ref(false);
@ -38,15 +38,18 @@ function confirm(id) {
function remove() { function remove() {
const id = rmaId.value; const id = rmaId.value;
axios.delete(`ClaimRmas/${id}`).then(() => { axios
confirmShown.value = false; .delete(`ClaimRmas/${id}`)
.then(() => {
confirmShown.value = false;
quasar.notify({ quasar.notify({
type: 'positive', type: 'positive',
message: 'Entry deleted', message: 'Entry deleted',
icon: 'check', icon: 'check',
}); });
}); })
.then(() => card.value.refresh());
} }
function hide() { function hide() {
@ -59,29 +62,38 @@ function hide() {
<q-page-sticky expand position="top" :offset="[16, 16]"> <q-page-sticky expand position="top" :offset="[16, 16]">
<q-card class="card q-pa-md"> <q-card class="card q-pa-md">
<q-form @submit="submit"> <q-form @submit="submit">
<q-input v-model="newRma.code" :label="t('claim.rmaList.newRma')" class="q-mb-md" /> <q-input v-model="newRma.code" :label="t('claim.rmaList.code')" class="q-mb-md" autofocus />
<div class="text-caption">$(0) entries</div> <!-- <div class="text-caption">$(0) entries</div> -->
</q-form> </q-form>
</q-card> </q-card>
</q-page-sticky> </q-page-sticky>
<smart-card ref="card" url="/ClaimRmas" sort-by="id DESC" auto-load> <paginate ref="card" url="/ClaimRmas" sort-by="id DESC" auto-load>
<template #labels="{ row }"> <template #body="{ rows }">
<q-list> <q-card class="card">
<q-item class="q-pa-none"> <template v-for="row of rows" :key="row.id">
<q-item-section> <q-item class="q-pa-none items-start">
<q-item-label caption>{{ t('claim.rmaList.code') }}</q-item-label> <q-item-section class="q-pa-md" @click="navigate(row.id)">
<q-item-label>{{ row.code }}</q-item-label> <q-list>
</q-item-section> <q-item class="q-pa-none">
</q-item> <q-item-section>
</q-list> <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-tooltip>{{ t('globals.remove') }}</q-tooltip>
</q-btn>
</q-card-actions>
</q-item>
<q-separator />
</template>
</q-card>
</template> </template>
<template #actions="{ row }"> </paginate>
<q-btn flat round color="primary" icon="vn:bin" @click="confirm(row.id)">
<q-tooltip>{{ t('globals.remove') }}</q-tooltip>
</q-btn>
</template>
</smart-card>
</q-page> </q-page>
<q-dialog v-model="confirmShown" persistent @hide="hide"> <q-dialog v-model="confirmShown" persistent @hide="hide">

View File

@ -1,135 +1,64 @@
<script setup> <script setup>
import { ref, onMounted, watch } from 'vue'; import { ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
import { useSession } from 'src/composables/useSession';
import { useValidator } from 'src/composables/useValidator';
import SkeletonForm from 'src/components/SkeletonForm.vue';
onMounted(() => { import { useSession } from 'src/composables/useSession';
fetch(); import FetchData from 'src/components/FetchData.vue';
fetchWorkers(); import FormModel from 'src/components/FormModel.vue';
fetchBusinessTypes();
fetchContactChannels();
});
const route = useRoute(); const route = useRoute();
const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const { validate } = useValidator();
const session = useSession(); const session = useSession();
const token = session.getToken(); const token = session.getToken();
const customer = ref(null);
const customerCopy = ref(null);
const hasChanges = ref(false);
function fetch() {
const id = route.params.id;
const filter = {
include: [],
};
const options = { params: { filter } };
axios.get(`Clients/${id}`, options).then(({ data }) => {
customer.value = data;
customerCopy.value = Object.assign({}, data);
watch(customer.value, () => (hasChanges.value = true));
});
}
const businessTypes = ref([]);
function fetchBusinessTypes() {
axios.get(`BusinessTypes`).then(({ data }) => {
businessTypes.value = data;
});
}
const contactChannels = ref([]);
function fetchContactChannels() {
axios.get(`ContactChannels`).then(({ data }) => {
contactChannels.value = data;
});
}
const workers = ref([]); const workers = ref([]);
const workersCopy = ref([]); const workersCopy = ref([]);
function fetchWorkers() { const businessTypes = ref([]);
const filter = { const contactChannels = ref([]);
where: {
role: 'salesPerson', function setWorkers(data) {
}, workers.value = data;
}; workersCopy.value = data;
const options = { params: { filter } };
axios.get(`Workers/activeWithRole`, options).then(({ data }) => {
workers.value = data;
workersCopy.value = data;
});
} }
function filter(value, update, options, originalOptions, filter) { const filterOptions = {
update( options: workers,
() => { filterFn: (options, value) => {
if (value === '') { const search = value.toLowerCase();
options.value = originalOptions.value;
return; if (value === '') return workersCopy.value;
}
options.value = options.value.filter(filter); return options.value.filter((row) => {
}, const id = row.id;
(ref) => { const name = row.name.toLowerCase();
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
);
}
function filterWorkers(value, update) { const idMatches = id == search;
const search = value.toLowerCase(); const nameMatches = name.indexOf(search) > -1;
filter(value, update, workers, workersCopy, (row) => { return idMatches || nameMatches;
const id = row.id;
const name = row.name.toLowerCase();
const idMatch = id == search;
const nameMatch = name.indexOf(search) > -1;
return idMatch || nameMatch;
});
}
function save() {
const id = route.params.id;
const formData = customer.value;
if (!hasChanges.value) {
return quasar.notify({
type: 'negative',
message: t('globals.noChanges'),
}); });
} },
};
axios.patch(`Clients/${id}`, formData).then((hasChanges.value = false));
}
function onReset() {
customer.value = customerCopy.value;
hasChanges.value = false;
}
</script> </script>
<template> <template>
<q-page class="q-pa-md"> <fetch-data
<div class="container"> url="Workers/activeWithInheritedRole"
<q-card class="q-pa-md"> :filter="{ where: { role: 'salesPerson' } }"
<skeleton-form v-if="!customer" /> @on-fetch="setWorkers"
<q-form v-if="customer" @submit="save" @reset="onReset" greedy> 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">
<template #form="{ data, validate, filter }">
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<q-input <q-input
v-model="customer.socialName" v-model="data.socialName"
:label="t('customer.basicData.socialName')" :label="t('customer.basicData.socialName')"
:rules="validate('client.socialName')" :rules="validate('client.socialName')"
autofocus autofocus
@ -137,7 +66,7 @@ function onReset() {
</div> </div>
<div class="col"> <div class="col">
<q-select <q-select
v-model="customer.businessTypeFk" v-model="data.businessTypeFk"
:options="businessTypes" :options="businessTypes"
option-value="code" option-value="code"
option-label="description" option-label="description"
@ -152,7 +81,7 @@ function onReset() {
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<q-input <q-input
v-model="customer.contact" v-model="data.contact"
:label="t('customer.basicData.contact')" :label="t('customer.basicData.contact')"
:rules="validate('client.contact')" :rules="validate('client.contact')"
clearable clearable
@ -160,7 +89,7 @@ function onReset() {
</div> </div>
<div class="col"> <div class="col">
<q-input <q-input
v-model="customer.email" v-model="data.email"
type="email" type="email"
:label="t('customer.basicData.email')" :label="t('customer.basicData.email')"
:rules="validate('client.email')" :rules="validate('client.email')"
@ -171,7 +100,7 @@ function onReset() {
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<q-input <q-input
v-model="customer.phone" v-model="data.phone"
:label="t('customer.basicData.phone')" :label="t('customer.basicData.phone')"
:rules="validate('client.phone')" :rules="validate('client.phone')"
clearable clearable
@ -179,7 +108,7 @@ function onReset() {
</div> </div>
<div class="col"> <div class="col">
<q-input <q-input
v-model="customer.mobile" v-model="data.mobile"
:label="t('customer.basicData.mobile')" :label="t('customer.basicData.mobile')"
:rules="validate('client.mobile')" :rules="validate('client.mobile')"
clearable clearable
@ -189,7 +118,7 @@ function onReset() {
<div class="row q-gutter-md q-mb-md"> <div class="row q-gutter-md q-mb-md">
<div class="col"> <div class="col">
<q-select <q-select
v-model="customer.salesPersonFk" v-model="data.salesPersonFk"
:options="workers" :options="workers"
option-value="id" option-value="id"
option-label="name" option-label="name"
@ -197,15 +126,15 @@ function onReset() {
:label="t('customer.basicData.salesPerson')" :label="t('customer.basicData.salesPerson')"
map-options map-options
use-input use-input
@filter="filterWorkers" @filter="(value, update) => filter(value, update, filterOptions)"
:rules="validate('client.salesPersonFk')" :rules="validate('client.salesPersonFk')"
:input-debounce="0" :input-debounce="0"
> >
<template #before> <template #before>
<q-avatar color="orange"> <q-avatar color="orange">
<q-img <q-img
v-if="customer.salesPersonFk" v-if="data.salesPersonFk"
:src="`/api/Images/user/160x160/${customer.salesPersonFk}/download?access_token=${token}`" :src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
spinner-color="white" spinner-color="white"
/> />
</q-avatar> </q-avatar>
@ -214,7 +143,7 @@ function onReset() {
</div> </div>
<div class="col"> <div class="col">
<q-select <q-select
v-model="customer.contactChannelFk" v-model="data.contactChannelFk"
:options="contactChannels" :options="contactChannels"
option-value="id" option-value="id"
option-label="name" option-label="name"
@ -226,14 +155,10 @@ function onReset() {
/> />
</div> </div>
</div> </div>
<div> </template>
<q-btn :label="t('globals.save')" type="submit" color="primary" /> </form-model>
<q-btn :label="t('globals.reset')" type="reset" class="q-ml-sm" color="primary" flat /> </q-card>
</div> </div>
</q-form>
</q-card>
</div>
</q-page>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -174,7 +174,9 @@ async function fetch() {
</q-scroll-area> </q-scroll-area>
</q-drawer> </q-drawer>
<q-page-container> <q-page-container>
<router-view></router-view> <q-page class="q-pa-md">
<router-view></router-view>
</q-page>
</q-page-container> </q-page-container>
</template> </template>

View File

@ -29,9 +29,7 @@ function fetch() {
} }
const balanceDue = computed(() => { const balanceDue = computed(() => {
const [defaulter] = customer.value.defaulters; return customer.value.defaulters.length && customer.value.defaulters[0].amount;
return defaulter.amount;
}); });
const balanceDueWarning = computed(() => (balanceDue.value ? 'negative' : '')); const balanceDueWarning = computed(() => (balanceDue.value ? 'negative' : ''));
@ -64,378 +62,374 @@ const creditWarning = computed(() => {
</script> </script>
<template> <template>
<q-page class="q-pa-md"> <div class="summary container">
<div class="summary container"> <q-card>
<q-card> <skeleton-summary v-if="!customer" />
<skeleton-summary v-if="!customer" /> <template v-if="customer">
<template v-if="customer"> <div class="header bg-primary q-pa-sm q-mb-md">{{ customer.id }} - {{ customer.name }}</div>
<div class="header bg-primary q-pa-sm q-mb-md">{{ customer.id }} - {{ customer.name }}</div> <div class="row q-pa-md q-col-gutter-md q-mb-md">
<div class="row q-pa-md q-col-gutter-md q-mb-md"> <div class="col">
<div class="col"> <q-list>
<q-list> <q-item-label header class="text-h6">
<q-item-label header class="text-h6"> {{ t('customer.summary.basicData') }}
{{ t('customer.summary.basicData') }} <router-link
<router-link :to="{ name: 'CustomerBasicData' }"> :to="{ name: 'CustomerBasicData', params: { id: entityId } }"
<q-icon name="open_in_new" /> target="_blank"
</router-link> >
</q-item-label> <q-icon name="open_in_new" />
</router-link>
</q-item-label>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('customer.summary.customerId') }}</q-item-label> <q-item-label caption>{{ t('customer.summary.customerId') }}</q-item-label>
<q-item-label>{{ customer.id }}</q-item-label> <q-item-label>{{ customer.id }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('customer.summary.name') }}</q-item-label> <q-item-label caption>{{ t('customer.summary.name') }}</q-item-label>
<q-item-label>{{ customer.name }}</q-item-label> <q-item-label>{{ customer.name }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('customer.summary.contact') }}</q-item-label> <q-item-label caption>{{ t('customer.summary.contact') }}</q-item-label>
<q-item-label>{{ customer.contact }}</q-item-label> <q-item-label>{{ customer.contact }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item v-if="customer.salesPersonUser">
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('customer.summary.salesPerson') }}</q-item-label> <q-item-label caption>{{ t('customer.summary.salesPerson') }}</q-item-label>
<q-item-label>{{ customer.salesPersonUser.name }}</q-item-label> <q-item-label>{{ customer.salesPersonUser.name }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('customer.summary.phone') }}</q-item-label> <q-item-label caption>{{ t('customer.summary.phone') }}</q-item-label>
<q-item-label>{{ customer.phone }}</q-item-label> <q-item-label>{{ customer.phone }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('customer.summary.mobile') }}</q-item-label> <q-item-label caption>{{ t('customer.summary.mobile') }}</q-item-label>
<q-item-label>{{ customer.mobile }}</q-item-label> <q-item-label>{{ customer.mobile }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('customer.summary.email') }}</q-item-label> <q-item-label caption>{{ t('customer.summary.email') }}</q-item-label>
<q-item-label>{{ customer.email }}</q-item-label> <q-item-label>{{ customer.email }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item v-if="customer.contactChannel">
<q-item-section> <q-item-section>
<q-item-label caption>{{ t('customer.summary.contactChannel') }}</q-item-label> <q-item-label caption>{{ t('customer.summary.contactChannel') }}</q-item-label>
<q-item-label>{{ customer.contactChannel.name }}</q-item-label> <q-item-label>{{ customer.contactChannel.name }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-list> </q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.fiscalAddress') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.socialName') }}</q-item-label>
<q-item-label>{{ customer.socialName }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.fiscalId') }}</q-item-label>
<q-item-label>{{ customer.fi }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.postcode') }}</q-item-label>
<q-item-label>{{ customer.postcode }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.province') }}</q-item-label>
<q-item-label>{{ customer.province.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.country') }}</q-item-label>
<q-item-label>{{ customer.country.country }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.street') }}</q-item-label>
<q-item-label>{{ customer.street }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.fiscalData') }}
</q-item-label>
<q-item dense>
<q-checkbox
v-model="customer.isEqualizated"
:label="t('customer.summary.isEqualizated')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.isActive"
:label="t('customer.summary.isActive')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasToInvoiceByAddress"
:label="t('customer.summary.invoiceByAddress')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.isTaxDataChecked"
:label="t('customer.summary.verifiedData')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasToInvoice"
:label="t('customer.summary.hasToInvoice')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.isToBeMailed"
:label="t('customer.summary.notifyByEmail')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox v-model="customer.isVies" :label="t('customer.summary.vies')" disable />
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.billingData') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.payMethod') }}</q-item-label>
<q-item-label>{{ customer.payMethod.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.bankAccount') }}</q-item-label>
<q-item-label>{{ customer.iban }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.dueDay') }}</q-item-label>
<q-item-label>{{ customer.dueDay }}</q-item-label>
</q-item-section>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasLcr"
:label="t('customer.summary.hasLcr')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasCoreVnl"
:label="t('customer.summary.hasCoreVnl')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasSepaVnl"
:label="t('customer.summary.hasB2BVnl')"
disable
/>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.consignee') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.addressName') }}</q-item-label>
<q-item-label>{{ customer.defaultAddress.nickname }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.addressCity') }}</q-item-label>
<q-item-label>{{ customer.defaultAddress.city }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.addressStreet') }}</q-item-label>
<q-item-label>{{ customer.defaultAddress.street }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.webAccess') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.username') }}</q-item-label>
<q-item-label>{{ customer.account.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.account.active"
:label="t('customer.summary.webAccess')"
disable
/>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.businessData') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.totalGreuge') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.totalGreuge) }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.mana">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.mana') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.mana.mana) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{
t('customer.summary.priceIncreasingRate')
}}</q-item-label>
<q-item-label>{{ toPercentage(priceIncreasingRate) }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.averageInvoiced">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.averageInvoiced') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.averageInvoiced.invoiced) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.claimRate') }}</q-item-label>
<q-item-label>{{ toPercentage(claimRate) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.financialData') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.risk') }}</q-item-label>
<q-item-label :class="debtWarning">
{{ toCurrency(customer.debt.debt) }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.riskInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.credit') }}</q-item-label>
<q-item-label :class="creditWarning">
{{ toCurrency(customer.credit) }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.creditInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.securedCredit') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.creditInsurance) }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.securedCreditInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.balance') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.sumRisk) || toCurrency(0) }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.balanceInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.balanceDue') }}</q-item-label>
<q-item-label :class="balanceDueWarning">
{{ toCurrency(balanceDue) }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.balanceDueInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item v-if="customer.recovery">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.recoverySince') }}</q-item-label>
<q-item-label>{{ toDate(customer.recovery.started) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</div> </div>
</template> <div class="col">
</q-card> <q-list>
</div> <q-item-label header class="text-h6">
</q-page> {{ t('customer.summary.fiscalAddress') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.socialName') }}</q-item-label>
<q-item-label>{{ customer.socialName }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.fiscalId') }}</q-item-label>
<q-item-label>{{ customer.fi }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.postcode') }}</q-item-label>
<q-item-label>{{ customer.postcode }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.province">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.province') }}</q-item-label>
<q-item-label>{{ customer.province.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.country">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.country') }}</q-item-label>
<q-item-label>{{ customer.country.country }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.street') }}</q-item-label>
<q-item-label>{{ customer.street }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.fiscalData') }}
</q-item-label>
<q-item dense>
<q-checkbox
v-model="customer.isEqualizated"
:label="t('customer.summary.isEqualizated')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.isActive"
:label="t('customer.summary.isActive')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasToInvoiceByAddress"
:label="t('customer.summary.invoiceByAddress')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.isTaxDataChecked"
:label="t('customer.summary.verifiedData')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasToInvoice"
:label="t('customer.summary.hasToInvoice')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.isToBeMailed"
:label="t('customer.summary.notifyByEmail')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox v-model="customer.isVies" :label="t('customer.summary.vies')" disable />
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.billingData') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.payMethod') }}</q-item-label>
<q-item-label>{{ customer.payMethod.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.bankAccount') }}</q-item-label>
<q-item-label>{{ customer.iban }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.dueDay') }}</q-item-label>
<q-item-label>{{ customer.dueDay }}</q-item-label>
</q-item-section>
</q-item>
<q-item dense>
<q-checkbox v-model="customer.hasLcr" :label="t('customer.summary.hasLcr')" disable />
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasCoreVnl"
:label="t('customer.summary.hasCoreVnl')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasSepaVnl"
:label="t('customer.summary.hasB2BVnl')"
disable
/>
</q-item>
</q-list>
</div>
<div class="col" v-if="customer.defaultAddress">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.consignee') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.addressName') }}</q-item-label>
<q-item-label>{{ customer.defaultAddress.nickname }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.addressCity') }}</q-item-label>
<q-item-label>{{ customer.defaultAddress.city }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.addressStreet') }}</q-item-label>
<q-item-label>{{ customer.defaultAddress.street }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col" v-if="customer.account">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.webAccess') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.username') }}</q-item-label>
<q-item-label>{{ customer.account.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.account.active"
:label="t('customer.summary.webAccess')"
disable
/>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.businessData') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.totalGreuge') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.totalGreuge) }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.mana">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.mana') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.mana.mana) }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.claimsRatio">
<q-item-section>
<q-item-label caption>
{{ t('customer.summary.priceIncreasingRate') }}
</q-item-label>
<q-item-label>{{ toPercentage(priceIncreasingRate) }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.averageInvoiced">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.averageInvoiced') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.averageInvoiced.invoiced) }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.claimsRatio">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.claimRate') }}</q-item-label>
<q-item-label>{{ toPercentage(claimRate) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.financialData') }}
</q-item-label>
<q-item v-if="customer.debt">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.risk') }}</q-item-label>
<q-item-label :class="debtWarning">
{{ toCurrency(customer.debt.debt) }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.riskInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.credit') }}</q-item-label>
<q-item-label :class="creditWarning">
{{ toCurrency(customer.credit) }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.creditInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item v-if="customer.creditInsurance">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.securedCredit') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.creditInsurance) }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.securedCreditInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.balance') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.sumRisk) || toCurrency(0) }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.balanceInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item v-if="customer.defaulters">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.balanceDue') }}</q-item-label>
<q-item-label :class="balanceDueWarning">
{{ toCurrency(balanceDue) }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.balanceDueInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item v-if="customer.recovery">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.recoverySince') }}</q-item-label>
<q-item-label>{{ toDate(customer.recovery.started) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</div>
</template>
</q-card>
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.container { .container {
display: flex; display: flex;
@ -491,4 +485,8 @@ const creditWarning = computed(() => {
} }
} }
} }
.q-dialog .summary {
max-width: 1200px;
}
</style> </style>

View File

@ -2,7 +2,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import SmartCard from 'src/components/SmartCard.vue'; import Paginate from 'src/components/Paginate.vue';
import CustomerSummary from './Card/CustomerSummary.vue'; import CustomerSummary from './Card/CustomerSummary.vue';
const router = useRouter(); const router = useRouter();
@ -26,55 +26,64 @@ function showPreview(id) {
<template> <template>
<q-page class="q-pa-md"> <q-page class="q-pa-md">
<smart-card url="/Clients" sort-by="id DESC" @on-navigate="navigate" auto-load> <paginate url="/Clients" sort-by="id DESC" auto-load>
<template #labels="{ row }"> <template #body="{ rows }">
<q-list> <q-card class="card" v-for="row of rows" :key="row.id">
<q-item class="q-pa-none"> <q-item class="q-pa-none items-start cursor-pointer q-hoverable" v-ripple clickable>
<q-item-section> <q-item-section class="q-pa-md" @click="navigate(row.id)">
<q-item-label caption>{{ t('customer.list.email') }}</q-item-label> <div class="text-h6">{{ row.name }}</div>
<q-item-label>{{ row.email }}</q-item-label> <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>{{ 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>{{ row.phone }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-item-section> </q-item-section>
</q-item> <q-separator vertical />
<q-item class="q-pa-none"> <q-card-actions vertical class="justify-between">
<q-item-section> <q-btn color="grey-7" round flat icon="more_vert">
<q-item-label caption>{{ t('customer.list.phone') }}</q-item-label> <q-tooltip>{{ t('customer.list.moreOptions') }}</q-tooltip>
<q-item-label>{{ row.phone }}</q-item-label> <q-menu cover auto-close>
</q-item-section> <q-list>
</q-item> <q-item clickable>
</q-list> <q-item-section avatar>
</template> <q-icon name="add" />
<template #actions="{ row }"> </q-item-section>
<q-btn color="grey-7" round flat icon="more_vert"> <q-item-section>Add a note</q-item-section>
<q-tooltip>{{ t('customer.list.moreOptions') }}</q-tooltip> </q-item>
<q-menu cover auto-close> <q-item clickable>
<q-list> <q-item-section avatar>
<q-item clickable> <q-icon name="history" />
<q-item-section avatar> </q-item-section>
<q-icon name="add" /> <q-item-section>Display customer history</q-item-section>
</q-item-section> </q-item>
<q-item-section>Add a note</q-item-section> </q-list>
</q-item> </q-menu>
<q-item clickable> </q-btn>
<q-item-section avatar>
<q-icon name="history" />
</q-item-section>
<q-item-section>Display customer history</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)"> <q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip> <q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
</q-btn> </q-btn>
<q-btn flat round color="grey-7" icon="preview" @click="showPreview(row.id)"> <q-btn flat round color="grey-7" icon="preview" @click="showPreview(row.id)">
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip> <q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
</q-btn> </q-btn>
<q-btn flat round color="grey-7" icon="vn:ticket"> <q-btn flat round color="grey-7" icon="vn:ticket">
<q-tooltip>{{ t('customer.list.customerOrders') }}</q-tooltip> <q-tooltip>{{ t('customer.list.customerOrders') }}</q-tooltip>
</q-btn> </q-btn>
</q-card-actions>
</q-item>
</q-card>
</template> </template>
</smart-card> </paginate>
</q-page> </q-page>
<q-dialog v-model="preview.shown"> <q-dialog v-model="preview.shown">
<customer-summary :customer-id="preview.data.customerId" /> <customer-summary :customer-id="preview.data.customerId" />

View File

@ -79,81 +79,75 @@ async function getVideoList(expeditionId, timed) {
</script> </script>
<template> <template>
<q-layout view="hhh lpr ffr" class="fit"> <q-drawer show-if-above side="right">
<q-drawer show-if-above side="right" bordered> <q-scroll-area class="fit">
<q-scroll-area class="fit"> <q-list bordered separator style="max-width: 318px">
<q-list bordered separator style="max-width: 318px"> <q-item v-if="lastExpedition && videoList.length">
<q-item v-if="lastExpedition && videoList.length"> <q-item-section>
<q-item-section> <q-item-label class="text-h6">
<q-item-label class="text-h6"> {{ t('ticket.boxing.selectTime') }} ({{ time.min }}-{{ time.max }})
{{ t('ticket.boxing.selectTime') }} ({{ time.min }}-{{ time.max }}) </q-item-label>
</q-item-label> <q-range
<q-range v-model="time"
v-model="time" @change="getVideoList(lastExpedition, time)"
@change="getVideoList(lastExpedition, time)" :min="0"
:min="0" :max="24"
:max="24" :step="1"
:step="1" :left-label-value="time.min + ':00'"
:left-label-value="time.min + ':00'" :right-label-value="time.max + ':00'"
:right-label-value="time.max + ':00'" label
label markers
markers snap
snap color="orange"
color="orange" />
/> </q-item-section>
</q-item-section> </q-item>
</q-item> <q-item v-if="lastExpedition && videoList.length">
<q-item v-if="lastExpedition && videoList.length"> <q-item-section>
<q-item-section> <q-select
<q-select color="orange"
color="orange" v-model="slide"
v-model="slide" :options="videoList"
:options="videoList" :label="t('ticket.boxing.selectVideo')"
:label="t('ticket.boxing.selectVideo')" emit-value
emit-value map-options
map-options >
> <template #prepend>
<template #prepend> <q-icon name="schedule" />
<q-icon name="schedule" /> </template>
</template> </q-select>
</q-select> </q-item-section>
</q-item-section> </q-item>
</q-item> <q-item
<q-item v-for="expedition in expeditions"
v-for="expedition in expeditions" :key="expedition.id"
:key="expedition.id" @click="getVideoList(expedition.id)"
@click="getVideoList(expedition.id)" clickable
clickable v-ripple
v-ripple >
> <q-item-section>
<q-item-section> <q-item-label class="text-h6">#{{ expedition.id }}</q-item-label>
<q-item-label class="text-h6">#{{ expedition.id }}</q-item-label> </q-item-section>
</q-item-section> <q-item-section>
<q-item-section> <q-item-label caption>{{ t('ticket.boxing.created') }}</q-item-label>
<q-item-label caption>{{ t('ticket.boxing.created') }}</q-item-label> <q-item-label>
<q-item-label> {{ date.formatDate(expedition.created, 'YYYY-MM-DD HH:mm:ss') }}
{{ date.formatDate(expedition.created, 'YYYY-MM-DD HH:mm:ss') }} </q-item-label>
</q-item-label> <q-item-label caption>{{ t('ticket.boxing.item') }}</q-item-label>
<q-item-label caption>{{ t('ticket.boxing.item') }}</q-item-label> <q-item-label>{{ expedition.packagingItemFk }}</q-item-label>
<q-item-label>{{ expedition.packagingItemFk }}</q-item-label> <q-item-label caption>{{ t('ticket.boxing.worker') }}</q-item-label>
<q-item-label caption>{{ t('ticket.boxing.worker') }}</q-item-label> <q-item-label>{{ expedition.userName }}</q-item-label>
<q-item-label>{{ expedition.userName }}</q-item-label> </q-item-section>
</q-item-section> </q-item>
</q-item> </q-list>
</q-list> </q-scroll-area>
</q-scroll-area> </q-drawer>
</q-drawer>
<q-page-container> <q-card>
<q-page> <q-carousel animated v-model="slide" height="max-content">
<q-card> <q-carousel-slide v-for="video in videoList" :key="video.value" :name="video.value">
<q-carousel animated v-model="slide" height="max-content"> <q-video :src="video.url" :ratio="16 / 9" />
<q-carousel-slide v-for="video in videoList" :key="video.value" :name="video.value"> </q-carousel-slide>
<q-video :src="video.url" :ratio="16 / 9" /> </q-carousel>
</q-carousel-slide> </q-card>
</q-carousel>
</q-card>
</q-page>
</q-page-container>
</q-layout>
</template> </template>

View File

@ -147,17 +147,19 @@ function stateColor(state) {
<q-separator /> <q-separator />
<q-list> <q-list>
<!-- <q-item :to="{ name: 'TicketBasicData' }" clickable v-ripple> <q-item :to="{ name: 'TicketBoxing' }" clickable v-ripple>
<q-item-section avatar> <q-item-section avatar>
<q-icon name="vn:settings" /> <q-icon name="vn:package" />
</q-item-section> </q-item-section>
<q-item-section>{{ t('ticket.pageTitles.basicData') }}</q-item-section> <q-item-section>{{ t('ticket.pageTitles.boxing') }}</q-item-section>
</q-item> --> </q-item>
</q-list> </q-list>
</q-scroll-area> </q-drawer </q-scroll-area>
>--> </q-drawer>
<q-page-container> <q-page-container>
<router-view></router-view> <q-page class="q-pa-md">
<router-view></router-view>
</q-page>
</q-page-container> </q-page-container>
</template> </template>

View File

@ -2,9 +2,9 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import SmartCard from 'src/components/SmartCard.vue'; import Paginate from 'src/components/Paginate.vue';
import { toDate, toCurrency } from 'src/filters/index'; import { toDate, toCurrency } from 'src/filters/index';
// import CustomerSummary from './Card/CustomerSummary.vue'; // import TicketSummary from './Card/TicketSummary.vue';
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
@ -62,77 +62,86 @@ function showPreview(id) {
<template> <template>
<q-page class="q-pa-md"> <q-page class="q-pa-md">
<smart-card url="/Tickets" :filter="filter" sort-by="id DESC" @on-navigate="navigate" auto-load> <paginate url="/Tickets" :filter="filter" sort-by="id DESC" auto-load>
<template #labels="{ row }"> <template #body="{ rows }">
<q-list> <q-card class="card" v-for="row of rows" :key="row.id">
<q-item class="q-pa-none"> <q-item class="q-pa-none items-start cursor-pointer q-hoverable" v-ripple clickable>
<q-item-section> <q-item-section class="q-pa-md" @click="navigate(row.id)">
<q-item-label caption>{{ t('ticket.list.nickname') }}</q-item-label> <div class="text-h6">{{ row.name }}</div>
<q-item-label>{{ row.nickname }}</q-item-label> <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>{{ 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>
<q-chip :color="stateColor(row.ticketState)" dense>
{{ row.ticketState.state.name }}
</q-chip>
</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-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-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-section>
<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-item-section>
<q-item-section> <q-separator vertical />
<q-item-label caption>{{ t('ticket.list.state') }}</q-item-label> <q-card-actions vertical class="justify-between">
<q-item-label> <q-btn color="grey-7" round flat icon="more_vert">
<q-chip :color="stateColor(row.ticketState)" dense> <q-tooltip>{{ t('customer.list.moreOptions') }}</q-tooltip>
{{ row.ticketState.state.name }} <q-menu cover auto-close>
</q-chip> <q-list>
</q-item-label> <q-item clickable>
</q-item-section> <q-item-section avatar>
</q-item> <q-icon name="add" />
<q-item class="q-pa-none"> </q-item-section>
<q-item-section> <q-item-section>Add a note</q-item-section>
<q-item-label caption>{{ t('ticket.list.shipped') }}</q-item-label> </q-item>
<q-item-label>{{ toDate(row.shipped) }}</q-item-label> <q-item clickable>
</q-item-section> <q-item-section avatar>
<q-item-section> <q-icon name="history" />
<q-item-label caption>{{ t('ticket.list.landed') }}</q-item-label> </q-item-section>
<q-item-label>{{ toDate(row.landed) }}</q-item-label> <q-item-section>Display customer history</q-item-section>
</q-item-section> </q-item>
</q-item> </q-list>
<q-item class="q-pa-none"> </q-menu>
<q-item-section v-if="row.client.salesPersonUser"> </q-btn>
<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-section>
<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>
</template>
<template #actions="{ row }">
<q-btn color="grey-7" round flat icon="more_vert">
<q-tooltip>{{ t('customer.list.moreOptions') }}</q-tooltip>
<q-menu cover auto-close>
<q-list>
<q-item clickable>
<q-item-section avatar>
<q-icon name="add" />
</q-item-section>
<q-item-section>Add a note</q-item-section>
</q-item>
<q-item clickable>
<q-item-section avatar>
<q-icon name="history" />
</q-item-section>
<q-item-section>Display customer history</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)"> <q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip> <q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
</q-btn> </q-btn>
<q-btn flat round color="grey-7" icon="preview" @click="showPreview(row.id)"> <q-btn flat round color="grey-7" icon="preview" @click="showPreview(row.id)">
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip> <q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
</q-btn> </q-btn>
<q-btn flat round color="grey-7" icon="vn:ticket"> <q-btn flat round color="grey-7" icon="vn:ticket">
<q-tooltip>{{ t('customer.list.customerOrders') }}</q-tooltip> <q-tooltip>{{ t('customer.list.customerOrders') }}</q-tooltip>
</q-btn> </q-btn>
</q-card-actions>
</q-item>
</q-card>
</template> </template>
</smart-card> </paginate>
</q-page> </q-page>
<!-- <q-dialog v-model="preview.shown"> <!-- <q-dialog v-model="preview.shown">
<customer-summary :customer-id="preview.data.customerId" /> <customer-summary :customer-id="preview.data.customerId" />

View File

@ -34,15 +34,6 @@ export default {
roles: ['claimManager'] roles: ['claimManager']
}, },
component: () => import('src/pages/Claim/ClaimRmaList.vue'), component: () => import('src/pages/Claim/ClaimRmaList.vue'),
},
{
name: 'ClaimCreate',
path: 'create',
meta: {
title: 'createClaim',
icon: 'vn:addperson',
},
component: () => import('src/pages/Claim/ClaimCreate.vue'),
} }
] ]
}, },
@ -76,8 +67,7 @@ export default {
title: 'rma', title: 'rma',
roles: ['claimManager'] roles: ['claimManager']
}, },
component: () => import('src/pages/Claim/Card/ClaimRma.vue'), component: () => import('src/pages/Claim/Card/ClaimRma.vue')
props: { claim: true }
} }
] ]
}, },