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

This commit is contained in:
Jorge Penadés 2023-12-07 16:25:13 +01:00
commit 479c782492
49 changed files with 1361 additions and 462 deletions

View File

@ -13,5 +13,6 @@
], ],
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
} },
"cSpell.words": ["axios"]
} }

View File

@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2352.01] - 2023-12-28
### Added
- (carros) => Se añade contador de carros. #6545
- (Reclamaciones) => Se añade la sección para hacer acciones sobre una reclamación. #5654
### Changed
### Fixed
- (Reclamaciones) => Se corrige el color de la barra según el tema y el evento de actualziar cantidades #6334
## [2253.01] - 2023-01-05 ## [2253.01] - 2023-01-05
### Added ### Added

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "23.36.01", "version": "23.52.01",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",
@ -15,7 +15,7 @@
"test:unit:ci": "vitest run" "test:unit:ci": "vitest run"
}, },
"dependencies": { "dependencies": {
"@quasar/cli": "^2.2.1", "@quasar/cli": "^2.3.0",
"@quasar/extras": "^1.16.4", "@quasar/extras": "^1.16.4",
"axios": "^1.4.0", "axios": "^1.4.0",
"chromium": "^3.0.3", "chromium": "^3.0.3",

View File

@ -66,7 +66,9 @@ module.exports = configure(function (/* ctx */) {
// publicPath: '/', // publicPath: '/',
// analyze: true, // analyze: true,
// env: {}, // env: {},
// rawDefine: {} rawDefine: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
},
// ignorePublicFolder: true, // ignorePublicFolder: true,
// minify: false, // minify: false,
// polyfillModulePreload: true, // polyfillModulePreload: true,
@ -89,11 +91,12 @@ module.exports = configure(function (/* ctx */) {
vitePlugins: [ vitePlugins: [
[ [
VueI18nPlugin, VueI18nPlugin({
runtimeOnly: false
}),
{ {
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false` // if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
// compositionOnly: false, // compositionOnly: false,
// you need to set i18n resource including paths ! // you need to set i18n resource including paths !
include: path.resolve(__dirname, './src/i18n/**'), include: path.resolve(__dirname, './src/i18n/**'),
}, },

View File

@ -89,6 +89,7 @@ async function fetch(data) {
watch(formData, () => (hasChanges.value = true), { deep: true }); watch(formData, () => (hasChanges.value = true), { deep: true });
emit('onFetch', data); emit('onFetch', data);
return data;
} }
async function reset() { async function reset() {
@ -268,7 +269,7 @@ watch(formUrl, async () => {
</VnPaginate> </VnPaginate>
<SkeletonTable v-if="!formData" /> <SkeletonTable v-if="!formData" />
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()"> <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<QBtnGroup push class="q-gutter-x-sm"> <QBtnGroup push style="column-gap: 10px">
<slot name="moreBeforeActions" /> <slot name="moreBeforeActions" />
<QBtn <QBtn
:label="tMobile('globals.remove')" :label="tMobile('globals.remove')"

View File

@ -25,27 +25,17 @@ const pinnedModulesRef = ref();
<template> <template>
<QHeader class="bg-dark" color="white" elevated> <QHeader class="bg-dark" color="white" elevated>
<QToolbar class="q-py-sm q-px-md"> <QToolbar
<QBtn class="q-py-sm q-px-md"
@click="stateStore.toggleLeftDrawer()" :class="{ 'q-gutter-x-sm': !quasar.platform.is.mobile }"
icon="menu" >
class="q-mr-sm" <QBtn @click="stateStore.toggleLeftDrawer()" icon="menu" round dense flat>
round
dense
flat
>
<QTooltip bottom anchor="bottom right"> <QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }} {{ t('globals.collapseMenu') }}
</QTooltip> </QTooltip>
</QBtn> </QBtn>
<RouterLink to="/"> <RouterLink to="/">
<QBtn <QBtn color="primary" flat round v-if="!quasar.platform.is.mobile">
class="q-ml-xs"
color="primary"
flat
round
v-if="!quasar.platform.is.mobile"
>
<QAvatar square size="md"> <QAvatar square size="md">
<QImg <QImg
src="~/assets/salix_icon.svg" src="~/assets/salix_icon.svg"

View File

@ -4,7 +4,7 @@ const emit = defineEmits(['update:modelValue', 'update:options']);
const $props = defineProps({ const $props = defineProps({
modelValue: { modelValue: {
type: [String, Number], type: [String, Number, Object],
default: null, default: null,
}, },
options: { options: {
@ -15,6 +15,10 @@ const $props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
isClearable: {
type: Boolean,
default: true,
},
}); });
const { optionLabel, options } = toRefs($props); const { optionLabel, options } = toRefs($props);
const myOptions = ref([]); const myOptions = ref([]);
@ -81,11 +85,10 @@ const value = computed({
map-options map-options
use-input use-input
@filter="filterHandler" @filter="filterHandler"
hide-selected
fill-input fill-input
ref="vnSelectRef" ref="vnSelectRef"
> >
<template #append> <template v-if="isClearable" #append>
<QIcon name="close" @click.stop="value = null" class="cursor-pointer" /> <QIcon name="close" @click.stop="value = null" class="cursor-pointer" />
</template> </template>
<template v-for="(_, slotName) in $slots" #[slotName]="slotData"> <template v-for="(_, slotName) in $slots" #[slotName]="slotData">

View File

@ -1,8 +1,9 @@
<script setup> <script setup>
import { onMounted, useSlots, ref, watch } from 'vue'; import { onMounted, useSlots, ref, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue'; import SkeletonDescriptor from 'components/ui/SkeletonDescriptor.vue';
import { useArrayData } from 'composables/useArrayData';
const $props = defineProps({ const $props = defineProps({
url: { url: {
@ -25,33 +26,37 @@ const $props = defineProps({
type: Number, type: Number,
default: 0, default: 0,
}, },
dataKey: {
type: String,
default: '',
},
}); });
const slots = useSlots(); const slots = useSlots();
const { t } = useI18n(); const { t } = useI18n();
const entity = ref(); const entity = computed(() => useArrayData($props.dataKey).store.data);
onMounted(async () => { onMounted(async () => {
await fetch(); await getData();
watch(
() => $props.url,
async (newUrl, lastUrl) => {
if (newUrl == lastUrl) return;
entity.value = null;
await getData();
}
);
}); });
const emit = defineEmits(['onFetch']); async function getData() {
const arrayData = useArrayData($props.dataKey, {
async function fetch() { url: $props.url,
const params = {}; filter: $props.filter,
skip: 0,
if ($props.filter) params.filter = JSON.stringify($props.filter); });
const { data } = await arrayData.fetch({ append: false });
const { data } = await axios.get($props.url, { params });
entity.value = data;
emit('onFetch', data); emit('onFetch', data);
} }
const emit = defineEmits(['onFetch']);
watch($props, async () => {
entity.value = null;
await fetch();
});
</script> </script>
<template> <template>

View File

@ -32,7 +32,7 @@ const $props = defineProps({
gap: 2%; gap: 2%;
width: 50%; width: 50%;
.label { .label {
width: 30%; width: 35%;
color: var(--vn-label); color: var(--vn-label);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@ -41,15 +41,11 @@ onMounted(() => {
const isLoading = ref(false); const isLoading = ref(false);
async function search() { async function search() {
for (const param in userParams.value) {
if (userParams.value[param] === '' || userParams.value[param] === null) {
delete userParams.value[param];
delete store.userParams[param];
}
}
const params = { ...userParams.value };
isLoading.value = true; isLoading.value = true;
await arrayData.addFilter({ params }); const params = { ...userParams.value };
const { params: newParams } = await arrayData.addFilter({ params });
userParams.value = newParams;
if (!props.showAll && !Object.values(params).length) store.data = []; if (!props.showAll && !Object.values(params).length) store.data = [];
isLoading.value = false; isLoading.value = false;
@ -78,10 +74,11 @@ async function clearFilters() {
const tags = computed(() => { const tags = computed(() => {
const params = []; const params = [];
for (const param in store.userParams) { for (const param in userParams.value) {
if (!userParams.value[param]) continue;
params.push({ params.push({
label: param, label: param,
value: store.userParams[param], value: userParams.value[param],
}); });
} }
@ -89,8 +86,7 @@ const tags = computed(() => {
}); });
async function remove(key) { async function remove(key) {
delete userParams.value[key]; userParams.value[key] = null;
delete store.userParams[key];
await search(); await search();
} }
@ -200,7 +196,7 @@ function formatValue(value) {
</template> </template>
<i18n> <i18n>
es: es:
No filters applied: No se han aplicado filtros No filters applied: No se han aplicado filtros
Applied filters: Filtros aplicados Applied filters: Filtros aplicados
Remove filters: Eliminar filtros Remove filters: Eliminar filtros

View File

@ -0,0 +1,22 @@
<script setup>
import { useI18n } from 'vue-i18n';
const props = defineProps({
phoneNumber: { type: [String, Number], default: null },
});
const { t } = useI18n();
</script>
<template>
<QBtn
v-if="props.phoneNumber"
flat
round
icon="phone"
size="sm"
color="primary"
padding="none"
:href="`sip:${props.phoneNumber}`"
:title="t('globals.microsip')"
@click.stop
/>
</template>
<style scoped></style>

View File

@ -50,6 +50,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
exprBuilder: {
type: Function,
default: null,
},
}); });
const emit = defineEmits(['onFetch', 'onPaginate']); const emit = defineEmits(['onFetch', 'onPaginate']);
@ -68,6 +72,7 @@ const arrayData = useArrayData(props.dataKey, {
limit: props.limit, limit: props.limit,
order: props.order, order: props.order,
userParams: props.userParams, userParams: props.userParams,
exprBuilder: props.exprBuilder,
}); });
const store = arrayData.store; const store = arrayData.store;

View File

@ -2,6 +2,7 @@ import { onMounted, ref, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import { useArrayDataStore } from 'stores/useArrayDataStore'; import { useArrayDataStore } from 'stores/useArrayDataStore';
import { buildFilter } from 'filters/filterPanel';
const arrayDataStore = useArrayDataStore(); const arrayDataStore = useArrayDataStore();
@ -29,6 +30,10 @@ export function useArrayData(key, userOptions) {
} }
}); });
if (key && userOptions) {
setOptions();
}
function setOptions() { function setOptions() {
const allowedOptions = [ const allowedOptions = [
'url', 'url',
@ -39,10 +44,11 @@ export function useArrayData(key, userOptions) {
'skip', 'skip',
'userParams', 'userParams',
'userFilter', 'userFilter',
'exprBuilder',
]; ];
if (typeof userOptions === 'object') { if (typeof userOptions === 'object') {
for (const option in userOptions) { for (const option in userOptions) {
const isEmpty = userOptions[option] == null || userOptions[option] == ''; const isEmpty = userOptions[option] == null || userOptions[option] === '';
if (isEmpty || !allowedOptions.includes(option)) continue; if (isEmpty || !allowedOptions.includes(option)) continue;
if (Object.prototype.hasOwnProperty.call(store, option)) { if (Object.prototype.hasOwnProperty.call(store, option)) {
@ -64,16 +70,27 @@ export function useArrayData(key, userOptions) {
skip: store.skip, skip: store.skip,
}; };
Object.assign(filter, store.userFilter); let exprFilter;
Object.assign(store.filter, filter); let userParams = { ...store.userParams };
if (store?.exprBuilder) {
const where = buildFilter(userParams, (param, value) => {
const res = store.exprBuilder(param, value);
if (res) delete userParams[param];
return res;
});
exprFilter = where ? { where } : null;
}
Object.assign(filter, store.userFilter, exprFilter);
Object.assign(store.filter, filter);
const params = { const params = {
filter: JSON.stringify(store.filter), filter: JSON.stringify(store.filter),
}; };
Object.assign(params, store.userParams); Object.assign(params, userParams);
store.isLoading = true; store.isLoading = true;
const response = await axios.get(store.url, { const response = await axios.get(store.url, {
signal: canceller.signal, signal: canceller.signal,
params, params,
@ -97,6 +114,7 @@ export function useArrayData(key, userOptions) {
store.isLoading = false; store.isLoading = false;
canceller = null; canceller = null;
return response;
} }
function destroy() { function destroy() {
@ -121,9 +139,30 @@ export function useArrayData(key, userOptions) {
async function addFilter({ filter, params }) { async function addFilter({ filter, params }) {
if (filter) store.userFilter = Object.assign(store.userFilter, filter); if (filter) store.userFilter = Object.assign(store.userFilter, filter);
if (params) store.userParams = Object.assign(store.userParams, params);
let userParams = Object.assign({}, store.userParams, params);
userParams = sanitizerParams(userParams, store?.exprBuilder);
store.userParams = userParams;
await fetch({ append: false }); await fetch({ append: false });
return { filter, params };
}
function sanitizerParams(params) {
for (const param in params) {
if (params[param] === '' || params[param] === null) {
delete store.userParams[param];
delete params[param];
if (store.filter?.where) {
delete store.filter.where[Object.keys(store?.exprBuilder(param))[0]];
if (Object.keys(store.filter.where).length === 0) {
delete store.filter.where;
}
}
}
}
return params;
} }
async function loadMore() { async function loadMore() {
@ -147,10 +186,11 @@ export function useArrayData(key, userOptions) {
if (store.userParams && Object.keys(store.userParams).length !== 0) if (store.userParams && Object.keys(store.userParams).length !== 0)
query.params = JSON.stringify(store.userParams); query.params = JSON.stringify(store.userParams);
router.replace({ if (router)
path: route.path, router.replace({
query: query, path: route.path,
}); query: query,
});
} }
const totalRows = computed(() => (store.data && store.data.length) || 0); const totalRows = computed(() => (store.data && store.data.length) || 0);

View File

@ -45,3 +45,9 @@ body.body--dark {
.bg-vn-dark { .bg-vn-dark {
background-color: var(--vn-dark); background-color: var(--vn-dark);
} }
.vn-card {
background-color: var(--vn-gray);
color: var(--vn-text);
border-radius: 8px;
}

View File

@ -0,0 +1,94 @@
/**
* Passes a loopback fields filter to an object.
*
* @param {Object} fields The fields object or array
* @return {Object} The fields as object
*/
function fieldsToObject(fields) {
let fieldsObj = {};
if (Array.isArray(fields)) {
for (let field of fields) fieldsObj[field] = true;
} else if (typeof fields == 'object') {
for (let field in fields) {
if (fields[field]) fieldsObj[field] = true;
}
}
return fieldsObj;
}
/**
* Merges two loopback fields filters.
*
* @param {Object|Array} src The source fields
* @param {Object|Array} dst The destination fields
* @return {Array} The merged fields as an array
*/
function mergeFields(src, dst) {
let fields = {};
Object.assign(fields, fieldsToObject(src), fieldsToObject(dst));
return Object.keys(fields);
}
/**
* Merges two loopback where filters.
*
* @param {Object|Array} src The source where
* @param {Object|Array} dst The destination where
* @return {Array} The merged wheres
*/
function mergeWhere(src, dst) {
let and = [];
if (src) and.push(src);
if (dst) and.push(dst);
return simplifyOperation(and, 'and');
}
/**
* Merges two loopback filters returning the merged filter.
*
* @param {Object} src The source filter
* @param {Object} dst The destination filter
* @return {Object} The result filter
*/
function mergeFilters(src, dst) {
let res = Object.assign({}, dst);
if (!src) return res;
if (src.fields) res.fields = mergeFields(src.fields, res.fields);
if (src.where) res.where = mergeWhere(res.where, src.where);
if (src.include) res.include = src.include;
if (src.order) res.order = src.order;
if (src.limit) res.limit = src.limit;
if (src.offset) res.offset = src.offset;
if (src.skip) res.skip = src.skip;
return res;
}
function simplifyOperation(operation, operator) {
switch (operation.length) {
case 0:
return undefined;
case 1:
return operation[0];
default:
return { [operator]: operation };
}
}
function buildFilter(params, builderFunc) {
let and = [];
for (let param in params) {
let value = params[param];
if (value == null) continue;
let expr = builderFunc(param, value);
if (expr) and.push(expr);
}
return simplifyOperation(and, 'and');
}
export { fieldsToObject, mergeFields, mergeWhere, mergeFilters, buildFilter };

View File

@ -45,6 +45,7 @@ export default {
today: 'Today', today: 'Today',
yesterday: 'Yesterday', yesterday: 'Yesterday',
dateFormat: 'en-GB', dateFormat: 'en-GB',
microsip: 'Open in MicroSIP',
noSelectedRows: `You don't have any line selected`, noSelectedRows: `You don't have any line selected`,
}, },
errors: { errors: {
@ -283,6 +284,7 @@ export default {
development: 'Development', development: 'Development',
log: 'Audit logs', log: 'Audit logs',
notes: 'Notes', notes: 'Notes',
action: 'Action',
}, },
list: { list: {
customer: 'Customer', customer: 'Customer',
@ -457,6 +459,7 @@ export default {
typesList: 'Types List', typesList: 'Types List',
typeCreate: 'Create type', typeCreate: 'Create type',
typeEdit: 'Edit type', typeEdit: 'Edit type',
wagonCounter: 'Trolley counter',
}, },
type: { type: {
name: 'Name', name: 'Name',

View File

@ -46,6 +46,7 @@ export default {
yesterday: 'Ayer', yesterday: 'Ayer',
dateFormat: 'es-ES', dateFormat: 'es-ES',
noSelectedRows: `No tienes ninguna línea seleccionada`, noSelectedRows: `No tienes ninguna línea seleccionada`,
microsip: 'Abrir en MicroSIP',
}, },
errors: { errors: {
statusUnauthorized: 'Acceso denegado', statusUnauthorized: 'Acceso denegado',
@ -282,6 +283,7 @@ export default {
photos: 'Fotos', photos: 'Fotos',
log: 'Registros de auditoría', log: 'Registros de auditoría',
notes: 'Notas', notes: 'Notas',
action: 'Acción',
}, },
list: { list: {
customer: 'Cliente', customer: 'Cliente',
@ -457,6 +459,7 @@ export default {
typesList: 'Listado tipos', typesList: 'Listado tipos',
typeCreate: 'Crear tipo', typeCreate: 'Crear tipo',
typeEdit: 'Editar tipo', typeEdit: 'Editar tipo',
wagonCounter: 'Contador de carros',
}, },
type: { type: {
name: 'Nombre', name: 'Nombre',

View File

@ -0,0 +1,518 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { useStateStore } from 'src/stores/useStateStore';
import { toDate, toPercentage, toCurrency } from 'filters/index';
import { tMobile } from 'src/composables/tMobile';
import CrudModel from 'src/components/CrudModel.vue';
import FetchData from 'src/components/FetchData.vue';
import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import { useArrayData } from 'composables/useArrayData';
const { t } = useI18n();
const quasar = useQuasar();
const route = useRoute();
const router = useRouter();
const stateStore = computed(() => useStateStore());
const claim = ref(null);
const claimRef = ref();
const claimId = route.params.id;
const dialogDestination = ref(false);
const claimDestinationFk = ref(null);
const resolvedStateId = ref(null);
const claimActionsForm = ref();
const rows = ref([]);
const selectedRows = ref([]);
const destinationTypes = ref([]);
const totalClaimed = ref(null);
const DEFAULT_MAX_RESPONSABILITY = 5;
const DEFAULT_MIN_RESPONSABILITY = 1;
const arrayData = useArrayData('claimData');
const marker_labels = [
{ value: DEFAULT_MIN_RESPONSABILITY, label: t('claim.summary.company') },
{ value: DEFAULT_MAX_RESPONSABILITY, label: t('claim.summary.person') },
];
const columns = computed(() => [
{
name: 'Id',
label: t('Id item'),
field: (row) => row.itemFk,
},
{
name: 'ticket',
label: t('Ticket'),
field: (row) => row.ticketFk,
align: 'center',
},
{
name: 'destination',
label: t('Destination'),
field: (row) => row.claimDestinationFk,
align: 'left',
},
{
name: 'Landed',
label: t('Landed'),
field: (row) => toDate(row.landed),
},
{
name: 'quantity',
label: t('Quantity'),
field: (row) => row.quantity,
},
{
name: 'concept',
label: t('Description'),
field: (row) => row.concept,
align: 'left',
},
{
name: 'price',
label: t('Price'),
field: (row) => row.price,
format: (value) => value,
align: 'center',
},
{
name: 'discount',
label: t('Discount'),
field: (row) => row.discount,
format: (value) => toPercentage(value / 100),
align: 'left',
},
{
name: 'total',
label: t('Total'),
field: (row) => row.total,
format: (value) => value,
align: 'center',
},
{
name: 'delete',
},
]);
onMounted(() => {
getTotal();
});
function setData(data) {
rows.value = data;
getTotal();
}
function getTotal() {
if (rows.value.length) {
totalClaimed.value = rows.value.reduce((total, row) => total + row.total, 0);
}
}
async function updateDestinations(claimDestinationFk) {
await updateDestination(claimDestinationFk, selectedRows.value, { reload: true });
}
async function updateDestination(claimDestinationFk, row, options = {}) {
if (claimDestinationFk) {
await axios.post('Claims/updateClaimDestination', {
claimDestinationFk,
rows: Array.isArray(row) ? row : [row],
});
options.reload && claimActionsForm.value.reload();
}
}
async function regularizeClaim() {
const query = `Claims/${claimId}/regularizeClaim`;
await axios.post(query);
if (claim.value.responsibility >= Math.ceil(DEFAULT_MAX_RESPONSABILITY) / 2) {
await claimRef.value.fetch();
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('confirmGreuges'),
message: t('confirmGreugesMessage'),
},
})
.onOk(async () => await onUpdateGreugeAccept());
} else {
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
}
await arrayData.fetch({ append: false });
}
async function updateGreuge(greuges) {
const { data } = await axios.post(`Greuges`, greuges);
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
return data;
}
async function onUpdateGreugeAccept() {
const greugeTypeFreightId = await getGreugeTypeId();
const freightPickUpPrice = await getGreugeConfig();
await updateGreuge({
clientFk: claim.value.clientFk,
description: `${t('ClaimGreugeDescription')} ${claimId}`.toUpperCase(),
amount: freightPickUpPrice,
greugeTypeFk: greugeTypeFreightId,
ticketFk: claim.value.ticketFk,
});
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
}
async function getGreugeTypeId() {
const params = { filter: { where: { code: 'freightPickUp' } } };
const query = `GreugeTypes/findOne`;
const { data } = await axios.get(query, { params });
return data.id;
}
async function getGreugeConfig() {
const query = `GreugeConfigs/findOne`;
const { data } = await axios.get(query);
return data.freightPickUpPrice;
}
async function save(data) {
const query = `Claims/${claimId}/updateClaimAction`;
await axios.patch(query, data);
}
async function importToNewRefundTicket() {
const query = `ClaimBeginnings/${claimId}/importToNewRefundTicket`;
await axios.post(query);
claimActionsForm.value.reload();
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
});
}
</script>
<template>
<FetchData
ref="claimRef"
:url="`Claims/${claimId}`"
@on-fetch="(data) => (claim = data)"
auto-load
/>
<FetchData
url="ClaimStates/findOne"
@on-fetch="(data) => (resolvedStateId = data.id)"
auto-load
:where="{ code: 'resolved' }"
/>
<FetchData
url="ClaimDestinations"
auto-load
@on-fetch="(data) => (destinationTypes = data)"
/>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDrawer
v-model="stateStore.rightDrawer"
side="right"
:width="300"
show-if-above
v-if="claim"
>
<QCard class="totalClaim vn-card q-my-md q-pa-sm">
{{ `${t('Total claimed')}: ${toCurrency(totalClaimed)}` }}
</QCard>
<QCard class="vn-card q-mb-md q-pa-sm">
<QItem class="justify-between">
<QItemLabel class="slider-container">
<p class="text-primary">
{{ t('claim.summary.actions') }}
</p>
<QSlider
class="responsibility { 'background-color:primary': quasar.platform.is.mobile }"
v-model="claim.responsibility"
:label-value="t('claim.summary.responsibility')"
@change="(value) => save({ responsibility: value })"
label-always
color="primary"
markers
:marker-labels="marker_labels"
:min="DEFAULT_MIN_RESPONSABILITY"
:max="DEFAULT_MAX_RESPONSABILITY"
/>
</QItemLabel>
</QItem>
</QCard>
<QItemLabel class="mana q-mb-md">
<QCheckbox
v-model="claim.isChargedToMana"
@update:model-value="(value) => save({ isChargedToMana: value })"
/>
<span>{{ t('mana') }}</span>
</QItemLabel>
</QDrawer>
<Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()"> </Teleport>
<CrudModel
v-if="claim"
data-key="ClaimEnds"
url="ClaimEnds/filter"
save-url="ClaimEnds/crud"
ref="claimActionsForm"
v-model:selected="selectedRows"
:filter="{ where: { claimFk: claimId } }"
:default-remove="true"
:default-save="false"
:default-reset="false"
@on-fetch="setData"
auto-load
>
<template #body>
<QTable
:columns="columns"
:rows="rows"
:dense="$q.screen.lt.md"
row-key="id"
selection="multiple"
v-model:selected="selectedRows"
:grid="$q.screen.lt.md"
>
<template #body-cell-ticket="{ value }">
<QTd align="center">
<span class="link">
{{ value }}
<TicketDescriptorProxy :id="value" />
</span>
</QTd>
</template>
<template #body-cell-destination="{ row }">
<QTd>
<VnSelectFilter
v-model="row.claimDestinationFk"
:options="destinationTypes"
option-label="description"
option-value="id"
:autofocus="true"
dense
input-debounce="0"
hide-selected
@update:model-value="(value) => updateDestination(value, row)"
/>
</QTd>
</template>
<template #body-cell-price="{ value }">
<QTd align="center">
{{ toCurrency(value) }}
</QTd>
</template>
<template #body-cell-total="{ value }">
<QTd align="center">
{{ toCurrency(value) }}
</QTd>
</template>
<!-- View for grid mode -->
<template #item="props">
<div class="q-mb-md col-12 grid-style-transition">
<QCard>
<QCardSection class="row justify-between">
<QCheckbox v-model="props.selected" />
<QBtn color="primary" icon="delete" flat round />
</QCardSection>
<QSeparator inset />
<QList dense>
<QItem v-for="column of props.cols" :key="column.name">
<QItemSection>
<QItemLabel caption>
{{ column.label }}
</QItemLabel>
</QItemSection>
<QItemSection side>
<QItemLabel v-if="column.name === 'destination'">
{{ column.value.description }}
</QItemLabel>
<QItemLabel v-else>
{{ column.value }}
</QItemLabel>
</QItemSection>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
</template>
<template #moreBeforeActions>
<QBtn
color="primary"
text-color="white"
:unelevated="true"
:label="tMobile('Regularize')"
:title="t('Regularize')"
icon="check"
@click="regularizeClaim"
:disable="claim.claimStateFk == resolvedStateId"
/>
<QBtn
color="primary"
text-color="white"
:unelevated="true"
:disable="!selectedRows.length"
:label="tMobile('Change destination')"
:title="t('Change destination')"
icon="swap_horiz"
@click="dialogDestination = !dialogDestination"
/>
<QBtn
color="primary"
text-color="white"
:unelevated="true"
:label="tMobile('Import claim')"
:title="t('Import claim')"
icon="Upload"
@click="importToNewRefundTicket"
:disable="claim.claimStateFk == resolvedStateId"
/>
</template>
</CrudModel>
<QDialog v-model="dialogDestination">
<QCard>
<QCardSection>
<QItem class="q-pa-sm">
<span class="q-dialog__title text-white">
{{ t('dialog title') }}
</span>
<QBtn icon="close" flat round dense v-close-popup />
</QItem>
</QCardSection>
<QItemSection>
<VnSelectFilter
class="q-pa-sm"
v-model="claimDestinationFk"
:options="destinationTypes"
option-label="description"
option-value="id"
:autofocus="true"
dense
input-debounce="0"
hide-selected
/>
</QItemSection>
<QCardActions class="justify-end q-mr-sm">
<QBtn flat :label="t('globals.close')" color="primary" v-close-popup />
<QBtn
:disable="!claimDestinationFk"
:label="t('globals.save')"
color="primary"
v-close-popup
@click="updateDestinations(claimDestinationFk)"
/>
</QCardActions>
</QCard>
</QDialog>
<!-- <QDialog v-model="dialogGreuge">
<QCardSection>
<QItem class="q-pa-sm">
<span class="q-pa-sm q-dialog__title text-white">
{{ t('dialogGreuge title') }}
</span>
<QBtn class="q-pa-sm" icon="close" flat round dense v-close-popup />
</QItem>
<QCardActions class="justify-end q-mr-sm">
<QBtn flat :label="t('globals.close')" color="primary" v-close-popup />
<QBtn
:label="t('globals.save')"
color="primary"
v-close-popup
@click="onUpdateGreugeAccept"
/>
</QCardActions>
</QCardSection>
</QDialog> -->
</template>
<style lang="scss" scoped>
.slider-container {
width: 50%;
}
@media (max-width: $breakpoint-xs) {
.slider-container {
width: 90%;
}
}
.q-table {
.q-item {
min-height: min-content;
height: 0;
}
}
.q-dialog {
.q-btn {
height: min-content;
}
}
.responsibility {
max-width: 100%;
margin-left: 40px;
}
.mana {
float: inline-start;
}
</style>
<i18n>
en:
mana: Is paid with mana
dialog title: Change destination to all selected rows
confirmGreuges: Do you want to insert complaints?
confirmGreugesMessage: Insert complaints into the client's record
es:
mana: Cargado al maná
Delivered: Descripción
Quantity: Cantidad
Claimed: Rec
Description: Descripción
Price: Precio
Discount: Dto.
Destination: Destino
Landed: F.entrega
Remove line: Eliminar línea
Total claimed: Total reclamado
Regularize: Regularizar
Change destination: Cambiar destino
Import claim: Importar reclamación
dialog title: Cambiar destino en todas las filas seleccionadas
Remove: Eliminar
dialogGreuge title: Insertar greuges en la ficha del cliente
ClaimGreugeDescription: Id reclamación
Id item: Id artículo
confirmGreuges: ¿Desea insertar greuges?
confirmGreugesMessage: Insertar greuges en la ficha del cliente
</i18n>

View File

@ -22,11 +22,6 @@ const $props = defineProps({
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
let salixUrl;
onMounted(async () => {
salixUrl = await getUrl(`claim/${entityId.value}`);
});
</script> </script>
<template> <template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()"> <Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
@ -42,18 +37,6 @@ onMounted(async () => {
<ClaimDescriptor /> <ClaimDescriptor />
<QSeparator /> <QSeparator />
<LeftMenu source="card" /> <LeftMenu source="card" />
<QSeparator />
<QList>
<QItem
active-class="text-primary"
clickable
v-ripple
:href="`${salixUrl}/action`"
>
<QItemSection avatar><QIcon name="vn:actions"></QIcon></QItemSection>
<QItemSection>{{ t('Action') }}</QItemSection>
</QItem>
</QList>
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<QPageContainer> <QPageContainer>

View File

@ -62,13 +62,18 @@ const filter = {
], ],
}; };
const STATE_COLOR = {
pending: 'positive',
managed: 'warning',
resolved: 'negative',
};
function stateColor(code) { function stateColor(code) {
if (code === 'pending') return 'positive'; return STATE_COLOR[code];
if (code === 'managed') return 'warning';
if (code === 'resolved') return 'negative';
} }
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const setData = (entity) => { const setData = (entity) => {
if (!entity) return;
data.value = useCardDescription(entity.client.name, entity.id); data.value = useCardDescription(entity.client.name, entity.id);
state.set('ClaimDescriptor', entity); state.set('ClaimDescriptor', entity);
}; };
@ -83,6 +88,7 @@ const setData = (entity) => {
:title="data.title" :title="data.title"
:subtitle="data.subtitle" :subtitle="data.subtitle"
@on-fetch="setData" @on-fetch="setData"
data-key="claimData"
> >
<template #menu="{ entity }"> <template #menu="{ entity }">
<ClaimDescriptorMenu :claim="entity" /> <ClaimDescriptorMenu :claim="entity" />
@ -120,16 +126,16 @@ const setData = (entity) => {
<VnLv :label="t('claim.card.commercial')"> <VnLv :label="t('claim.card.commercial')">
<template #value> <template #value>
<span class="link"> <span class="link">
{{ entity.client.salesPersonUser.name }} {{ entity.client?.salesPersonUser?.name }}
<WorkerDescriptorProxy :id="entity.client.salesPersonFk" /> <WorkerDescriptorProxy :id="entity.client?.salesPersonFk" />
</span> </span>
</template> </template>
</VnLv> </VnLv>
<VnLv <VnLv
:label="t('claim.card.province')" :label="t('claim.card.province')"
:value="entity.ticket.address.province.name" :value="entity.ticket?.address?.province?.name"
/> />
<VnLv :label="t('claim.card.zone')" :value="entity.ticket.zone.name" /> <VnLv :label="t('claim.card.zone')" :value="entity.ticket?.zone?.name" />
</template> </template>
<template #actions="{ entity }"> <template #actions="{ entity }">
<QCardActions> <QCardActions>

View File

@ -7,6 +7,7 @@ import FetchData from 'components/FetchData.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue'; import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import { getUrl } from 'composables/getUrl'; import { getUrl } from 'composables/getUrl';
import { tMobile } from 'composables/tMobile'; import { tMobile } from 'composables/tMobile';
import router from 'src/router';
const route = useRoute(); const route = useRoute();
@ -19,7 +20,7 @@ const claimResponsibles = ref([]);
const claimRedeliveries = ref([]); const claimRedeliveries = ref([]);
const workers = ref([]); const workers = ref([]);
const selected = ref([]); const selected = ref([]);
const insertButtonRef = ref(); const saveButtonRef = ref();
let salixUrl; let salixUrl;
onMounted(async () => { onMounted(async () => {
@ -102,10 +103,6 @@ const columns = computed(() => [
tabIndex: 5, tabIndex: 5,
}, },
]); ]);
function goToAction() {
location.href = `${salixUrl}/action`;
}
</script> </script>
<template> <template>
<FetchData <FetchData
@ -148,7 +145,7 @@ function goToAction() {
:data-required="{ claimFk: route.params.id }" :data-required="{ claimFk: route.params.id }"
v-model:selected="selected" v-model:selected="selected"
auto-load auto-load
@save-changes="goToAction" @save-changes="$router.push(`/claim/${route.params.id}/action`)"
:default-save="false" :default-save="false"
> >
<template #body="{ rows }"> <template #body="{ rows }">
@ -175,6 +172,7 @@ function goToAction() {
:option-label="col.optionLabel" :option-label="col.optionLabel"
:autofocus="col.tabIndex == 1" :autofocus="col.tabIndex == 1"
input-debounce="0" input-debounce="0"
hide-selected
> >
<template #option="scope" v-if="col.name == 'worker'"> <template #option="scope" v-if="col.name == 'worker'">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -213,6 +211,7 @@ function goToAction() {
dense dense
input-debounce="0" input-debounce="0"
:autofocus="col.tabIndex == 1" :autofocus="col.tabIndex == 1"
hide-selected
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -236,13 +235,11 @@ function goToAction() {
</CrudModel> </CrudModel>
<QPageSticky position="bottom-right" :offset="[25, 25]"> <QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn <QBtn
ref="insertButtonRef"
fab fab
color="primary" color="primary"
icon="add" icon="add"
@keydown.tab.prevent="saveButtonRef.$el.focus()"
@click="claimDevelopmentForm.insert()" @click="claimDevelopmentForm.insert()"
@keydown.ctrl.enter.stop="claimDevelopmentForm.saveChanges()"
@keydown.enter.stop
/> />
</QPageSticky> </QPageSticky>
</template> </template>
@ -251,9 +248,6 @@ function goToAction() {
.grid-style-transition { .grid-style-transition {
transition: transform 0.28s, background-color 0.28s; transition: transform 0.28s, background-color 0.28s;
} }
.maxwidth {
width: 100%;
}
</style> </style>
<i18n> <i18n>

View File

@ -46,7 +46,7 @@ async function onFetchClaim(data) {
const amount = ref(0); const amount = ref(0);
const amountClaimed = ref(0); const amountClaimed = ref(0);
async function onFetch(rows) { async function onFetch(rows) {
if (!rows || rows.length) return; if (!rows || !rows.length) return;
amount.value = rows.reduce( amount.value = rows.reduce(
(acumulator, { sale }) => acumulator + sale.price * sale.quantity, (acumulator, { sale }) => acumulator + sale.price * sale.quantity,
0 0
@ -155,7 +155,7 @@ function showImportDialog() {
</script> </script>
<template> <template>
<Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()"> <Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()">
<QToolbar class="bg-dark text-white"> <QToolbar>
<div class="row q-gutter-md"> <div class="row q-gutter-md">
<div> <div>
{{ t('Amount') }} {{ t('Amount') }}

View File

@ -26,6 +26,7 @@ const client = ref({});
const inputFile = ref(); const inputFile = ref();
const files = ref({}); const files = ref({});
const spinnerRef = ref();
const claimDmsRef = ref(); const claimDmsRef = ref();
const dmsType = ref({}); const dmsType = ref({});
const config = ref({}); const config = ref({});
@ -118,11 +119,11 @@ async function create() {
clientId: client.value.id, clientId: client.value.id,
}).toUpperCase(), }).toUpperCase(),
}; };
spinnerRef.value.show();
await axios.post(query, formData, { await axios.post(query, formData, {
params: dms, params: dms,
}); });
spinnerRef.value.hide();
quasar.notify({ quasar.notify({
message: t('globals.dataSaved'), message: t('globals.dataSaved'),
type: 'positive', type: 'positive',
@ -234,7 +235,9 @@ function onDrag() {
</div> </div>
</div> </div>
</div> </div>
<QDialog ref="spinnerRef">
<QSpinner color="primary" size="xl" />
</QDialog>
<QPageSticky position="bottom-right" :offset="[25, 25]"> <QPageSticky position="bottom-right" :offset="[25, 25]">
<label for="fileInput"> <label for="fileInput">
<QBtn fab @click="inputFile.nativeEl.click()" icon="add" color="primary"> <QBtn fab @click="inputFile.nativeEl.click()" icon="add" color="primary">

View File

@ -85,10 +85,15 @@ const detailsColumns = ref([
}, },
]); ]);
const STATE_COLOR = {
pending: 'positive',
managed: 'warning',
resolved: 'negative',
};
function stateColor(code) { function stateColor(code) {
if (code === 'pending') return 'green'; return STATE_COLOR[code];
if (code === 'managed') return 'orange';
if (code === 'resolved') return 'red';
} }
const developmentColumns = ref([ const developmentColumns = ref([

View File

@ -3,6 +3,7 @@ import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
@ -60,7 +61,7 @@ const states = ref();
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="workers"> <QItemSection v-if="workers">
<QSelect <VnSelectFilter
:label="t('Salesperson')" :label="t('Salesperson')"
v-model="params.salesPersonFk" v-model="params.salesPersonFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
@ -79,7 +80,7 @@ const states = ref();
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="workers"> <QItemSection v-if="workers">
<QSelect <VnSelectFilter
:label="t('Attender')" :label="t('Attender')"
v-model="params.attenderFk" v-model="params.attenderFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
@ -98,7 +99,7 @@ const states = ref();
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="workers"> <QItemSection v-if="workers">
<QSelect <VnSelectFilter
:label="t('Responsible')" :label="t('Responsible')"
v-model="params.claimResponsibleFk" v-model="params.claimResponsibleFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
@ -117,7 +118,7 @@ const states = ref();
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="states"> <QItemSection v-if="states">
<QSelect <VnSelectFilter
:label="t('State')" :label="t('State')"
v-model="params.claimStateFk" v-model="params.claimStateFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"

View File

@ -17,12 +17,14 @@ const router = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const STATE_COLOR = {
pending: 'positive',
managed: 'warning',
resolved: 'negative',
};
function stateColor(code) { function stateColor(code) {
if (code === 'pending') return 'green'; return STATE_COLOR[code];
if (code === 'managed') return 'orange';
if (code === 'resolved') return 'red';
} }
function navigate(id) { function navigate(id) {
router.push({ path: `/claim/${id}` }); router.push({ path: `/claim/${id}` });
} }

View File

@ -34,6 +34,7 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
:title="data.title" :title="data.title"
:subtitle="data.subtitle" :subtitle="data.subtitle"
@on-fetch="setData" @on-fetch="setData"
data-key="customerData"
> >
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv v-if="entity.salesPersonUser" :label="t('customer.card.salesPerson')"> <VnLv v-if="entity.salesPersonUser" :label="t('customer.card.salesPerson')">

View File

@ -6,6 +6,7 @@ import { toCurrency, toPercentage, toDate } from 'src/filters';
import CardSummary from 'components/ui/CardSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -68,8 +69,18 @@ const creditWarning = computed(() => {
<VnLv :label="t('customer.summary.customerId')" :value="entity.id" /> <VnLv :label="t('customer.summary.customerId')" :value="entity.id" />
<VnLv :label="t('customer.summary.name')" :value="entity.name" /> <VnLv :label="t('customer.summary.name')" :value="entity.name" />
<VnLv :label="t('customer.summary.contact')" :value="entity.contact" /> <VnLv :label="t('customer.summary.contact')" :value="entity.contact" />
<VnLv :label="t('customer.summary.phone')" :value="entity.phone" /> <VnLv :value="entity.phone">
<VnLv :label="t('customer.summary.mobile')" :value="entity.mobile" /> <template #label>
{{ t('customer.summary.phone') }}
<VnLinkPhone :phone-number="entity.phone" />
</template>
</VnLv>
<VnLv :value="entity.mobile">
<template #label>
{{ t('customer.summary.mobile') }}
<VnLinkPhone :phone-number="entity.mobile" />
</template>
</VnLv>
<VnLv :label="t('customer.summary.email')" :value="entity.email" /> <VnLv :label="t('customer.summary.email')" :value="entity.email" />
<VnLv <VnLv
:label="t('customer.summary.salesPerson')" :label="t('customer.summary.salesPerson')"

View File

@ -1,56 +0,0 @@
<script setup>
import { reactive, watch } from 'vue';
const customer = reactive({
name: '',
});
watch(() => customer.name);
</script>
<template>
<QPage class="q-pa-md">
<QCard class="q-pa-md">
<QForm @submit="onSubmit" @reset="onReset" class="q-gutter-md">
<QInput
filled
v-model="customer.name"
label="Your name *"
hint="Name and surname"
lazy-rules
:rules="[(val) => (val && val.length > 0) || 'Please type something']"
/>
<QInput
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>
<QBtn label="Submit" type="submit" color="primary" />
<QBtn
label="Reset"
type="reset"
color="primary"
flat
class="q-ml-sm"
/>
</div>
</QForm>
</QCard>
</QPage>
</template>
<style lang="scss" scoped>
.card {
width: 100%;
max-width: 60em;
}
</style>

View File

@ -3,6 +3,7 @@ import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
@ -63,7 +64,7 @@ const zones = ref();
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="workers"> <QItemSection v-if="workers">
<QSelect <VnSelectFilter
:label="t('Salesperson')" :label="t('Salesperson')"
v-model="params.salesPersonFk" v-model="params.salesPersonFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
@ -82,7 +83,7 @@ const zones = ref();
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="provinces"> <QItemSection v-if="provinces">
<QSelect <VnSelectFilter
:label="t('Province')" :label="t('Province')"
v-model="params.provinceFk" v-model="params.provinceFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
@ -91,6 +92,7 @@ const zones = ref();
option-label="name" option-label="name"
emit-value emit-value
map-options map-options
:input-debounce="0"
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -124,7 +126,7 @@ const zones = ref();
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="zones"> <QItemSection v-if="zones">
<QSelect <VnSelectFilter
:label="t('Zone')" :label="t('Zone')"
v-model="params.zoneFk" v-model="params.zoneFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"

View File

@ -9,6 +9,7 @@ import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import CustomerFilter from './CustomerFilter.vue'; import CustomerFilter from './CustomerFilter.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue'; import CardList from 'src/components/ui/CardList.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const router = useRouter(); const router = useRouter();
@ -77,7 +78,12 @@ function viewSummary(id) {
<template #list-items> <template #list-items>
<VnLv label="ID" :value="row.id" /> <VnLv label="ID" :value="row.id" />
<VnLv :label="t('customer.list.email')" :value="row.email" /> <VnLv :label="t('customer.list.email')" :value="row.email" />
<VnLv :label="t('customer.list.phone')" :value="row.phone" /> <VnLv :value="row.phone">
<template #label>
{{ t('customer.list.phone') }}
<VnLinkPhone :phone-number="row.phone" />
</template>
</VnLv>
</template> </template>
<template #actions> <template #actions>
<QBtn <QBtn

View File

@ -57,6 +57,7 @@ const setData = (entity) => (data.value = useCardDescription(entity.ref, entity.
:title="data.title" :title="data.title"
:subtitle="data.subtitle" :subtitle="data.subtitle"
@on-fetch="setData" @on-fetch="setData"
data-key="invoiceOutData"
> >
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('invoiceOut.card.issued')" :value="toDate(entity.issued)" /> <VnLv :label="t('invoiceOut.card.issued')" :value="toDate(entity.issued)" />

View File

@ -81,6 +81,7 @@ const setData = (entity) =>
:filter="filter" :filter="filter"
:title="data.title" :title="data.title"
:subtitle="data.subtitle" :subtitle="data.subtitle"
data-key="ticketData"
@on-fetch="setData" @on-fetch="setData"
> >
<template #menu="{ entity }"> <template #menu="{ entity }">

View File

@ -10,6 +10,7 @@ import FetchedTags from 'components/ui/FetchedTags.vue';
import InvoiceOutDescriptorProxy from 'pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue'; import InvoiceOutDescriptorProxy from 'pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue';
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
onUpdated(() => summaryRef.value.fetch()); onUpdated(() => summaryRef.value.fetch());
@ -172,7 +173,7 @@ async function changeState(value) {
:label="t('ticket.summary.agency')" :label="t('ticket.summary.agency')"
:value="ticket.agencyMode.name" :value="ticket.agencyMode.name"
/> />
<VnLv :label="t('ticket.summary.zone')" :value="ticket.zone.name" /> <VnLv :label="t('ticket.summary.zone')" :value="ticket?.zone?.name" />
<VnLv <VnLv
:label="t('ticket.summary.warehouse')" :label="t('ticket.summary.warehouse')"
:value="ticket.warehouse?.name" :value="ticket.warehouse?.name"
@ -180,7 +181,7 @@ async function changeState(value) {
<VnLv :label="t('ticket.summary.route')" :value="ticket.routeFk" /> <VnLv :label="t('ticket.summary.route')" :value="ticket.routeFk" />
<VnLv :label="t('ticket.summary.invoice')"> <VnLv :label="t('ticket.summary.invoice')">
<template #value> <template #value>
<span class="link"> <span :class="{ link: ticket.refFk }">
{{ dashIfEmpty(ticket.refFk) }} {{ dashIfEmpty(ticket.refFk) }}
<InvoiceOutDescriptorProxy <InvoiceOutDescriptorProxy
:id="ticket.id" :id="ticket.id"
@ -208,22 +209,30 @@ async function changeState(value) {
:value="toDate(ticket.landed)" :value="toDate(ticket.landed)"
/> />
<VnLv :label="t('ticket.summary.packages')" :value="ticket.packages" /> <VnLv :label="t('ticket.summary.packages')" :value="ticket.packages" />
<VnLv <VnLv :value="ticket.address.phone">
:label="t('ticket.summary.consigneePhone')" <template #label>
:value="ticket.address.phone" {{ t('ticket.summary.consigneePhone') }}
/> <VnLinkPhone :phone-number="ticket.address.phone" />
<VnLv </template>
:label="t('ticket.summary.consigneeMobile')" </VnLv>
:value="ticket.address.mobile" <VnLv :value="ticket.address.mobile">
/> <template #label>
<VnLv {{ t('ticket.summary.consigneeMobile') }}
:label="t('ticket.summary.clientPhone')" <VnLinkPhone :phone-number="ticket.address.mobile" />
:value="ticket.client.phone" </template>
/> </VnLv>
<VnLv <VnLv :value="ticket.client.phone">
:label="t('ticket.summary.clientMobile')" <template #label>
:value="ticket.client.mobile" {{ t('ticket.summary.clientPhone') }}
/> <VnLinkPhone :phone-number="ticket.client.phone" />
</template>
</VnLv>
<VnLv :value="ticket.client.mobile">
<template #label>
{{ t('ticket.summary.clientMobile') }}
<VnLinkPhone :phone-number="ticket.client.mobile" />
</template>
</VnLv>
<VnLv <VnLv
:label="t('ticket.summary.consignee')" :label="t('ticket.summary.consignee')"
:value="formattedAddress()" :value="formattedAddress()"

View File

@ -68,7 +68,7 @@ function viewSummary(id) {
</div> </div>
</Teleport> </Teleport>
</template> </template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256">
<QScrollArea class="fit text-grey-8"> <QScrollArea class="fit text-grey-8">
<TicketFilter data-key="TicketList" /> <TicketFilter data-key="TicketList" />
</QScrollArea> </QScrollArea>

View File

@ -0,0 +1,154 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useSession } from 'src/composables/useSession';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import VnConfirm from 'components/ui/VnConfirm.vue';
const quasar = useQuasar();
const { t } = useI18n();
const session = useSession();
const token = session.getToken();
const counters = ref({
alquilerBandeja: { count: 0, id: 96001, title: 'CC Bandeja', isTray: true },
bandejaRota: { count: 0, id: 88381, title: 'CC Bandeja Rota', isTray: true },
carryOficial: { count: 0, id: 96000, title: 'CC Carry OFICIAL TAG5' },
candadoRojo: { count: 0, id: 96002, title: 'CC Carry NO OFICIAL' },
sacadores: { count: 0, id: 142260, title: 'CC Sacadores' },
sinChapa: { count: 0, id: 2214, title: 'DC Carry Sin Placa CC' },
carroRoto: { count: 0, id: 142251, title: 'Carro Roto' },
});
const actions = {
add: (counter) => counter + 1,
subtract: (counter) => (counter ? counter - 1 : 0),
flush: () => 0,
addSpecific: (counter, amount) => counter + amount,
};
onMounted(() => {
const types = Object.keys(counters.value);
for (let type of types) {
const counter = localStorage.getItem(type);
counters.value[type].count = counter ? parseInt(counter) : 0;
}
});
function getUrl(id) {
return `/api/Images/catalog/200x200/${id}/download?access_token=${token}`;
}
async function handleEvent(type, action, amount) {
const counter = counters.value[type].count;
let isOk = true;
if (action == 'flush') isOk = await confirm();
if (isOk) {
counters.value[type].count = actions[action](counter, amount);
localStorage.setItem(type, counters.value[type].count);
}
}
function confirm() {
return new Promise((resolve) => {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Are you sure?'),
message: t('The counter will be reset to zero'),
},
})
.onOk(() => resolve(true))
.onCancel(() => resolve(false));
});
}
</script>
<template>
<QList class="row q-mx-auto q-mt-xl">
<QItem v-for="(props, name) in counters" :key="name" class="col-6">
<QItemSection>
<QImg
:src="getUrl(props.id)"
width="130px"
@click="handleEvent(name, 'add')"
/>
<p class="title">{{ props.title }}</p>
</QItemSection>
<QItemSection class="q-ma-none">
<QItemLabel class="text-h4">
{{ props.count }}
</QItemLabel>
<QItemLabel>
<QBtn
class="text-center q-mr-xs"
color="warning"
dense
size="sm"
v-if="props.isTray"
@click="handleEvent(name, 'addSpecific', 30)"
>
{{ t('Add 30') }}
</QBtn>
<QBtn
class="text-center q-mr-xs"
color="warning"
dense
size="sm"
v-else
@click="handleEvent(name, 'addSpecific', 10)"
>
{{ t('Add 10') }}
</QBtn>
</QItemLabel>
<QItemLabel>
<QBtn
class="text-center q-mr-xs"
color="warning"
dense
size="sm"
@click="handleEvent(name, 'subtract')"
>
{{ t('Subtract 1') }}
</QBtn>
<QBtn
class="text-center q-ml-xs"
color="red"
dense
size="sm"
@click="handleEvent(name, 'flush')"
>
{{ t('Flush') }}
</QBtn>
</QItemLabel>
</QItemSection>
<QSeparator class="q-mt-sm q-mx-none" color="primary" />
</QItem>
</QList>
</template>
<style lang="scss" scoped>
.q-list {
max-width: 50em;
}
@media (max-width: $breakpoint-sm) {
.q-item {
display: flex;
flex-direction: column;
}
.title {
min-height: 3em;
}
}
</style>
<i18n>
es:
Subtract 1: Quitar 1
Add 30: Añadir 30
Add 10: Añadir 10
Flush: Vaciar
Are you sure?: ¿Estás seguro?
It will set to 0: Se pondrá a 0
The counter will be reset to zero: Se pondrá el contador a cero
</i18n>

View File

@ -5,7 +5,9 @@ import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import CardDescriptor from 'src/components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
const $props = defineProps({ const $props = defineProps({
id: { id: {
type: Number, type: Number,
@ -47,19 +49,22 @@ const filter = {
], ],
}; };
const sip = computed(() => worker.value.sip && worker.value.sip.extension); const sip = computed(() => worker.value?.sip && worker.value.sip.extension);
function getWorkerAvatar() { function getWorkerAvatar() {
const token = getToken(); const token = getToken();
return `/api/Images/user/160x160/${route.params.id}/download?access_token=${token}`; return `/api/Images/user/160x160/${route.params.id}/download?access_token=${token}`;
} }
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const setData = (entity) => const setData = (entity) => {
(data.value = useCardDescription(entity.user.nickname, entity.id)); if (!entity) return;
data.value = useCardDescription(entity.user.nickname, entity.id);
};
</script> </script>
<template> <template>
<CardDescriptor <CardDescriptor
module="Worker" module="Worker"
data-key="workerData"
:url="`Workers/${entityId}`" :url="`Workers/${entityId}`"
:filter="filter" :filter="filter"
:title="data.title" :title="data.title"
@ -90,14 +95,24 @@ const setData = (entity) =>
</QImg> </QImg>
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('worker.card.name')" :value="entity.user.nickname" /> <VnLv :label="t('worker.card.name')" :value="entity.user?.nickname" />
<VnLv :label="t('worker.card.email')" :value="entity.user.email"> </VnLv> <VnLv :label="t('worker.card.email')" :value="entity.user?.email"> </VnLv>
<VnLv <VnLv
:label="t('worker.list.department')" :label="t('worker.list.department')"
:value="entity.department ? entity.department.department.name : null" :value="entity.department ? entity.department.department.name : null"
/> />
<VnLv :label="t('worker.card.phone')" :value="entity.phone" /> <VnLv :value="entity.phone">
<VnLv :label="t('worker.summary.sipExtension')" :value="sip" /> <template #label>
{{ t('worker.card.phone') }}
<VnLinkPhone :phone-number="entity.phone" />
</template>
</VnLv>
<VnLv :value="sip">
<template #label>
{{ t('worker.summary.sipExtension') }}
<VnLinkPhone v-if="sip" :phone-number="sip" />
</template>
</VnLv>
</template> </template>
</CardDescriptor> </CardDescriptor>
</template> </template>

View File

@ -1,10 +1,12 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { computed, onMounted, onUpdated, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import CrudModel from 'components/CrudModel.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
type: Number, type: Number,
@ -12,131 +14,139 @@ const $props = defineProps({
default: null, default: null,
}, },
}); });
const entityId = computed(() => $props.id || route.params.id);
onMounted(() => fetch());
onUpdated(() => fetch());
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const quasar = useQuasar(); const quasar = useQuasar();
const entityId = computed(() => $props.id || route.params.id);
const notifications = ref([]); const URL_KEY = 'NotificationSubscriptions';
const active = ref();
async function fetch() { const available = ref();
try {
await axios
.get(`NotificationSubscriptions/${entityId.value}/getList`)
.then(async (res) => {
if (res.data) {
notifications.value = res.data;
}
});
} catch (e) {
//
}
}
async function disableNotification(notification) {
await axios
.delete(`NotificationSubscriptions/${notification.id}`)
.catch(() => (notification.active = true))
.then((res) => {
if (res.data) {
notification.id = null;
notification.active = false;
quasar.notify({
type: 'positive',
message: t('worker.notificationsManager.unsubscribed'),
});
}
});
}
async function toggleNotification(notification) { async function toggleNotification(notification) {
if (!notification.active) { try {
await disableNotification(notification); if (!notification.active) {
} else { await axios.delete(`${URL_KEY}/${notification.id}`);
await axios swapEntry(active.value, available.value, notification.notificationFk);
.post(`NotificationSubscriptions`, { } else {
const { data } = await axios.post(URL_KEY, {
notificationFk: notification.notificationFk, notificationFk: notification.notificationFk,
userFk: entityId.value, userFk: entityId.value,
})
.catch(() => (notification.active = false))
.then((res) => {
if (res.data) {
notification.id = res.data.id;
quasar.notify({
type: 'positive',
message: t('worker.notificationsManager.subscribed'),
});
}
}); });
notification.id = data.id;
swapEntry(available.value, active.value, notification.notificationFk);
}
quasar.notify({
type: 'positive',
message: t(
`worker.notificationsManager.${notification.active ? '' : 'un'}subscribed`
),
});
} catch {
notification.active = !notification.active;
} }
} }
const swapEntry = (from, to, key) => {
const element = from.get(key);
to.set(key, element);
from.delete(key);
};
function setNotifications(data) {
active.value = new Map(data.active);
available.value = new Map(data.available);
}
</script> </script>
<template> <template>
<QPage> <CrudModel
<QCard class="q-pa-md"> auto-load
<QList> :data-key="URL_KEY"
<div :url="`${URL_KEY}/${entityId}/getList`"
v-show=" :default-reset="false"
notifications.filter( :default-remove="false"
(notification) => notification.active == true :default-save="false"
).length @on-fetch="setNotifications"
" >
> <template #body>
<QItemLabel header class="text-h6"> <div
{{ t('worker.notificationsManager.activeNotifications') }} v-for="(notifications, index) in [
</QItemLabel> [...active.values()],
<QItem> [...available.values()],
<div ]"
v-for="notification in notifications.filter( :key="notifications"
(notification) => notification.active == true >
)" <QList class="notificationList">
:key="notification.id" <TransitionGroup>
> <QCard
<QChip
:key="notification.id"
:label="notification.name"
text-color="white"
color="primary"
class="q-mr-sm"
removable
@remove="disableNotification(notification)"
/>
</div>
</QItem>
</div>
<div v-show="notifications.length">
<QItemLabel header class="text-h6">
{{ t('worker.notificationsManager.availableNotifications') }}
</QItemLabel>
<div class="row">
<QItem
class="col-3"
:key="notification.notificationFk"
v-for="notification in notifications" v-for="notification in notifications"
:key="notification.notificationFk"
class="q-pa-md"
> >
<QItemSection> <QItem>
<QItemLabel>{{ notification.name }}</QItemLabel> <QItemSection avatar>
<QItemLabel caption>{{ <QBtn
notification.description round
}}</QItemLabel> icon="mail"
</QItemSection> :color="notification.active ? 'green' : 'grey'"
<QItemSection side top> />
</QItemSection>
<QItemSection>
<QItemLabel>{{ notification.name }}</QItemLabel>
<QItemLabel caption>
{{ notification.description }}
</QItemLabel>
</QItemSection>
<QToggle <QToggle
checked-icon="check" checked-icon="check"
unchecked-icon="close" unchecked-icon="close"
indeterminate-icon="block"
v-model="notification.active" v-model="notification.active"
color="green"
@update:model-value="toggleNotification(notification)" @update:model-value="toggleNotification(notification)"
/> />
</QItemSection> </QItem>
</QItem> </QCard>
</div> </TransitionGroup>
</div> </QList>
</QList> <QSeparator
</QCard> color="primary"
</QPage> class="q-my-lg"
v-if="!index && available.size && active.size"
/>
</div>
</template>
</CrudModel>
</template> </template>
<style lang="scss" scoped>
.notificationList {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 10px;
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
}
@media (max-width: $breakpoint-md) {
.notificationList {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: $breakpoint-xs) {
.notificationList {
grid-template-columns: repeat(1, 1fr);
}
}
</style>

View File

@ -7,6 +7,7 @@ import { getUrl } from 'src/composables/getUrl';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import WorkerDescriptorProxy from './WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from './WorkerDescriptorProxy.vue';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -91,15 +92,24 @@ const filter = {
</span> </span>
</template> </template>
</VnLv> </VnLv>
<VnLv <VnLv :value="worker.mobileExtension">
:label="t('worker.summary.phoneExtension')" <template #label>
:value="worker.mobileExtension" {{ t('worker.summary.phoneExtension') }}
/> <VnLinkPhone :phone-number="worker.mobileExtension" />
<VnLv :label="t('worker.summary.entPhone')" :value="worker.phone" /> </template>
<VnLv </VnLv>
:label="t('worker.summary.personalPhone')" <VnLv :value="worker.phone">
:value="worker.client.phone" <template #label>
/> {{ t('worker.summary.entPhone') }}
<VnLinkPhone :phone-number="worker.phone" />
</template>
</VnLv>
<VnLv :value="worker.client.phone">
<template #label>
{{ t('worker.summary.personalPhone') }}
<VnLinkPhone :phone-number="worker.client.phone" />
</template>
</VnLv>
<VnLv :label="t('worker.summary.locker')" :value="worker.locker" /> <VnLv :label="t('worker.summary.locker')" :value="worker.locker" />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
@ -109,10 +119,12 @@ const filter = {
<VnLv :label="t('worker.summary.userId')" :value="worker.user.id" /> <VnLv :label="t('worker.summary.userId')" :value="worker.user.id" />
<VnLv :label="t('worker.card.name')" :value="worker.user.nickname" /> <VnLv :label="t('worker.card.name')" :value="worker.user.nickname" />
<VnLv :label="t('worker.summary.role')" :value="worker.user.role.name" /> <VnLv :label="t('worker.summary.role')" :value="worker.user.role.name" />
<VnLv <VnLv :value="worker?.sip?.extension">
:label="t('worker.summary.sipExtension')" <template #label>
:value="worker?.sip?.extension" {{ t('worker.summary.sipExtension') }}
/> <VnLinkPhone :phone-number="worker?.sip?.extension" />
</template>
</VnLv>
</QCard> </QCard>
</template> </template>
</CardSummary> </CardSummary>

View File

@ -19,6 +19,7 @@ export default {
'ClaimLog', 'ClaimLog',
'ClaimNotes', 'ClaimNotes',
'ClaimDevelopment', 'ClaimDevelopment',
'ClaimAction',
], ],
}, },
children: [ children: [
@ -130,6 +131,15 @@ export default {
}, },
component: () => import('src/pages/Claim/Card/ClaimNotes.vue'), component: () => import('src/pages/Claim/Card/ClaimNotes.vue'),
}, },
{
name: 'ClaimAction',
path: 'action',
meta: {
title: 'action',
icon: 'vn:actions',
},
component: () => import('src/pages/Claim/Card/ClaimAction.vue'),
},
], ],
}, },
], ],

View File

@ -10,7 +10,7 @@ export default {
component: RouterView, component: RouterView,
redirect: { name: 'CustomerMain' }, redirect: { name: 'CustomerMain' },
menus: { menus: {
main: ['CustomerList', 'CustomerPayments', 'CustomerCreate'], main: ['CustomerList', 'CustomerPayments'],
card: ['CustomerBasicData'], card: ['CustomerBasicData'],
}, },
children: [ children: [
@ -27,7 +27,7 @@ export default {
title: 'list', title: 'list',
icon: 'view_list', icon: 'view_list',
}, },
component: () => import('src/pages/Customer/CustomerList.vue') component: () => import('src/pages/Customer/CustomerList.vue'),
}, },
{ {
path: 'payments', path: 'payments',
@ -36,17 +36,7 @@ export default {
title: 'webPayments', title: 'webPayments',
icon: 'vn:onlinepayment', icon: 'vn:onlinepayment',
}, },
component: () => import('src/pages/Customer/CustomerPayments.vue') component: () => import('src/pages/Customer/CustomerPayments.vue'),
},
{
path: 'create',
name: 'CustomerCreate',
meta: {
title: 'createCustomer',
icon: 'vn:addperson',
roles: ['developer'],
},
component: () => import('src/pages/Customer/CustomerCreate.vue'),
}, },
], ],
}, },
@ -63,7 +53,8 @@ export default {
title: 'summary', title: 'summary',
icon: 'launch', icon: 'launch',
}, },
component: () => import('src/pages/Customer/Card/CustomerSummary.vue'), component: () =>
import('src/pages/Customer/Card/CustomerSummary.vue'),
}, },
{ {
path: 'basic-data', path: 'basic-data',
@ -72,7 +63,8 @@ export default {
title: 'basicData', title: 'basicData',
icon: 'vn:settings', icon: 'vn:settings',
}, },
component: () => import('src/pages/Customer/Card/CustomerBasicData.vue'), component: () =>
import('src/pages/Customer/Card/CustomerBasicData.vue'),
}, },
], ],
}, },

View File

@ -10,7 +10,7 @@ export default {
component: RouterView, component: RouterView,
redirect: { name: 'WagonMain' }, redirect: { name: 'WagonMain' },
menus: { menus: {
main: ['WagonList', 'WagonTypeList'], main: ['WagonList', 'WagonTypeList', 'WagonCounter'],
card: [], card: [],
}, },
children: [ children: [
@ -47,6 +47,15 @@ export default {
}, },
component: () => import('src/pages/Wagon/WagonCreate.vue'), component: () => import('src/pages/Wagon/WagonCreate.vue'),
}, },
{
path: 'counter',
name: 'WagonCounter',
meta: {
title: 'wagonCounter',
icon: 'add_circle',
},
component: () => import('src/pages/Wagon/WagonCounter.vue'),
},
], ],
}, },
{ {

View File

@ -11,7 +11,7 @@ export default {
redirect: { name: 'WorkerMain' }, redirect: { name: 'WorkerMain' },
menus: { menus: {
main: ['WorkerList'], main: ['WorkerList'],
// card: ['WorkerNotificationsManager'], card: ['WorkerNotificationsManager'],
}, },
children: [ children: [
{ {
@ -46,15 +46,16 @@ export default {
}, },
component: () => import('src/pages/Worker/Card/WorkerSummary.vue'), component: () => import('src/pages/Worker/Card/WorkerSummary.vue'),
}, },
// { {
// name: 'WorkerNotificationsManager', name: 'WorkerNotificationsManager',
// path: 'notifications', path: 'notifications',
// meta: { meta: {
// title: 'notifications', title: 'notifications',
// icon: 'notifications', icon: 'notifications',
// }, },
// component: () => import('src/pages/Worker/Card/WorkerNotificationsManager.vue'), component: () =>
// }, import('src/pages/Worker/Card/WorkerNotificationsManager.vue'),
},
], ],
}, },
], ],

View File

@ -18,7 +18,8 @@ export const useArrayDataStore = defineStore('arrayDataStore', () => {
skip: 0, skip: 0,
order: '', order: '',
data: ref(), data: ref(),
isLoading: false isLoading: false,
exprBuilder: null,
}; };
} }

View File

@ -0,0 +1,45 @@
/// <reference types="cypress" />
describe('ClaimAction', () => {
const claimId = 2;
const firstRow = 'tbody > :nth-child(1)';
const destinationRow = '.q-item__section > .q-field';
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit(`/#/claim/${claimId}/action`);
});
it('should import claim', () => {
cy.get('[title="Import claim"]').click();
});
it('should change destination', () => {
const rowData = [true, null, null, 'Bueno'];
cy.fillRow(firstRow, rowData);
});
it('should change destination from other button', () => {
const rowData = [true];
cy.fillRow(firstRow, rowData);
cy.get('[title="Change destination"]').click();
cy.selectOption(destinationRow, 'Confeccion');
cy.get('.q-card > .q-card__actions > .q-btn--standard').click();
});
it('should regularize', () => {
cy.get('[title="Regularize"]').click();
cy.clickConfirm();
});
it('should remove the line', () => {
cy.fillRow(firstRow, [true]);
cy.removeCard();
cy.clickConfirm();
cy.reload();
cy.get(firstRow).should('not.exist');
});
});

View File

@ -7,7 +7,7 @@ describe('ClaimNotes', () => {
it('should add a new note', () => { it('should add a new note', () => {
const message = 'This is a new message.'; const message = 'This is a new message.';
cy.get('.q-page-sticky button').click(); cy.get('.q-page-sticky > div > button').click();
cy.get('.q-dialog .q-card__section:nth-child(2)').type(message); cy.get('.q-dialog .q-card__section:nth-child(2)').type(message);
cy.get('.q-card__actions button:nth-child(2)').click(); cy.get('.q-card__actions button:nth-child(2)').click();
cy.get('.q-card .q-card__section:nth-child(2)') cy.get('.q-card .q-card__section:nth-child(2)')

View File

@ -1,36 +1,79 @@
xdescribe('WorkerNotificationsManager', () => { describe('WorkerNotificationsManager', () => {
const salesPersonId = 18;
const developerId = 9;
const activeList = ':nth-child(1) > .q-list';
const availableList = ':nth-child(2) > .q-list';
const firstActiveNotification =
':nth-child(1) > .q-list > :nth-child(1) > .q-item > .q-toggle > .q-toggle__inner';
const firstAvailableNotification =
':nth-child(2) > .q-list > :nth-child(1) > .q-item > .q-toggle > .q-toggle__inner';
beforeEach(() => { beforeEach(() => {
const workerId = 1110;
cy.viewport(1280, 720); cy.viewport(1280, 720);
cy.login('salesBoss');
cy.visit(`/#/worker/${workerId}/notifications`);
}); });
it('should unsubscribe 2 notifications, check the unsubscription has been saved, subscribe to other one and should check the data has been saved', () => { it('should throw an error if you try to change a notification that is not yours', () => {
cy.get('.q-chip').should('have.length', 3); cy.login('developer');
cy.get('.q-toggle__thumb').eq(0).click(); cy.visit(`/#/worker/${salesPersonId}/notifications`);
cy.get('.q-notification__message').should( cy.get(firstAvailableNotification).click();
'have.text', cy.notificationHas(
'Unsubscribed from the notification' '.q-notification__message',
'The notification subscription of this worker cant be modified'
); );
cy.get('.q-chip > .q-icon').eq(0).click(); });
cy.reload(); it('should active a notification that is yours', () => {
cy.login('developer');
cy.visit(`/#/worker/${developerId}/notifications`);
cy.waitForElement(activeList);
cy.waitForElement(availableList);
cy.get('.q-chip').should('have.length', 1); cy.get(activeList)
cy.get('.q-toggle__thumb').should('have.length', 3).eq(0).click(); .children()
cy.get('.q-notification__message').should( .its('length')
'have.text', .then((beforeSize) => {
'Subscribed to the notification' cy.get(firstAvailableNotification).click();
); cy.get(activeList)
cy.get('.q-toggle__thumb').should('have.length', 3).eq(1).click(); .children()
cy.get('.q-notification__message').should( .should('have.length', beforeSize + 1);
'have.text', });
'Subscribed to the notification' });
);
cy.reload(); it('should deactivate a notification that is yours', () => {
cy.login('developer');
cy.visit(`/#/worker/${developerId}/notifications`);
cy.waitForElement(activeList);
cy.waitForElement(availableList);
cy.get('.q-chip').should('have.length', 3); cy.get(availableList)
.children()
.its('length')
.then((beforeSize) => {
cy.get(firstActiveNotification).click();
cy.get(availableList)
.children()
.should('have.length', beforeSize + 1);
});
});
it('should active a notification if you are their boss', () => {
cy.login('salesBoss');
cy.visit(`/#/worker/${salesPersonId}/notifications`);
cy.waitForElement(activeList);
cy.waitForElement(availableList);
cy.get(activeList)
.children()
.its('length')
.then((beforeSize) => {
cy.get(firstAvailableNotification).click();
cy.get(activeList)
.children()
.should('have.length', beforeSize + 1);
//Rollback
cy.get(firstActiveNotification).click();
});
}); });
}); });

View File

@ -88,7 +88,12 @@ Cypress.Commands.add('addCard', () => {
cy.get('.q-page-sticky > div > .q-btn').click(); cy.get('.q-page-sticky > div > .q-btn').click();
}); });
Cypress.Commands.add('clickConfirm', () => { Cypress.Commands.add('clickConfirm', () => {
cy.get('.q-btn--unelevated > .q-btn__content > .block').click(); cy.waitForElement('.q-dialog__inner > .q-card');
cy.get('.q-card__actions > .q-btn--unelevated > .q-btn__content > .block').click();
});
Cypress.Commands.add('notificationHas', (selector, text) => {
cy.get(selector).should('have.text', text);
}); });
Cypress.Commands.add('fillRow', (rowSelector, data) => { Cypress.Commands.add('fillRow', (rowSelector, data) => {

View File

@ -1,15 +1,15 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper'; import { createWrapper } from 'app/test/vitest/helper';
import WorkerNotificationsManager from 'src/pages/Worker/Card/WorkerNotificationsManager.vue'; import WorkerNotificationsManager from 'src/pages/Worker/Card/WorkerNotificationsManager.vue';
import { ref } from 'vue';
describe('WorkerNotificationsManager', () => { describe('WorkerNotificationsManager', () => {
let vm; let vm;
const entityId = 1110;
beforeAll(() => { beforeAll(() => {
vm = createWrapper(WorkerNotificationsManager, { vm = createWrapper(WorkerNotificationsManager, {
propsData: { global: {
id: entityId, stubs: ['CrudModel'],
}, },
}).vm; }).vm;
}); });
@ -18,83 +18,16 @@ describe('WorkerNotificationsManager', () => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
describe('fetch()', () => { describe('swapEntry()', () => {
it('should fetch notification subscriptions and role mappings', async () => { it('should swap notification', async () => {
vi.spyOn(axios, 'get') const from = ref(new Map());
.mockResolvedValueOnce({ const to = ref(new Map());
data: [ from.value.set(1, { notificationFk: 1 });
{ to.value.set(2, { notificationFk: 2 });
id: 1,
name: 'Name 1',
description: 'Description 1',
notificationFk: 1,
active: true
},
],
});
await vm.fetch();
expect(axios.get).toHaveBeenCalledWith(`NotificationSubscriptions/${entityId}/getList`); await vm.swapEntry(from.value, to.value, 1);
expect(vm.notifications).toEqual([ expect(to.value.size).toBe(2);
{ expect(from.value.size).toBe(0);
id: 1,
notificationFk: 1,
name: 'Name 1',
description: 'Description 1',
active: true,
},
]);
});
});
describe('disableNotification()', () => {
it('should disable the notification', async () => {
vi.spyOn(axios, 'delete').mockResolvedValue({ data: { count: 1 } });
vi.spyOn(vm.quasar, 'notify');
const subscriptionId = 1;
vm.notifications = [{ id: 1, active: true }];
await vm.disableNotification(vm.notifications[0]);
expect(axios.delete).toHaveBeenCalledWith(
`NotificationSubscriptions/${subscriptionId}`
);
expect(vm.notifications[0].id).toBeNull();
expect(vm.notifications[0].id).toBeFalsy();
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({ type: 'positive' })
);
});
});
describe('toggleNotification()', () => {
it('should activate the notification', async () => {
vi.spyOn(axios, 'post').mockResolvedValue({
data: { id: 1, notificationFk: 1 },
});
vm.notifications = [{ id: null, active: true, notificationFk: 1 }];
await vm.toggleNotification(vm.notifications[0]);
expect(axios.post).toHaveBeenCalledWith('NotificationSubscriptions', {
notificationFk: 1,
userFk: entityId,
});
expect(vm.notifications[0].id).toBe(1);
expect(vm.notifications[0].active).toBeTruthy();
expect(vm.quasar.notify).toHaveBeenCalledWith(
expect.objectContaining({ type: 'positive' })
);
});
it('should disable the notification', async () => {
vi.spyOn(vm, 'disableNotification');
vm.notifications = [{ id: 1, active: false, notificationFk: 1 }];
await vm.toggleNotification(vm.notifications[0]);
expect(vm.notifications[0].id).toBe(null);
expect(vm.notifications[0].active).toBeFalsy();
}); });
}); });
}); });