#7353 fine tunning monitors #624

Merged
jorgep merged 34 commits from 7353-fineTunningMonitor into dev 2024-09-02 07:33:43 +00:00
22 changed files with 1115 additions and 884 deletions

View File

@ -332,299 +332,280 @@ defineExpose({
</QScrollArea>
</QDrawer>
<!-- class in div to fix warn-->
<div class="q-px-md">
<CrudModel
v-bind="$attrs"
:limit="20"
ref="CrudModelRef"
@on-fetch="(...args) => emit('onFetch', ...args)"
:search-url="searchUrl"
:disable-infinite-scroll="isTableMode"
@save-changes="reload"
:has-sub-toolbar="$attrs['hasSubToolbar'] ?? isEditable"
:auto-load="hasParams || $attrs['auto-load']"
>
<template
v-for="(_, slotName) in $slots"
#[slotName]="slotData"
:key="slotName"
<CrudModel
v-bind="$attrs"
:class="$attrs['class'] ?? 'q-px-md'"
:limit="20"
ref="CrudModelRef"
@on-fetch="(...args) => emit('onFetch', ...args)"
:search-url="searchUrl"
:disable-infinite-scroll="isTableMode"
@save-changes="reload"
:has-sub-toolbar="$attrs['hasSubToolbar'] ?? isEditable"
:auto-load="hasParams || $attrs['auto-load']"
>
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template>
<template #body="{ rows }">
<QTable
v-bind="table"
class="vnTable"
:columns="splittedColumns.columns"
:rows="rows"
v-model:selected="selected"
:grid="!isTableMode"
table-header-class="bg-header"
card-container-class="grid-three"
flat
:style="isTableMode && `max-height: ${tableHeight}`"
virtual-scroll
@virtual-scroll="
(event) =>
event.index > rows.length - 2 &&
CrudModelRef.vnPaginateRef.paginate()
"
@row-click="(_, row) => rowClickFunction && rowClickFunction(row)"
@update:selected="emit('update:selected', $event)"
>
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template>
<template #body="{ rows }">
<QTable
v-bind="table"
class="vnTable"
:columns="splittedColumns.columns"
:rows="rows"
v-model:selected="selected"
:grid="!isTableMode"
table-header-class="bg-header"
card-container-class="grid-three"
flat
:style="isTableMode && `max-height: ${tableHeight}`"
virtual-scroll
@virtual-scroll="
(event) =>
event.index > rows.length - 2 &&
CrudModelRef.vnPaginateRef.paginate()
"
@row-click="(_, row) => rowClickFunction && rowClickFunction(row)"
@update:selected="emit('update:selected', $event)"
>
<template #top-left v-if="!$props.withoutHeader">
<slot name="top-left"></slot>
</template>
<template #top-right v-if="!$props.withoutHeader">
<VnVisibleColumn
v-if="isTableMode"
v-model="splittedColumns.columns"
:table-code="tableCode ?? route.name"
:skip="columnsVisibilitySkipped"
/>
<QBtnToggle
v-model="mode"
toggle-color="primary"
class="bg-vn-section-color"
dense
:options="tableModes.filter((mode) => !mode.disable)"
/>
<QBtn
v-if="$props.rightSearch"
icon="filter_alt"
class="bg-vn-section-color q-ml-md"
dense
@click="stateStore.toggleRightDrawer()"
/>
</template>
<template #header-cell="{ col }">
<QTh v-if="col.visible ?? true">
<div
class="column self-start q-ml-xs ellipsis"
:class="`text-${col?.align ?? 'left'}`"
:style="$props.columnSearch ? 'height: 75px' : ''"
>
<div
class="row items-center no-wrap"
style="height: 30px"
>
<QTooltip v-if="col.toolTip">{{
col.toolTip
}}</QTooltip>
<VnTableOrder
v-model="orders[col.orderBy ?? col.name]"
:name="col.orderBy ?? col.name"
:label="col?.label"
:data-key="$attrs['data-key']"
:search-url="searchUrl"
/>
</div>
<VnTableFilter
v-if="$props.columnSearch"
:column="col"
:show-title="true"
<template #top-left v-if="!$props.withoutHeader">
<slot name="top-left"></slot>
</template>
<template #top-right v-if="!$props.withoutHeader">
<VnVisibleColumn
v-if="isTableMode"
v-model="splittedColumns.columns"
:table-code="tableCode ?? route.name"
:skip="columnsVisibilitySkipped"
/>
<QBtnToggle
v-model="mode"
toggle-color="primary"
class="bg-vn-section-color"
dense
:options="tableModes.filter((mode) => !mode.disable)"
/>
<QBtn
v-if="$props.rightSearch"
icon="filter_alt"
class="bg-vn-section-color q-ml-md"
dense
@click="stateStore.toggleRightDrawer()"
/>
</template>
<template #header-cell="{ col }">
<QTh v-if="col.visible ?? true">
<div
class="column self-start q-ml-xs ellipsis"
:class="`text-${col?.align ?? 'left'}`"
:style="$props.columnSearch ? 'height: 75px' : ''"
>
<div class="row items-center no-wrap" style="height: 30px">
<QTooltip v-if="col.toolTip">{{ col.toolTip }}</QTooltip>
<VnTableOrder
v-model="orders[col.orderBy ?? col.name]"
:name="col.orderBy ?? col.name"
:label="col?.label"
:data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
:search-url="searchUrl"
class="full-width"
/>
</div>
</QTh>
</template>
<template #header-cell-tableActions>
<QTh auto-width class="sticky" />
</template>
<template #body-cell-tableStatus="{ col, row }">
<QTd auto-width :class="getColAlign(col)">
<VnTableChip
:columns="splittedColumns.columnChips"
<VnTableFilter
v-if="$props.columnSearch"
:column="col"
:show-title="true"
:data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
:search-url="searchUrl"
class="full-width"
/>
</div>
</QTh>
</template>
<template #header-cell-tableActions>
<QTh auto-width class="sticky" />
</template>
<template #body-cell-tableStatus="{ col, row }">
<QTd auto-width :class="getColAlign(col)">
<VnTableChip :columns="splittedColumns.columnChips" :row="row">
<template #afterChip>
<slot name="afterChip" :row="row"></slot>
</template>
</VnTableChip>
</QTd>
</template>
<template #body-cell="{ col, row, rowIndex }">
<!-- Columns -->
<QTd
auto-width
class="no-margin q-px-xs"
:class="[getColAlign(col), col.columnClass]"
v-if="col.visible ?? true"
@click.ctrl="
($event) =>
rowCtrlClickFunction && rowCtrlClickFunction($event, row)
"
>
<slot
:name="`column-${col.name}`"
:col="col"
:row="row"
:row-index="rowIndex"
>
<VnTableColumn
:column="col"
:row="row"
>
<template #afterChip>
<slot name="afterChip" :row="row"></slot>
</template>
</VnTableChip>
</QTd>
</template>
<template #body-cell="{ col, row, rowIndex }">
<!-- Columns -->
<QTd
auto-width
class="no-margin q-px-xs"
:class="[getColAlign(col), col.columnClass]"
v-if="col.visible ?? true"
@click.ctrl="
($event) =>
rowCtrlClickFunction &&
rowCtrlClickFunction($event, row)
:is-editable="col.isEditable ?? isEditable"
v-model="row[col.name]"
component-prop="columnField"
/>
</slot>
</QTd>
</template>
<template #body-cell-tableActions="{ col, row }">
<QTd
auto-width
:class="getColAlign(col)"
class="sticky no-padding"
@click="stopEventPropagation($event)"
>
<QBtn
v-for="(btn, index) of col.actions"
:key="index"
:title="btn.title"
:icon="btn.icon"
class="q-px-sm"
flat
:class="
btn.isPrimary ? 'text-primary-light' : 'color-vn-text '
"
:style="`visibility: ${
(btn.show && btn.show(row)) ?? true ? 'visible' : 'hidden'
}`"
@click="btn.action(row)"
/>
</QTd>
</template>
<template #item="{ row, colsMap }">
<component
:is="$props.redirect ? 'router-link' : 'span'"
:to="`/${$props.redirect}/` + row.id"
>
<QCard
bordered
flat
class="row no-wrap justify-between cursor-pointer"
@click="
(_, row) => {
$props.rowClick && $props.rowClick(row);
}
"
>
<slot
:name="`column-${col.name}`"
:col="col"
:row="row"
:row-index="rowIndex"
>
<VnTableColumn
:column="col"
:row="row"
:is-editable="col.isEditable ?? isEditable"
v-model="row[col.name]"
component-prop="columnField"
/>
</slot>
</QTd>
</template>
<template #body-cell-tableActions="{ col, row }">
<QTd
auto-width
:class="getColAlign(col)"
class="sticky no-padding"
@click="stopEventPropagation($event)"
>
<QBtn
v-for="(btn, index) of col.actions"
:key="index"
:title="btn.title"
:icon="btn.icon"
class="q-px-sm"
flat
:class="
btn.isPrimary
? 'text-primary-light'
: 'color-vn-text '
"
:style="`visibility: ${
(btn.show && btn.show(row)) ?? true
? 'visible'
: 'hidden'
}`"
@click="btn.action(row)"
/>
</QTd>
</template>
<template #item="{ row, colsMap }">
<component
:is="$props.redirect ? 'router-link' : 'span'"
:to="`/${$props.redirect}/` + row.id"
>
<QCard
bordered
flat
class="row no-wrap justify-between cursor-pointer"
@click="
(_, row) => {
$props.rowClick && $props.rowClick(row);
}
"
<QCardSection
vertical
class="no-margin no-padding"
:class="colsMap.tableActions ? 'w-80' : 'fit'"
>
<!-- Chips -->
<QCardSection
vertical
class="no-margin no-padding"
:class="colsMap.tableActions ? 'w-80' : 'fit'"
v-if="splittedColumns.chips.length"
class="no-margin q-px-xs q-py-none"
>
<!-- Chips -->
<QCardSection
v-if="splittedColumns.chips.length"
class="no-margin q-px-xs q-py-none"
<VnTableChip
:columns="splittedColumns.chips"
:row="row"
>
<VnTableChip
:columns="splittedColumns.chips"
:row="row"
>
<template #afterChip>
<slot name="afterChip" :row="row"></slot>
</template>
</VnTableChip>
</QCardSection>
<!-- Title -->
<QCardSection
v-if="splittedColumns.title"
class="q-pl-sm q-py-none text-primary-light text-bold text-h6 cardEllipsis"
<template #afterChip>
<slot name="afterChip" :row="row"></slot>
</template>
</VnTableChip>
</QCardSection>
<!-- Title -->
<QCardSection
v-if="splittedColumns.title"
class="q-pl-sm q-py-none text-primary-light text-bold text-h6 cardEllipsis"
>
<span
:title="row[splittedColumns.title.name]"
@click="stopEventPropagation($event)"
class="cursor-text"
>
<span
:title="row[splittedColumns.title.name]"
@click="stopEventPropagation($event)"
class="cursor-text"
>
{{ row[splittedColumns.title.name] }}
</span>
</QCardSection>
<!-- Fields -->
<QCardSection
class="q-pl-sm q-pr-lg q-py-xs"
:class="$props.cardClass"
{{ row[splittedColumns.title.name] }}
</span>
</QCardSection>
<!-- Fields -->
<QCardSection
class="q-pl-sm q-pr-lg q-py-xs"
:class="$props.cardClass"
>
<div
v-for="(
col, index
) of splittedColumns.cardVisible"
:key="col.name"
class="fields"
>
<div
v-for="(
col, index
) of splittedColumns.cardVisible"
:key="col.name"
class="fields"
<VnLv
:label="
!col.component && col.label
? `${col.label}:`
: ''
"
>
<VnLv
:label="
!col.component && col.label
? `${col.label}:`
: ''
"
>
<template #value>
<span
@click="
stopEventPropagation($event)
"
<template #value>
<span
@click="stopEventPropagation($event)"
>
<slot
:name="`column-${col.name}`"
:col="col"
:row="row"
:row-index="index"
>
<slot
:name="`column-${col.name}`"
:col="col"
<VnTableColumn
:column="col"
:row="row"
:row-index="index"
>
<VnTableColumn
:column="col"
:row="row"
:is-editable="false"
v-model="row[col.name]"
component-prop="columnField"
:show-label="true"
/>
</slot>
</span>
</template>
</VnLv>
</div>
</QCardSection>
:is-editable="false"
v-model="row[col.name]"
component-prop="columnField"
:show-label="true"
/>
</slot>
</span>
</template>
</VnLv>
</div>
</QCardSection>
<!-- Actions -->
<QCardSection
v-if="colsMap.tableActions"
class="column flex-center w-10 no-margin q-pa-xs q-gutter-y-xs"
@click="stopEventPropagation($event)"
>
<QBtn
v-for="(btn, index) of splittedColumns.actions
.actions"
:key="index"
:title="btn.title"
:icon="btn.icon"
class="q-pa-xs"
flat
:class="
btn.isPrimary
? 'text-primary-light'
: 'color-vn-text '
"
@click="btn.action(row)"
/>
</QCardSection>
</QCard>
</component>
</template>
</QTable>
</template>
</CrudModel>
</div>
</QCardSection>
<!-- Actions -->
<QCardSection
v-if="colsMap.tableActions"
class="column flex-center w-10 no-margin q-pa-xs q-gutter-y-xs"
@click="stopEventPropagation($event)"
>
<QBtn
v-for="(btn, index) of splittedColumns.actions
.actions"
:key="index"
:title="btn.title"
:icon="btn.icon"
class="q-pa-xs"
flat
:class="
btn.isPrimary
? 'text-primary-light'
: 'color-vn-text '
"
@click="btn.action(row)"
/>
</QCardSection>
</QCard>
</component>
</template>
</QTable>
</template>
</CrudModel>
<QPageSticky v-if="create" :offset="[20, 20]" style="z-index: 2">
<QBtn @click="showForm = !showForm" color="primary" fab icon="add" />
<QTooltip>

View File

@ -3,7 +3,7 @@ import { onMounted, watch, computed, ref } from 'vue';
import { date } from 'quasar';
import { useI18n } from 'vue-i18n';
const model = defineModel({ type: String });
const model = defineModel({ type: [String, Date] });
const $props = defineProps({
isOutlined: {
type: Boolean,

View File

@ -77,6 +77,10 @@ const $props = defineProps({
type: Object,
default: null,
},
noOne: {
type: Boolean,
default: false,
},
});
const { t } = useI18n();
@ -89,6 +93,11 @@ const myOptionsOriginal = ref([]);
const vnSelectRef = ref();
const dataRef = ref();
const lastVal = ref();
const noOneText = t('globals.noOne');
const noOneOpt = ref({
[optionValue.value]: false,
[optionLabel.value]: noOneText,
});
const value = computed({
get() {
@ -104,9 +113,11 @@ watch(options, (newValue) => {
setOptions(newValue);
});
watch(modelValue, (newValue) => {
watch(modelValue, async (newValue) => {
if (!myOptions.value.some((option) => option[optionValue.value] == newValue))
fetchFilter(newValue);
await fetchFilter(newValue);
if ($props.noOne) myOptions.value.unshift(noOneOpt.value);
});
onMounted(() => {
@ -169,6 +180,7 @@ async function fetchFilter(val) {
const fetchOptions = { where, include, limit };
if (fields) fetchOptions.fields = fields;
if (sortBy) fetchOptions.order = sortBy;
return dataRef.value.fetch(fetchOptions);
}
@ -189,6 +201,9 @@ async function filterHandler(val, update) {
} else newOptions = filter(val, myOptionsOriginal.value);
update(
() => {
if ($props.noOne && noOneText.toLowerCase().includes(val.toLowerCase()))
newOptions.unshift(noOneOpt.value);
myOptions.value = newOptions;
},
(ref) => {

View File

@ -24,7 +24,7 @@ const $props = defineProps({
type: Boolean,
default: true,
},
unRemovableParams: {
unremovableParams: {
Review

Unremovable es una palabra.

Unremovable es una palabra.
type: Array,
required: false,
default: () => [],
@ -148,7 +148,7 @@ async function clearFilters() {
arrayData.reset(['skip', 'filter.skip', 'page']);
// Filtrar los params no removibles
const removableFilters = Object.keys(userParams.value).filter((param) =>
$props.unRemovableParams.includes(param)
$props.unremovableParams.includes(param)
);
const newParams = {};
// Conservar solo los params que no son removibles
@ -180,10 +180,10 @@ const tagsList = computed(() => {
});
const tags = computed(() => {
return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.key));

esto no iba.

esto no iba.
return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.label));
});
const customTags = computed(() =>
tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.key))
tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label))
);
async function remove(key) {
@ -268,7 +268,7 @@ function sanitizer(params) {
<VnFilterPanelChip
v-for="chip of tags"
:key="chip.label"
:removable="!unRemovableParams.includes(chip.label)"
:removable="!unremovableParams?.includes(chip.label)"
@remove="remove(chip.label)"
>
<slot name="tags" :tag="chip" :format-fn="formatValue">

View File

@ -10,6 +10,10 @@ const props = defineProps({
type: String,
required: true,
},
class: {
Review

Si no da warning en consola

Si no da warning en consola
type: String,
default: '',
},
autoLoad: {
type: Boolean,
default: false,
@ -215,7 +219,7 @@ defineExpose({ fetch, addFilter, paginate });
v-if="store.data"
@load="onLoad"
:offset="offset"
class="full-width"
:class="['full-width', props.class]"
:disable="disableInfiniteScroll || !store.hasMoreData"
v-bind="$attrs"
>

View File

@ -253,7 +253,6 @@ input::-webkit-inner-spin-button {
}
td {
font-size: 11pt;
border-top: 1px solid var(--vn-page-color);

Ya no hace falta,

Ya no hace falta,
border-collapse: collapse;
}
}

View File

@ -20,21 +20,21 @@ export function isValidDate(date) {
* Converts a given date to a specific format.
*
* @param {number|string|Date} date - The date to be formatted.
* @param {Object} opts - Optional parameters to customize the output format.
* @returns {string} The formatted date as a string in 'dd/mm/yyyy' format. If the provided date is not valid, an empty string is returned.
*
* @example
* // returns "02/12/2022"
* toDateFormat(new Date(2022, 11, 2));
*/
export function toDateFormat(date, locale = 'es-ES') {
if (!isValidDate(date)) {
return '';
}
return new Date(date).toLocaleDateString(locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
export function toDateFormat(date, locale = 'es-ES', opts = {}) {
if (!isValidDate(date)) return '';
const format = Object.assign(
{ year: 'numeric', month: '2-digit', day: '2-digit' },
opts
);
return new Date(date).toLocaleDateString(locale, format);
}
/**

View File

@ -1,7 +1,7 @@
export default function dateRange(value) {
const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0);
const maxHour = new Date();
const maxHour = new Date(value);
Review

Si no te pone hasta la fecha de hoy.

Si no te pone hasta la fecha de hoy.
maxHour.setHours(23, 59, 59, 59);
return [minHour, maxHour];

View File

@ -94,6 +94,7 @@ globals:
from: From
to: To
notes: Notes
refresh: Refresh
pageTitles:
logIn: Login
summary: Summary
@ -255,6 +256,8 @@ globals:
twoFactor: Two factor
recoverPassword: Recover password
resetPassword: Reset password
ticketsMonitor: Tickets monitor
clientsActionsMonitor: Clients and actions
serial: Serial
created: Created
worker: Worker
@ -269,6 +272,7 @@ globals:
subtitle: Are you sure exit without saving?
createInvoiceIn: Create invoice in
myAccount: My account
noOne: No one
errors:
statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred

View File

@ -94,6 +94,7 @@ globals:
from: Desde
to: Hasta
notes: Notas
refresh: Actualizar
pageTitles:
logIn: Inicio de sesión
summary: Resumen
@ -257,6 +258,8 @@ globals:
twoFactor: Doble factor
recoverPassword: Recuperar contraseña
resetPassword: Restablecer contraseña
ticketsMonitor: Monitor de tickets
clientsActionsMonitor: Clientes y acciones
serial: Facturas por serie
created: Fecha creación
worker: Trabajador
@ -271,6 +274,7 @@ globals:
subtitle: ¿Seguro que quiere salir sin guardar?
createInvoiceIn: Crear factura recibida
myAccount: Mi cuenta
noOne: Nadie
errors:
statusUnauthorized: Acceso denegado
statusInternalServerError: Ha ocurrido un error interno del servidor

View File

@ -0,0 +1,145 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import { toDateFormat } from 'src/filters/date.js';
import VnTable from 'src/components/VnTable/VnTable.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnRow from 'src/components/ui/VnRow.vue';
import { dateRange } from 'src/filters';
const { t } = useI18n();
const dates = dateRange(Date.vnNew());
const from = ref(dates[0]);
const to = ref(dates[1]);
const filter = computed(() => {
const obj = {};
const formatFrom = setHours(from.value, 'from');
const formatTo = setHours(to.value, 'to');
let stamp;
jorgep marked this conversation as resolved Outdated

Veo mucho Object.assign.
Crees que podemos dejar esto debajo del if...else..if y que el valor de v.stamp sea una variable?

Veo mucho Object.assign. Crees que podemos dejar esto debajo del if...else..if y que el valor de v.stamp sea una variable?
if (!formatFrom && formatTo) stamp = { lte: formatTo };
else if (formatFrom && !formatTo) stamp = { gte: formatFrom };
else if (formatFrom && formatTo) stamp = { between: [formatFrom, formatTo] };
return Object.assign(obj, { where: { 'v.stamp': stamp } });
});
function exprBuilder(param, value) {
switch (param) {
case 'clientFk':
return { [`c.id`]: value };
case 'salesPersonFk':
return { [`c.${param}`]: value };
}
}
function setHours(date, type) {
if (!date) return null;
const d = new Date(date);
if (type == 'from') d.setHours(0, 0, 0, 0);
else d.setHours(23, 59, 59, 59);
return d;
}
const columns = computed(() => [
{
label: t('salesClientsTable.date'),
name: 'dated',
field: 'dated',
align: 'left',
columnFilter: false,
format: (row) => toDateFormat(row.dated, 'es-ES', { year: '2-digit' }),
},
{
label: t('salesClientsTable.hour'),
name: 'hour',
jorgep marked this conversation as resolved Outdated

Revisamos el formateo ya que es dd/mm/yy

Revisamos el formateo ya que es dd/mm/yy
field: 'hour',
align: 'left',
columnFilter: false,
},
{
label: t('salesClientsTable.salesPerson'),
name: 'salesPersonFk',
field: 'salesPerson',
align: 'left',
columnField: {
component: null,
},
optionFilter: 'firstName',
columnFilter: {
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
sortBy: 'nickname ASC',
where: { role: 'salesPerson' },
useLike: false,
},
},
columnClass: 'no-padding',
},
{
label: t('salesClientsTable.client'),
field: 'clientName',
name: 'clientFk',
align: 'left',
columnField: {
component: null,
},
orderBy: 'c.name',
columnFilter: {
component: 'select',
attrs: {
url: 'Clients',
fields: ['id', 'name'],
sortBy: 'name ASC',
},
},
columnClass: 'no-padding',
},
]);
</script>
<template>
<VnTable
ref="table"
data-key="SalesMonitorClients"
url="SalesMonitors/clientsFilter"
search-url="SalesMonitorClients"
:order="['dated DESC', 'hour DESC']"
:expr-builder="exprBuilder"
:filter="filter"
:offset="50"
auto-load
:columns="columns"
:right-search="false"
default-mode="table"
:disable-option="{ card: true }"
dense
class="q-px-none"
>
<template #top-left>
<VnRow>
<VnInputDate v-model="from" :label="$t('globals.from')" dense />
<VnInputDate v-model="to" :label="$t('globals.to')" dense />
jorgep marked this conversation as resolved Outdated

PAra los 2 VnInputDate aplicamos el mismo style.
Usamos class

PAra los 2 VnInputDate aplicamos el mismo style. Usamos class

Te lo mejoro:
.full-width .vn-row > * { flex: 0.4; }

Te lo mejoro: `.full-width .vn-row > * { flex: 0.4; }`
</VnRow>
jorgep marked this conversation as resolved Outdated

Traducir

Traducir
</template>
jorgep marked this conversation as resolved Outdated

Traducir

Traducir
<template #column-salesPersonFk="{ row }">
<span class="link" :title="row.salesPerson" v-text="row.salesPerson" />
<WorkerDescriptorProxy :id="row.salesPersonFk" dense />
</template>
jorgep marked this conversation as resolved Outdated

Revisa porque creo que le sobre el Qtd y le falta el @click.stop
O pones Qtd por el .no-padding?

Revisa porque creo que le sobre el Qtd y le falta el @click.stop O pones Qtd por el .no-padding?

Esta tabla no redirige a ningún sitio, no le hace falta el click.stop. El qTd lo quito.

Esta tabla no redirige a ningún sitio, no le hace falta el click.stop. El qTd lo quito.
<template #column-clientFk="{ row }">
<span class="link" :title="row.clientName" v-text="row.clientName" />
<CustomerDescriptorProxy :id="row.clientFk" />
</template>
</VnTable>
</template>

El texto no aparece completo
"Bruce Way..." es lo que se ve

El texto no aparece completo "Bruce Way..." es lo que se ve

Le he quitado el padding, con el espacio que hay poco más se puede hacer. la tabla tiene el ancho que toca. Ahora los nombre de esa longitud y un poco más se ven.

Le he quitado el padding, con el espacio que hay poco más se puede hacer. la tabla tiene el ancho que toca. Ahora los nombre de esa longitud y un poco más se ven.
<style lang="scss" scoped>
.full-width .vn-row > * {
flex: 0.4;
}
</style>

View File

@ -0,0 +1,26 @@
<script setup>
import SalesClientTable from './MonitorClients.vue';
import SalesOrdersTable from './MonitorOrders.vue';
import VnRow from 'src/components/ui/VnRow.vue';
</script>
<template>
<VnRow
class="q-pa-md"
:style="{ 'flex-direction': $q.screen.lt.lg ? 'column' : 'row', gap: '0px' }"
>
<div style="flex: 0.3">
<span
class="q-ml-md text-body1"
v-text="$t('salesMonitor.clientsOnWebsite')"
/>
<SalesClientTable />
</div>
<div style="flex: 0.7">
<span
class="q-ml-md text-body1"
v-text="$t('salesMonitor.recentOrderActions')"
/>
<SalesOrdersTable />
</div>
</VnRow>
</template>

View File

@ -1,90 +0,0 @@
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import SalesClientTable from './SalesClientsTable.vue';
import SalesOrdersTable from './SalesOrdersTable.vue';
import SalesTicketsTable from './SalesTicketsTable.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
const { t } = useI18n();
const stateStore = useStateStore();
const expanded = ref(true);
onMounted(async () => {
stateStore.leftDrawer = false;
});
onUnmounted(() => (stateStore.leftDrawer = true));
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar
data-key="SalesMonitorTickets"
url="SalesMonitors/salesFilter"
:redirect="false"
:label="t('searchBar.label')"
:info="t('searchBar.info')"
/>
</Teleport>
</template>
<QPage class="column items-center q-pa-md">
<QCard class="full-width q-mb-lg">
<QExpansionItem v-model="expanded" dense :duration="150">
<template v-if="!expanded" #header>
<div class="row full-width">
<span class="flex col text-body1">
{{ t('salesMonitor.clientsOnWebsite') }}
</span>
<span class="flex col q-ml-xl text-body1">
{{ t('salesMonitor.recentOrderActions') }}
</span>
</div>
</template>
<template #default>
<div class="expansion-tables-container">
<QCardSection class="col">
<span class="flex col q-mb-sm text-body1">
{{ t('salesMonitor.clientsOnWebsite') }}
</span>
<SalesClientTable />
</QCardSection>
<QCardSection class="col">
<span class="flex col q-mb-sm text-body1">
{{ t('salesMonitor.recentOrderActions') }}
</span>
<SalesOrdersTable />
</QCardSection>
</div>
</template>
</QExpansionItem>
</QCard>
<QCard class="full-width">
<QItem class="justify-between">
<QItemLabel class="col slider-container">
<span class="text-body1"
>{{ t('salesMonitor.ticketsMonitor') }}
</span>
<QCardSection class="col" style="padding-inline: 0"
><SalesTicketsTable />
</QCardSection>
</QItemLabel>
</QItem>
</QCard>
</QPage>
</template>
<style lang="scss" scoped>
.expansion-tables-container {
display: flex;
border-top: 1px solid $color-spacer;
@media (max-width: $breakpoint-md-max) {
flex-direction: column;
}
}
</style>

View File

@ -0,0 +1,203 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import VnTable from 'components/VnTable/VnTable.vue';
import { toDateFormat, toDateTimeFormat } from 'src/filters/date.js';
import { toCurrency } from 'src/filters';
import { useVnConfirm } from 'composables/useVnConfirm';
import axios from 'axios';
const { t } = useI18n();
const { openConfirmationModal } = useVnConfirm();
const table = ref();
const selectedRows = ref([]);
function exprBuilder(param, value) {
switch (param) {
case 'clientFk':
return { [`c.id`]: value };
case 'salesPersonFk':
return { [`c.salesPersonFk`]: value };
}
}
const columns = computed(() => [
{
label: t('salesOrdersTable.dateSend'),
name: 'dateSend',
field: 'dateSend',
align: 'left',
orderBy: 'date_send',
columnFilter: false,
},
{
label: t('salesOrdersTable.dateMake'),
name: 'dateMake',
field: 'dateMake',
align: 'left',
orderBy: 'date_make',
columnFilter: false,
format: (row) => toDateTimeFormat(row.date_make),
},
{
label: t('salesOrdersTable.client'),
name: 'clientFk',
align: 'left',
columnFilter: {
component: 'select',
attrs: {
url: 'Clients',
fields: ['id', 'name'],
sortBy: 'name ASC',
},
},
},
{
label: t('salesOrdersTable.agency'),
name: 'agencyName',
align: 'left',
columnFilter: false,
},
{
label: t('salesOrdersTable.salesPerson'),
name: 'salesPersonFk',
align: 'left',
optionFilter: 'firstName',
columnFilter: {
Review

Cuando uso este filtro o el de cliente, me ocurre que al pulsar sobre la X, VnTable va bien, pero me aparece un toast de error sin texto
Te ocurre? Es poblema de VnTable?

Cuando uso este filtro o el de cliente, me ocurre que al pulsar sobre la X, VnTable va bien, pero me aparece un toast de error sin texto Te ocurre? Es poblema de VnTable?
Review

Sí, a veces. Pero esto es cosa de VnTable. Lo he probado en ClaimList y pasa igual.

Sí, a veces. Pero esto es cosa de VnTable. Lo he probado en ClaimList y pasa igual.
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
sortBy: 'nickname ASC',
where: { role: 'salesPerson' },
useLike: false,
},
},
},
{
label: t('salesOrdersTable.import'),
name: 'import',
field: 'import',
align: 'left',
columnFilter: false,
format: (row) => toCurrency(row.import),
},
]);
const getBadgeColor = (date) => {
const today = Date.vnNew();
today.setHours(0, 0, 0, 0);
const orderLanded = new Date(date);
orderLanded.setHours(0, 0, 0, 0);
const difference = today - orderLanded;
if (difference == 0) return 'warning';
if (difference < 0) return 'success';
if (difference > 0) return 'alert';
};
const removeOrders = async () => {
try {
const selectedIds = selectedRows.value.map((row) => row.id);
const params = { deletes: selectedIds };
await axios.post('SalesMonitors/deleteOrders', params);
selectedRows.value = [];
await table.value.reload();
} catch (err) {
console.error('Error deleting orders', err);
}
};
const openTab = (id) =>
window.open(`#/order/${id}/summary`, '_blank', 'noopener, noreferrer');
</script>
<template>
<VnTable
jorgep marked this conversation as resolved
Review

Pregunta, En Salix es order: date_makeDESC y aqui es date_send DESC, ha cambiado el criterio?

Pregunta, En Salix es order: date_makeDESC y aqui es date_send DESC, ha cambiado el criterio?
Review

Se lo consulté a J. Gallego y acordamos dejarlo como está ahora.

Se lo consulté a J. Gallego y acordamos dejarlo como está ahora.
ref="table"
class="q-px-none"
data-key="SalesMonitorOrders"
url="SalesMonitors/ordersFilter"
search-url="SalesMonitorOrders"
order="date_send DESC"
:right-search="false"
:expr-builder="exprBuilder"
auto-load
:columns="columns"
:table="{
'row-key': 'id',
selection: 'multiple',
'hide-bottom': true,
}"
default-mode="table"
:row-click="({ id }) => openTab(id)"
v-model:selected="selectedRows"
:disable-option="{ card: true }"
>
<template #top-left>
<QBtn
icon="refresh"
size="md"
color="primary"
dense
flat
@click="$refs.table.reload()"
jorgep marked this conversation as resolved
Review

tooltip

tooltip
>
<QTooltip>{{ $t('globals.refresh') }}</QTooltip>
</QBtn>
<QBtn
v-if="selectedRows.length"
icon="delete"
size="md"
dense
flat
color="primary"
@click="
openConfirmationModal(
$t('salesOrdersTable.deleteConfirmTitle'),
$t('salesOrdersTable.deleteConfirmMessage'),
removeOrders
)
"
>
<QTooltip>{{ t('salesOrdersTable.delete') }}</QTooltip>
</QBtn>
</template>
<template #column-dateSend="{ row }">
<QTd>
<QBadge
:color="getBadgeColor(row.date_send)"
text-color="black"
class="q-pa-sm"
style="font-size: 14px"
>
{{ toDateFormat(row.date_send) }}
</QBadge>
</QTd>
</template>
<template #column-clientFk="{ row }">
jorgep marked this conversation as resolved Outdated

Comportamiento exacto que en column-comercialFk

Comportamiento exacto que en column-comercialFk
<QTd @click.stop>
<span class="link" v-text="row.clientName" :title="row.clientName" />
<CustomerDescriptorProxy :id="row.clientFk" />
</QTd>
</template>
jorgep marked this conversation as resolved Outdated

mmm...revisa el comportamiento porque al hacer click te abre una nueva pestña en vez de mostrarte el descriptor. si vuelves a monitores si que se ve el descriptor

mmm...revisa el comportamiento porque al hacer click te abre una nueva pestña en vez de mostrarte el descriptor. si vuelves a monitores si que se ve el descriptor
<template #column-salesPersonFk="{ row }">
<QTd @click.stop>
jorgep marked this conversation as resolved
Review

Está diferente a MonitorClients, por algún detalle en concreto que se me pase?

Está diferente a MonitorClients, por algún detalle en concreto que se me pase?
Review

Las filas redirigen al hacer click. En clients no.

Las filas redirigen al hacer click. En clients no.
<span class="link" v-text="row.salesPerson" />
<WorkerDescriptorProxy :id="row.salesPersonFk" dense />
</QTd>
</template>
</VnTable>
</template>
<style lang="scss" scoped>
.q-td {
max-width: 140px;
}
</style>

View File

@ -1,147 +0,0 @@
<script setup>
import { ref, computed, reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import { toDateFormat } from 'src/filters/date.js';
import VnTable from 'src/components/VnTable/VnTable.vue';
const { t } = useI18n();
const paginateRef = ref(null);
const workersActiveOptions = ref([]);
const clientsOptions = ref([]);
const from = ref(Date.vnNew());
const to = ref(Date.vnNew());
const dateRange = computed(() => {
const minHour = new Date(from.value);
minHour.setHours(0, 0, 0, 0);
const maxHour = new Date(to.value);
maxHour.setHours(23, 59, 59, 59);
return [minHour, maxHour];
});
const filter = reactive({
where: {
'v.stamp': {
between: dateRange.value,
},
},
});
const refetch = async () => await paginateRef.value.fetch();
watch(dateRange, (val) => {
filter.where['v.stamp'].between = val;
refetch();
});
function exprBuilder(param, value) {
switch (param) {
case 'clientFk':
return { [`c.id`]: value };
case 'salesPersonFk':
return { [`c.${param}`]: value };
}
}
const params = reactive({});
const columns = computed(() => [
{
label: t('salesClientsTable.date'),
name: 'dated',
field: 'dated',
align: 'left',
columnFilter: null,
sortable: true,
format: (row) => toDateFormat(row.dated),
},
{
label: t('salesClientsTable.hour'),
name: 'hour',
field: 'hour',
align: 'left',
sortable: true,
},
{
label: t('salesClientsTable.salesPerson'),
name: 'salesPerson',
field: 'salesPerson',
align: 'left',
sortable: true,
columnField: {
component: null,
},
},
{
label: t('salesClientsTable.client'),
field: 'clientName',
name: 'client',
align: 'left',
sortable: true,
columnField: {
component: null,
},
},
]);
</script>
<template>
<FetchData
url="Workers/activeWithInheritedRole"
:filter="{
fields: ['id', 'nickname'],
order: 'nickname ASC',
where: { role: 'salesPerson' },
}"
auto-load
@on-fetch="(data) => (workersActiveOptions = data)"
/>
<FetchData
url="Clients"
:filter="{
fields: ['id', 'name'],
order: 'name ASC',
}"
auto-load
@on-fetch="(data) => (clientsOptions = data)"
/>
<QCard style="max-height: 380px; overflow-y: scroll">
<VnTable
ref="paginateRef"
data-key="SalesMonitorClients"
url="SalesMonitors/clientsFilter"
:order="['dated DESC', 'hour DESC']"
:limit="6"
:expr-builder="exprBuilder"
:user-params="params"
:filter="filter"
:offset="50"
auto-load
:columns="columns"
:right-search="false"
default-mode="table"
dense
:without-header="true"
>
<template #column-salesPerson="{ row }">
<QTd>
<span class="link">{{ row.salesPerson }}</span>
<WorkerDescriptorProxy :id="row.salesPersonFk" dense />
</QTd>
</template>
<template #column-client="{ row }">
<QTd>
<span class="link">{{ row.clientName }}</span>
<CustomerDescriptorProxy :id="row.clientFk" />
</QTd>
</template>
</VnTable>
</QCard>
</template>

View File

@ -1,204 +0,0 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnTable from 'components/VnTable/VnTable.vue';
import { toDateFormat, toDateTimeFormat } from 'src/filters/date.js';
import { toCurrency } from 'src/filters';
import { useVnConfirm } from 'composables/useVnConfirm';
import axios from 'axios';
const { t } = useI18n();
const { openConfirmationModal } = useVnConfirm();
const paginateRef = ref(null);
const workersActiveOptions = ref([]);
const clientsOptions = ref([]);
const selectedRows = ref([]);
const dateRange = (value) => {
const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0);
const maxHour = new Date(value);
maxHour.setHours(23, 59, 59, 59);
return [minHour, maxHour];
};
function exprBuilder(param, value) {
switch (param) {
case 'date_send':
return {
[`o.date_send`]: {
between: dateRange(value),
},
};
case 'clientFk':
return { [`c.id`]: value };
case 'salesPersonFk':
return { [`c.${param}`]: value };
}
}
const columns = computed(() => [
{
label: t('salesOrdersTable.date'),
name: 'date',
field: 'dated',
align: 'left',
sortable: true,
cardVisible: true,
},
{
label: t('salesOrdersTable.client'),
name: 'client',
align: 'left',
sortable: true,
cardVisible: true,
},
{
label: t('salesOrdersTable.salesPerson'),
name: 'salesPerson',
align: 'left',
sortable: true,
cardVisible: true,
},
]);
const getBadgeColor = (date) => {
const today = Date.vnNew();
today.setHours(0, 0, 0, 0);
const orderLanded = new Date(date);
orderLanded.setHours(0, 0, 0, 0);
const difference = today - orderLanded;
if (difference == 0) return 'warning';
if (difference < 0) return 'success';
if (difference > 0) return 'alert';
};
const removeOrders = async () => {
try {
const selectedIds = selectedRows.value.map((row) => row.id);
const params = { deletes: selectedIds };
await axios.post('SalesMonitors/deleteOrders', params);
selectedRows.value = [];
await paginateRef.value.fetch();
} catch (err) {
console.error('Error deleting orders', err);
}
};
const redirectToOrderSummary = (orderId) => {
const url = `#/order/${orderId}/summary`;
window.open(url, '_blank');
};
</script>
<template>
<FetchData
url="Workers/activeWithInheritedRole"
:filter="{
fields: ['id', 'nickname'],
order: 'nickname ASC',
where: { role: 'salesPerson' },
}"
auto-load
@on-fetch="(data) => (workersActiveOptions = data)"
/>
<FetchData
url="Clients"
:filter="{
fields: ['id', 'name'],
order: 'name ASC',
}"
auto-load
@on-fetch="(data) => (clientsOptions = data)"
/>
<VnSubToolbar />
<QCard style="max-height: 380px; overflow-y: scroll">
<VnTable
ref="paginateRef"
data-key="SalesMonitorOrders"
url="SalesMonitors/ordersFilter"
order="date_make DESC"
:limit="6"
:right-search="false"
:expr-builder="exprBuilder"
auto-load
:columns="columns"
:table="{
'row-key': 'id',
selection: 'multiple',
'hide-bottom': true,
}"
default-mode="table"
:without-header="false"
@row-click="(_, row) => redirectToOrderSummary(row.id)"
v-model:selected="selectedRows"
>
<template #top-left>
<QBtn
v-if="selectedRows.length > 0"
icon="delete"
size="md"
color="primary"
@click="
openConfirmationModal(
t('salesOrdersTable.deleteConfirmTitle'),
t('salesOrdersTable.deleteConfirmMessage'),
removeOrders
)
"
>
<QTooltip>{{ t('salesOrdersTable.delete') }}</QTooltip>
</QBtn>
</template>
<template #column-date="{ row }">
<QTd>
<QBadge
:color="getBadgeColor(row.date_send)"
text-color="black"
class="q-pa-sm q-mb-md"
style="font-size: 14px"
>
{{ toDateFormat(row.date_send) }}
</QBadge>
<div>{{ toDateTimeFormat(row.date_make) }}</div>
</QTd>
</template>
<template #column-client="{ row }">
<QTd>
<div class="q-mb-md">
<span class="link">{{ row.clientName }}</span>
<CustomerDescriptorProxy :id="row.clientFk" />
</div>
<span> {{ row.agencyName }}</span>
</QTd>
</template>
<template #column-salesPerson="{ row }">
<QTd>
<div class="q-mb-md">
<span class="link">{{ row.salesPerson }}</span>
<WorkerDescriptorProxy :id="row.salesPersonFk" dense />
</div>
<span>{{ toCurrency(row.import) }}</span>
</QTd>
</template>
</VnTable>
</QCard>
</template>
<style lang="scss" scoped>
.q-td {
color: gray;
}
</style>

View File

@ -0,0 +1,286 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnFilterPanelChip from 'src/components/ui/VnFilterPanelChip.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import FetchData from 'src/components/FetchData.vue';
import { dateRange } from 'src/filters';
defineProps({ dataKey: { type: String, required: true } });
const { t } = useI18n();
const warehouses = ref();
const groupedStates = ref();
const handleScopeDays = (params, days, callback) => {
const [from, to] = dateRange(Date.vnNew());
if (!days) {
Object.assign(params, { from, to, scopeDays: 1 });
} else {
params.from = from;
to.setDate(to.getDate() + days);
params.to = to;
}
if (callback) callback();
};
</script>
<template>
<FetchData url="Warehouses" auto-load @on-fetch="(data) => (warehouses = data)" />
Review

VnSelect podria tener esta url no?
No nos quitaremos la dependencia de FetchData pero ya que tenemos esa funcionalidad...
Y realmente warehouses solo se repite 4 veces y nos quitamos la variable que no tiene recorrido en este componente

VnSelect podria tener esta url no? No nos quitaremos la dependencia de FetchData pero ya que tenemos esa funcionalidad... Y realmente warehouses solo se repite 4 veces y nos quitamos la variable que no tiene recorrido en este componente
Review

Solo hay 88 registros en la tabla warehouse, cuando son tablas pequeñas se acordó cargarla toda de golpe.

Solo hay 88 registros en la tabla warehouse, cuando son tablas pequeñas se acordó cargarla toda de golpe.
<FetchData
url="AlertLevels"
auto-load
@on-fetch="
(data) =>
(groupedStates = data.map((x) => Object.assign(x, { code: t(x.code) })))
"
/>
<VnFilterPanel
:data-key="dataKey"
:search-button="true"
:hidden-tags="['from', 'to']"
:custom-tags="['scopeDays']"
:unremovable-params="['from', 'to', 'scopeDays']"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong v-text="`${t(`params.${tag.label}`)}:`" />
<span v-text="formatFn(tag.value)" />
</div>
</template>
<template #customTags="{ params, searchFn, formatFn }">
<VnFilterPanelChip
v-if="params.scopeDays"
removable
@remove="handleScopeDays(params, null, searchFn)"
>
<strong v-text="`${t(`params.scopeDays`)}:`" />
<span v-text="formatFn(params.scopeDays)" />
</VnFilterPanelChip>
</template>
<template #body="{ params }">
<QItem>
<QItemSection>
<VnInput
:label="t('params.clientFk')"
v-model="params.clientFk"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('params.orderFk')"
v-model="params.orderFk"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputNumber
:label="t('params.scopeDays')"
v-model="params.scopeDays"
is-outlined
@update:model-value="(val) => handleScopeDays(params, val)"
@remove="(val) => handleScopeDays(params, val)"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('params.nickname')"
v-model="params.nickname"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
outlined
dense
rounded
:label="t('params.salesPersonFk')"
v-model="params.salesPersonFk"
url="Workers/search"
:params="{ departmentCodes: ['VT'] }"
is-outlined
option-value="code"
option-label="name"
:no-one="true"
>
<template #option="{ opt, itemProps }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel
v-if="opt.code"
class="text-grey text-caption"
>
{{ `${opt.nickname}, ${opt.code}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('params.refFk')"
v-model="params.refFk"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
outlined
dense
rounded
:label="t('params.agencyModeFk')"
v-model="params.agencyModeFk"
url="AgencyModes/isActive"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
outlined
dense
rounded
:label="t('params.stateFk')"
v-model="params.stateFk"
url="States"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
outlined
dense
rounded
:label="t('params.groupedStates')"
v-model="params.alertLevel"
:options="groupedStates"
option-label="code"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
outlined
dense
rounded
:label="t('params.warehouseFk')"
v-model="params.warehouseFk"
:options="warehouses"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
outlined
dense
rounded
:label="t('params.provinceFk')"
v-model="params.provinceFk"
url="Provinces"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
:label="t('params.myTeam')"
v-model="params.myTeam"
toggle-indeterminate
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
:label="t('params.problems')"
v-model="params.problems"
toggle-indeterminate
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
:label="t('params.pending')"
v-model="params.pending"
toggle-indeterminate
/>
</QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
clientFk: Client id
orderFk: Order id
scopeDays: Days onward
nickname: Nickname
salesPersonFk: Sales person
refFk: Invoice
agencyModeFk: Agency
stateFk: State
groupedStates: Grouped State
warehouseFk: Warehouse
provinceFk: Province
myTeam: My team
problems: With problems
pending: Pending
from: From
to: To
alertLevel: Grouped State
FREE: Free
DELIVERED: Delivered
ON_PREPARATION: On preparation
ON_PREVIOUS: On previous
PACKED: Packed
No one: No one
es:
params:
clientFk: Id cliente
orderFk: Id cesta
scopeDays: Días en adelante
nickname: Nombre mostrado
salesPersonFk: Comercial
refFk: Factura
agencyModeFk: Agencia
stateFk: Estado
groupedStates: Estado agrupado
warehouseFk: Almacén
provinceFk: Provincia
myTeam: Mi equipo
problems: Con problemas
pending: Pendiente
from: Desde
To: Hasta
alertLevel: Estado agrupado
FREE: Libre
DELIVERED: Servido
ON_PREPARATION: En preparación
ON_PREVIOUS: En previa
PACKED: Encajado
</i18n>

View File

@ -0,0 +1,12 @@
<script setup>
import VnSearchbar from 'components/ui/VnSearchbar.vue';
</script>
<template>
<VnSearchbar
data-key="SalesMonitorTickets"
url="SalesMonitors/salesFilter"
:redirect="false"
:label="$t('searchBar.label')"
:info="$t('searchBar.info')"
/>
</template>

View File

@ -1,37 +1,38 @@
<script setup>
import { ref, computed, reactive } from 'vue';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue';
import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue';
import TicketSummary from 'src/pages/Ticket/Card/TicketSummary.vue';
import VnTable from 'components/VnTable/VnTable.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { toDateFormat, toTimeFormat } from 'src/filters/date.js';
import { toCurrency, dateRange } from 'src/filters';
import { toDateFormat } from 'src/filters/date.js';
import { toCurrency, dateRange, dashIfEmpty } from 'src/filters';
import RightMenu from 'src/components/common/RightMenu.vue';
import MonitorTicketSearchbar from './MonitorTicketSearchbar.vue';
import MonitorTicketFilter from './MonitorTicketFilter.vue';
const DEFAULT_AUTO_REFRESH = 1000;
const DEFAULT_AUTO_REFRESH = 2 * 60 * 1000; // 2min in ms
const { t } = useI18n();
const autoRefresh = ref(false);
const paginateRef = ref(null);
const workersActiveOptions = ref([]);
const provincesOptions = ref([]);
const statesOptions = ref([]);
const zonesOptions = ref([]);
const tableRef = ref(null);
const provinceOpts = ref([]);
const stateOpts = ref([]);
const zoneOpts = ref([]);
const visibleColumns = ref([]);
const allColumnNames = ref([]);
const { viewSummary } = useSummaryDialog();
const [from, to] = dateRange(Date.vnNew());
function exprBuilder(param, value) {
switch (param) {
case 'stateFk':
return { 'ts.stateFk': value };
case 'salesPersonFk':
return { 'c.salesPersonFk': value };
return { 'c.salesPersonFk': !value ? null : value };
case 'provinceFk':
return { 'a.provinceFk': value };
case 'theoreticalHour':
@ -48,15 +49,12 @@ function exprBuilder(param, value) {
}
}
const filter = { order: ['totalProblems DESC'] };
let params = reactive({});
const columns = computed(() => [
{
label: t('salesTicketsTable.problems'),
name: 'problems',
name: 'totalProblems',
align: 'left',
sortable: true,
columnFilter: false,
attrs: {
dense: true,
@ -64,13 +62,12 @@ const columns = computed(() => [
},
{
label: t('salesTicketsTable.identifier'),
name: 'identifier',
name: 'id',
field: 'id',
align: 'left',
sortable: true,
columnFilter: {
component: 'input',
component: 'number',
name: 'id',
attrs: {
dense: true,
@ -79,41 +76,41 @@ const columns = computed(() => [
},
{
label: t('salesTicketsTable.client'),
name: 'client',
name: 'clientFk',
align: 'left',
field: 'nickname',
sortable: true,
columnFilter: {
component: 'input',
name: 'nickname',
component: 'select',
attrs: {
dense: true,
url: 'Clients',
fields: ['id', 'name', 'nickname'],
sortBy: 'name ASC',
},
},
},
{
label: t('salesTicketsTable.salesPerson'),
name: 'salesPerson',
name: 'salesPersonFk',
field: 'userName',
align: 'left',
sortable: true,
optionFilter: 'firstName',
columnFilter: {
component: 'select',
name: 'salesPersonFk',
attrs: {
options: workersActiveOptions.value,
'option-value': 'id',
'option-label': 'name',
dense: true,
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
sortBy: 'nickname ASC',
where: { role: 'salesPerson' },
useLike: false,
},
},
},
{
label: t('salesTicketsTable.date'),
name: 'date',
name: 'shippedDate',
style: { 'max-width': '100px' },
align: 'left',
sortable: true,
columnFilter: {
component: 'date',
name: 'shippedDate',
@ -124,61 +121,39 @@ const columns = computed(() => [
},
{
label: t('salesTicketsTable.theoretical'),
name: 'theoretical',
name: 'theoreticalhour',
field: 'zoneLanding',
align: 'left',
sortable: true,
format: (val) => toTimeFormat(val),
columnFilter: {
component: 'input',
name: 'theoreticalHour',
attrs: {
dense: true,
},
},
format: (row) => row.theoreticalhour,
columnFilter: false,
},
{
label: t('salesTicketsTable.practical'),
name: 'practical',
name: 'practicalHour',
Review

Revisamos porque en salix esta columna si tiene filtro

Revisamos porque en salix esta columna si tiene filtro
Review

El filtro no iba en salix y en VnTable no funciona el filtro de hora.

El filtro no iba en salix y en VnTable no funciona el filtro de hora.
field: 'practicalHour',
align: 'left',
sortable: true,
columnFilter: {
component: 'input',
name: 'practicalHour',
attrs: {
dense: true,
},
},
format: (row) => row.practicalHour,
columnFilter: false,
},
{
label: t('salesTicketsTable.preparation'),
name: 'preparation',
name: 'preparationHour',
field: 'shipped',
align: 'left',
sortable: true,
format: (val) => toTimeFormat(val),
columnFilter: {
component: 'input',
name: 'shippedDate',
attrs: {
dense: true,
},
},
format: (row) => row.preparationHour,
columnFilter: false,
},
{
label: t('salesTicketsTable.province'),
name: 'province',
name: 'provinceFk',
jorgep marked this conversation as resolved
Review

Podemos dartle un poco mas de espacio a provincia y comprimir fragil?

Podemos dartle un poco mas de espacio a provincia y comprimir fragil?
Review

comprimir fragil no, porque cuando se ordena por esa columna, se corta. A provincia se le puede dar más espacio.

comprimir fragil no, porque cuando se ordena por esa columna, se corta. A provincia se le puede dar más espacio.
field: 'province',
align: 'left',
style: { 'max-width': '100px' },
sortable: true,
format: (row) => row.province,
columnFilter: {
component: 'select',
name: 'provinceFk',
attrs: {
options: provincesOptions.value,
options: provinceOpts.value,
'option-value': 'id',
'option-label': 'name',
dense: true,
@ -190,12 +165,11 @@ const columns = computed(() => [
name: 'state',
align: 'left',
style: { 'max-width': '100px' },
sortable: true,
columnFilter: {
component: 'select',
name: 'stateFk',
attrs: {
options: statesOptions.value,
options: stateOpts.value,
'option-value': 'id',
'option-label': 'name',
dense: true,
@ -207,10 +181,7 @@ const columns = computed(() => [
name: 'isFragile',
field: 'isFragile',
align: 'left',
sortable: true,
columnFilter: {
inWhere: true,
},
columnFilter: false,
attrs: {
'checked-icon': 'local_bar',
'unchecked-icon': 'local_bar',
@ -220,14 +191,14 @@ const columns = computed(() => [
},
{
label: t('salesTicketsTable.zone'),
name: 'zone',
name: 'zoneFk',
align: 'left',
sortable: true,
columnFilter: {
component: 'select',
name: 'zoneFk',
attrs: {
options: zonesOptions.value,
options: zoneOpts.value,
'option-value': 'id',
'option-label': 'name',
dense: true,
@ -236,13 +207,13 @@ const columns = computed(() => [
},
{
label: t('salesTicketsTable.total'),
name: 'total',
name: 'totalWithVat',
field: 'totalWithVat',
align: 'left',
style: { 'max-width': '75px' },
sortable: true,
columnFilter: {
component: 'input',
component: 'number',
name: 'totalWithVat',
attrs: {
dense: true,
@ -258,7 +229,7 @@ const columns = computed(() => [
title: t('salesTicketsTable.goToLines'),
icon: 'vn:lines',
color: 'priamry',
action: (row) => redirectToSales(row.id),
action: (row) => openTab(row.id),
isPrimary: true,
attrs: {
flat: true,
@ -297,18 +268,13 @@ let refreshTimer = null;
const autoRefreshHandler = (value) => {
if (value)
refreshTimer = setInterval(() => paginateRef.value.fetch(), DEFAULT_AUTO_REFRESH);
refreshTimer = setInterval(() => tableRef.value.reload(), DEFAULT_AUTO_REFRESH);
else {
clearInterval(refreshTimer);
refreshTimer = null;
}
};
const redirectToTicketSummary = (id) => {
const url = `#/ticket/${id}/summary`;
window.open(url, '_blank');
};
const stateColors = {
notice: 'info',
success: 'positive',
@ -329,23 +295,10 @@ const formatShippedDate = (date) => {
return toDateFormat(_date);
};
const redirectToSales = (id) => {
const url = `#/ticket/${id}/sale`;
window.open(url, '_blank');
};
const openTab = (id) =>
window.open(`#/ticket/${id}/sale`, '_blank', 'noopener, noreferrer');
</script>
<template>
<FetchData
url="Workers/activeWithInheritedRole"
:filter="{
fields: ['id', 'nickname'],
order: 'nickname ASC',
where: { role: 'salesPerson' },
}"
auto-load
@on-fetch="(data) => (workersActiveOptions = data)"
/>
<FetchData
url="Provinces"
:filter="{
@ -353,7 +306,7 @@ const redirectToSales = (id) => {
order: 'name ASC',
}"
auto-load
@on-fetch="(data) => (provincesOptions = data)"
@on-fetch="(data) => (provinceOpts = data)"
/>
<FetchData
url="States"
@ -362,7 +315,7 @@ const redirectToSales = (id) => {
order: 'name ASC',
}"
auto-load
@on-fetch="(data) => (statesOptions = data)"
@on-fetch="(data) => (stateOpts = data)"
/>
<FetchData
url="Zones"
@ -371,46 +324,60 @@ const redirectToSales = (id) => {
order: 'name ASC',
}"
auto-load
@on-fetch="(data) => (zonesOptions = data)"
@on-fetch="(data) => (zoneOpts = data)"
/>
<MonitorTicketSearchbar />
<RightMenu>
<template #right-panel>
<MonitorTicketFilter data-key="saleMonitorTickets" />
</template>
</RightMenu>
<VnTable
ref="paginateRef"
data-key="SalesMonitorTickets"
ref="tableRef"
data-key="saleMonitorTickets"
url="SalesMonitors/salesFilter"
:filter="filter"
:limit="20"
search-url="saleMonitorTickets"
:expr-builder="exprBuilder"
:user-params="params"
:offset="50"
:columns="columns"
:visible-columns="visibleColumns"
:right-search="false"
default-mode="table"
auto-load
@row-click="(_, row) => redirectToTicketSummary(row.id)"
:row-click="({ id }) => openTab(id)"
jorgep marked this conversation as resolved
Review

hay alguna opción para esconde el modo card?

hay alguna opción para esconde el modo card?
Review

El modo card se puede deshabilitar, pero, esconder el icono para cambiar a este no.

El modo card se puede deshabilitar, pero, esconder el icono para cambiar a este no.
:disable-option="{ card: true }"
:user-params="{ from, to, scopeDays: 1 }"
>
<template #top-left>
<TableVisibleColumns
:all-columns="allColumnNames"
table-code="ticketsMonitor"
labels-traductions-path="salesTicketsTable"
@on-config-saved="visibleColumns = [...$event, 'rowActions']"
/>
<QBtn
jorgep marked this conversation as resolved
Review

Añadir tooltip.

Añadir tooltip.
icon="refresh"
size="md"
color="primary"
class="q-mr-sm"
dense
flat
@click="$refs.tableRef.reload()"
>
<QTooltip>{{ $t('globals.refresh') }}</QTooltip>
</QBtn>
<QCheckbox
v-model="autoRefresh"
:label="t('salesTicketsTable.autoRefresh')"
:label="$t('salesTicketsTable.autoRefresh')"
@update:model-value="autoRefreshHandler"
/>
dense
>
<QTooltip>{{ $t('refreshInfo') }}</QTooltip>
</QCheckbox>
</template>
<template #column-problems="{ row }">
<QTd class="no-padding" style="max-width: 50px">
<template #column-totalProblems="{ row }">
<QTd class="no-padding" style="max-width: 60px">
<QIcon
v-if="row.isTaxDataChecked === 0"
name="vn:no036"
color="primary"
size="xs"
>
<QTooltip>{{ t('salesTicketsTable.noVerifiedData') }}</QTooltip>
<QTooltip>{{ $t('salesTicketsTable.noVerifiedData') }}</QTooltip>
</QIcon>
<QIcon
v-if="row.hasTicketRequest"
@ -418,7 +385,7 @@ const redirectToSales = (id) => {
color="primary"
size="xs"
>
<QTooltip>{{ t('salesTicketsTable.purchaseRequest') }}</QTooltip>
<QTooltip>{{ $t('salesTicketsTable.purchaseRequest') }}</QTooltip>
</QIcon>
<QIcon
v-if="row.itemShortage"
@ -426,10 +393,10 @@ const redirectToSales = (id) => {
color="primary"
size="xs"
>
<QTooltip>{{ t('salesTicketsTable.notVisible') }}</QTooltip>
<QTooltip>{{ $t('salesTicketsTable.notVisible') }}</QTooltip>
</QIcon>
<QIcon v-if="row.isFreezed" name="vn:frozen" color="primary" size="xs">
<QTooltip>{{ t('salesTicketsTable.clientFrozen') }}</QTooltip>
<QTooltip>{{ $t('salesTicketsTable.clientFrozen') }}</QTooltip>
</QIcon>
<QIcon
v-if="row.risk"
@ -437,7 +404,9 @@ const redirectToSales = (id) => {
:color="row.hasHighRisk ? 'negative' : 'primary'"
size="xs"
>
<QTooltip>{{ t('salesTicketsTable.risk') }}: {{ row.risk }}</QTooltip>
<QTooltip
>{{ $t('salesTicketsTable.risk') }}: {{ row.risk }}</QTooltip
>
</QIcon>
<QIcon
v-if="row.hasComponentLack"
@ -445,7 +414,7 @@ const redirectToSales = (id) => {
color="primary"
size="xs"
>
<QTooltip>{{ t('salesTicketsTable.componentLack') }}</QTooltip>
<QTooltip>{{ $t('salesTicketsTable.componentLack') }}</QTooltip>
</QIcon>
<QIcon
v-if="row.isTooLittle"
@ -453,11 +422,11 @@ const redirectToSales = (id) => {
color="primary"
size="xs"
>
<QTooltip>{{ t('salesTicketsTable.tooLittle') }}</QTooltip>
<QTooltip>{{ $t('salesTicketsTable.tooLittle') }}</QTooltip>
</QIcon>
</QTd>
</template>
<template #column-identifier="{ row }">
<template #column-id="{ row }">
<QTd class="no-padding">
<span class="link" @click.stop.prevent>
{{ row.id }}
@ -465,19 +434,19 @@ const redirectToSales = (id) => {
</span>
</QTd>
</template>
<template #column-client="{ row }">
<QTd class="no-padding" @click.stop.prevent>
<template #column-clientFk="{ row }">
<QTd class="no-padding" @click.stop :title="row.nickname">
<span class="link">{{ row.nickname }}</span>
<CustomerDescriptorProxy :id="row.clientFk" />
</QTd>
</template>
<template #column-salesPerson="{ row }">
<QTd class="no-padding" @click.stop.prevent>
<span class="link">{{ row.userName }}</span>
<template #column-salesPersonFk="{ row }">
<QTd class="no-padding" @click.stop :title="row.userName">
jorgep marked this conversation as resolved Outdated

Creo que falta añadir dashIfEmpty

Creo que falta añadir dashIfEmpty
<span class="link" v-text="dashIfEmpty(row.userName)" />
<WorkerDescriptorProxy :id="row.salesPersonFk" />
</QTd>
</template>
<template #column-date="{ row }">
<template #column-shippedDate="{ row }">
<QTd class="no-padding">
<QBadge
v-bind="getBadgeAttrs(row.shippedDate)"
@ -488,6 +457,11 @@ const redirectToSales = (id) => {
</QBadge>
</QTd>
</template>
<template #column-provinceFk="{ row }">
<QTd class="no-padding">
<span :title="row.province" v-text="row.province" />
</QTd>
</template>
<template #column-state="{ row }">
<QTd class="no-padding" @click.stop.prevent>
<div v-if="row.refFk">
@ -508,17 +482,17 @@ const redirectToSales = (id) => {
<template #column-isFragile="{ row }">
<QTd class="no-padding">
<QIcon v-if="row.isFragile" name="local_bar" color="primary" size="sm">
<QTooltip>{{ t('salesTicketsTable.isFragile') }}</QTooltip>
<QTooltip>{{ $t('salesTicketsTable.isFragile') }}</QTooltip>
</QIcon>
</QTd>
</template>
<template #column-zone="{ row }">
<QTd class="no-padding" @click.stop.prevent>
<template #column-zoneFk="{ row }">
<QTd class="no-padding" @click.stop.prevent :title="row.zoneName">
<span class="link">{{ row.zoneName }}</span>
<ZoneDescriptorProxy :id="row.zoneFk" />
</QTd>
</template>
<template #column-total="{ row }">
<template #column-totalWithVat="{ row }">
<QTd class="no-padding">
<QBadge
:color="totalPriceColor(row) || 'transparent'"

View File

@ -11,11 +11,14 @@ salesClientsTable:
client: Client
salesOrdersTable:
delete: Delete
date: Date
dateSend: Send date
dateMake: Make date
client: Client
salesPerson: Salesperson
deleteConfirmTitle: Delete selected elements
deleteConfirmMessage: All the selected elements will be deleted. Are you sure you want to continue?
agency: Agency
import: Import
salesTicketsTable:
autoRefresh: Auto-refresh
problems: Problems
@ -43,3 +46,4 @@ salesTicketsTable:
searchBar:
label: Search tickets
info: Search tickets by id or alias
refreshInfo: Toggle auto-refresh every 2 minutes

View File

@ -11,11 +11,14 @@ salesClientsTable:
client: Cliente
salesOrdersTable:
delete: Eliminar
date: Fecha
dateSend: Fecha de envío
dateMake: Fecha de realización
client: Cliente
salesPerson: Comercial
deleteConfirmTitle: Eliminar los elementos seleccionados
deleteConfirmMessage: Todos los elementos seleccionados serán eliminados. ¿Seguro que quieres continuar?
agency: Agencia
import: Importe
salesTicketsTable:
autoRefresh: Auto-refresco
problems: Problemas
@ -43,3 +46,4 @@ salesTicketsTable:
searchBar:
label: Buscar tickets
info: Buscar tickets por identificador o alias
refreshInfo: Conmuta el refresco automático cada 2 minutos

View File

@ -11,7 +11,7 @@ export default {
component: RouterView,
redirect: { name: 'MonitorMain' },
menus: {
main: ['MonitorList'],
main: ['MonitorTickets', 'MonitorClientsActions'],
card: [],
},
children: [
@ -19,16 +19,27 @@ export default {
path: '',
name: 'MonitorMain',
component: () => import('src/components/common/VnSectionMain.vue'),
redirect: { name: 'MonitorList' },
redirect: { name: 'MonitorTickets' },
children: [
{
path: 'list',
name: 'MonitorList',
path: 'tickets',
name: 'MonitorTickets',
meta: {
title: 'list',
icon: 'grid_view',
title: 'ticketsMonitor',
icon: 'vn:ticket',
},
component: () => import('src/pages/Monitor/MonitorList.vue'),
component: () =>
import('src/pages/Monitor/Ticket/MonitorTickets.vue'),
},
{
path: 'clients-actions',
name: 'MonitorClientsActions',
meta: {
title: 'clientsActionsMonitor',
icon: 'vn:client',
},
component: () =>
import('src/pages/Monitor/MonitorClientsActions.vue'),
},
],
},