forked from verdnatura/salix-front
756 lines
26 KiB
Vue
756 lines
26 KiB
Vue
<script setup>
|
|
import { ref, onMounted, computed, watch } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useQuasar } from 'quasar';
|
|
import { useStateStore } from 'stores/useStateStore';
|
|
|
|
import CrudModel from 'src/components/CrudModel.vue';
|
|
import FormModelPopup from 'components/FormModelPopup.vue';
|
|
|
|
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
|
|
import VnTableColumn from 'components/VnTable/VnColumn.vue';
|
|
import VnTableFilter from 'components/VnTable/VnFilter.vue';
|
|
import VnTableChip from 'components/VnTable/VnChip.vue';
|
|
import VnVisibleColumn from 'src/components/VnTable/VnVisibleColumn.vue';
|
|
import VnLv from 'components/ui/VnLv.vue';
|
|
import VnTableOrder from 'src/components/VnTable/VnOrder.vue';
|
|
|
|
const $props = defineProps({
|
|
columns: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
defaultMode: {
|
|
type: String,
|
|
default: 'table', // 'table', 'card'
|
|
},
|
|
columnSearch: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
rightSearch: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
rowClick: {
|
|
type: [Function, Boolean],
|
|
default: null,
|
|
},
|
|
redirect: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
create: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
createAsDialog: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
cardClass: {
|
|
type: String,
|
|
default: 'flex-one',
|
|
},
|
|
searchUrl: {
|
|
type: String,
|
|
default: 'table',
|
|
},
|
|
isEditable: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
useModel: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
hasSubToolbar: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
disableOption: {
|
|
type: Object,
|
|
default: () => ({ card: false, table: false }),
|
|
},
|
|
withoutHeader: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
tableCode: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
table: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
tableHeight: {
|
|
type: String,
|
|
default: '90vh',
|
|
},
|
|
});
|
|
const { t } = useI18n();
|
|
const stateStore = useStateStore();
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const quasar = useQuasar();
|
|
|
|
const DEFAULT_MODE = 'card';
|
|
const TABLE_MODE = 'table';
|
|
const mode = ref(DEFAULT_MODE);
|
|
const selected = ref([]);
|
|
const routeQuery = JSON.parse(route?.query[$props.searchUrl] ?? '{}');
|
|
const params = ref({ ...routeQuery, ...routeQuery.filter?.where });
|
|
const orders = ref(parseOrder(routeQuery.filter?.order));
|
|
const CrudModelRef = ref({});
|
|
const showForm = ref(false);
|
|
const splittedColumns = ref({ columns: [] });
|
|
const columnsVisibilitySkiped = ref();
|
|
const tableModes = [
|
|
{
|
|
icon: 'view_column',
|
|
title: t('table view'),
|
|
value: TABLE_MODE,
|
|
disable: $props.disableOption?.table,
|
|
},
|
|
{
|
|
icon: 'grid_view',
|
|
title: t('grid view'),
|
|
value: DEFAULT_MODE,
|
|
disable: $props.disableOption?.card,
|
|
},
|
|
];
|
|
|
|
onMounted(() => {
|
|
mode.value = quasar.platform.is.mobile ? DEFAULT_MODE : $props.defaultMode;
|
|
stateStore.rightDrawer = true;
|
|
setUserParams(route.query[$props.searchUrl]);
|
|
columnsVisibilitySkiped.value = [
|
|
...splittedColumns.value.columns
|
|
.filter((c) => c.visible == false)
|
|
.map((c) => c.name),
|
|
...['tableActions'],
|
|
];
|
|
});
|
|
|
|
watch(
|
|
() => $props.columns,
|
|
(value) => splitColumns(value),
|
|
{ immediate: true }
|
|
);
|
|
|
|
watch(
|
|
() => route.query[$props.searchUrl],
|
|
(val) => setUserParams(val)
|
|
);
|
|
|
|
const isTableMode = computed(() => mode.value == TABLE_MODE);
|
|
|
|
function setUserParams(watchedParams) {
|
|
if (!watchedParams) return;
|
|
|
|
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
|
|
const filter = JSON.parse(watchedParams?.filter);
|
|
const where = filter?.where;
|
|
const order = filter?.order;
|
|
|
|
watchedParams = { ...watchedParams, ...where };
|
|
delete watchedParams.filter;
|
|
delete params.value?.filter;
|
|
params.value = { ...params.value, ...watchedParams };
|
|
orders.value = parseOrder(order);
|
|
}
|
|
|
|
function splitColumns(columns) {
|
|
splittedColumns.value = {
|
|
columns: [],
|
|
chips: [],
|
|
create: [],
|
|
cardVisible: [],
|
|
};
|
|
|
|
for (const col of columns) {
|
|
if (col.name == 'tableActions') splittedColumns.value.actions = col;
|
|
if (col.chip) splittedColumns.value.chips.push(col);
|
|
if (col.isTitle) splittedColumns.value.title = col;
|
|
if (col.create) splittedColumns.value.create.push(col);
|
|
if (col.cardVisible) splittedColumns.value.cardVisible.push(col);
|
|
if ($props.isEditable && col.disable == null) col.disable = false;
|
|
if ($props.useModel) col.columnFilter = { ...col.columnFilter, inWhere: true };
|
|
splittedColumns.value.columns.push(col);
|
|
}
|
|
// Status column
|
|
if (splittedColumns.value.chips.length) {
|
|
splittedColumns.value.columnChips = splittedColumns.value.chips.filter(
|
|
(c) => !c.isId
|
|
);
|
|
if (splittedColumns.value.columnChips.length)
|
|
splittedColumns.value.columns.unshift({
|
|
align: 'left',
|
|
label: t('status'),
|
|
name: 'tableStatus',
|
|
columnFilter: false,
|
|
orderBy: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
const rowClickFunction = computed(() => {
|
|
if ($props.rowClick != undefined) return $props.rowClick;
|
|
if ($props.redirect) return ({ id }) => redirectFn(id);
|
|
return () => {};
|
|
});
|
|
|
|
function redirectFn(id) {
|
|
router.push({ path: `/${$props.redirect}/${id}` });
|
|
}
|
|
|
|
function stopEventPropagation(event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
|
|
function reload(params) {
|
|
CrudModelRef.value.reload(params);
|
|
}
|
|
|
|
function columnName(col) {
|
|
const column = { ...col, ...col.columnFilter };
|
|
let name = column.name;
|
|
if (column.alias) name = column.alias + '.' + name;
|
|
return name;
|
|
}
|
|
|
|
function getColAlign(col) {
|
|
return 'text-' + (col.align ?? 'left');
|
|
}
|
|
|
|
function parseOrder(urlOrders) {
|
|
const orderObject = {};
|
|
if (!urlOrders) return orderObject;
|
|
if (typeof urlOrders == 'string') urlOrders = [urlOrders];
|
|
for (const [index, orders] of urlOrders.entries()) {
|
|
const [name, direction] = orders.split(' ');
|
|
orderObject[name] = { direction, index: index + 1 };
|
|
}
|
|
return orderObject;
|
|
}
|
|
|
|
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
|
|
defineExpose({
|
|
reload,
|
|
redirect: redirectFn,
|
|
selected,
|
|
});
|
|
|
|
function handleOnDataSaved(_, res) {
|
|
if (_.onDataSaved) _.onDataSaved(this);
|
|
else $props.create.onDataSaved(_);
|
|
}
|
|
</script>
|
|
<template>
|
|
<QDrawer
|
|
v-if="$props.rightSearch"
|
|
v-model="stateStore.rightDrawer"
|
|
side="right"
|
|
:width="256"
|
|
show-if-above
|
|
>
|
|
<QScrollArea class="fit">
|
|
<VnFilterPanel
|
|
:data-key="$attrs['data-key']"
|
|
:search-button="true"
|
|
v-model="params"
|
|
:search-url="searchUrl"
|
|
:redirect="!!redirect"
|
|
>
|
|
<template #body>
|
|
<div
|
|
class="row no-wrap flex-center"
|
|
v-for="col of splittedColumns.columns"
|
|
:key="col.id"
|
|
>
|
|
<VnTableFilter
|
|
:column="col"
|
|
:data-key="$attrs['data-key']"
|
|
v-model="params[columnName(col)]"
|
|
:search-url="searchUrl"
|
|
/>
|
|
<VnTableOrder
|
|
v-model="orders[col.name]"
|
|
:name="col.orderBy ?? col.name"
|
|
:data-key="$attrs['data-key']"
|
|
:search-url="searchUrl"
|
|
:vertical="true"
|
|
/>
|
|
</div>
|
|
<slot
|
|
name="moreFilterPanel"
|
|
:params="params"
|
|
:columns="splittedColumns.columns"
|
|
/>
|
|
</template>
|
|
</VnFilterPanel>
|
|
</QScrollArea>
|
|
</QDrawer>
|
|
<!-- class in div to fix warn-->
|
|
<div class="q-px-md">
|
|
<CrudModel
|
|
v-bind="$attrs"
|
|
:limit="20"
|
|
ref="CrudModelRef"
|
|
:search-url="searchUrl"
|
|
:disable-infinite-scroll="isTableMode"
|
|
@save-changes="reload"
|
|
:has-sub-toolbar="$attrs['hasSubToolbar'] ?? isEditable"
|
|
>
|
|
<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)"
|
|
>
|
|
<template #top-left v-if="!$props.withoutHeader">
|
|
<slot name="top-left"></slot>
|
|
</template>
|
|
<template #top-right>
|
|
<VnVisibleColumn
|
|
v-if="isTableMode"
|
|
v-model="splittedColumns.columns"
|
|
:table-code="tableCode ?? route.name"
|
|
:skip="columnsVisibilitySkiped"
|
|
/>
|
|
<QBtnToggle
|
|
v-model="mode"
|
|
toggle-color="primary"
|
|
class="bg-vn-section-color"
|
|
dense
|
|
:options="tableModes"
|
|
/>
|
|
<QBtn
|
|
v-if="$props.rightSearch"
|
|
icon="filter_alt"
|
|
title="asd"
|
|
class="bg-vn-section-color q-ml-md"
|
|
dense
|
|
@click="stateStore.toggleRightDrawer()"
|
|
/>
|
|
</template>
|
|
<template #header-cell="{ col }">
|
|
<QTh v-if="col.visible ?? true" auto-width>
|
|
<div
|
|
class="column self-start q-ml-xs ellipsis"
|
|
:class="`text-${col?.align ?? 'left'}`"
|
|
style="height: 75px"
|
|
>
|
|
<div
|
|
class="row items-center no-wrap"
|
|
style="height: 30px"
|
|
>
|
|
<VnTableOrder
|
|
v-model="orders[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"
|
|
:data-key="$attrs['data-key']"
|
|
v-model="params[columnName(col)]"
|
|
:search-url="searchUrl"
|
|
/>
|
|
</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 }">
|
|
<!-- Columns -->
|
|
<QTd
|
|
auto-width
|
|
class="no-margin q-px-xs"
|
|
:class="[getColAlign(col), col.class, col.columnField?.class]"
|
|
v-if="col.visible ?? true"
|
|
>
|
|
<slot :name="`column-${col.name}`" :col="col" :row="row">
|
|
<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 '
|
|
"
|
|
@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
|
|
v-if="splittedColumns.chips.length"
|
|
class="no-margin q-px-xs q-py-none"
|
|
>
|
|
<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"
|
|
>
|
|
<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"
|
|
>
|
|
<div
|
|
v-for="col of splittedColumns.cardVisible"
|
|
:key="col.name"
|
|
class="fields"
|
|
>
|
|
<VnLv
|
|
:label="
|
|
!col.component && col.label
|
|
? `${col.label}:`
|
|
: ''
|
|
"
|
|
>
|
|
<template #value>
|
|
<span
|
|
@click="
|
|
stopEventPropagation($event)
|
|
"
|
|
>
|
|
<slot
|
|
:name="`column-${col.name}`"
|
|
:col="col"
|
|
:row="row"
|
|
>
|
|
<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>
|
|
</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>
|
|
<QPageSticky :offset="[20, 20]" style="z-index: 2">
|
|
<QBtn
|
|
@click="
|
|
() =>
|
|
createAsDialog ? (showForm = !showForm) : handleOnDataSaved(create)
|
|
"
|
|
color="primary"
|
|
fab
|
|
icon="add"
|
|
/>
|
|
<QTooltip>
|
|
{{ create.title }}
|
|
</QTooltip>
|
|
</QPageSticky>
|
|
<QDialog v-model="showForm" transition-show="scale" transition-hide="scale">
|
|
<FormModelPopup
|
|
v-bind="create"
|
|
:model="$attrs['data-key'] + 'Create'"
|
|
@on-data-saved="handleOnDataSaved"
|
|
>
|
|
<template #form-inputs="{ data }">
|
|
<div class="grid-create">
|
|
<VnTableColumn
|
|
v-for="column of splittedColumns.create"
|
|
:key="column.name"
|
|
:column="column"
|
|
:row="{}"
|
|
default="input"
|
|
v-model="data[column.name]"
|
|
:show-label="true"
|
|
component-prop="columnCreate"
|
|
/>
|
|
<slot name="more-create-dialog" :data="data" />
|
|
</div>
|
|
</template>
|
|
</FormModelPopup>
|
|
</QDialog>
|
|
</template>
|
|
<i18n>
|
|
en:
|
|
status: Status
|
|
table view: Table view
|
|
grid view: Grid view
|
|
es:
|
|
status: Estados
|
|
table view: Vista en tabla
|
|
grid view: Vista en cuadrícula
|
|
</i18n>
|
|
|
|
<style lang="scss">
|
|
.bg-chip-secondary {
|
|
background-color: var(--vn-page-color);
|
|
color: var(--vn-text-color);
|
|
}
|
|
|
|
.bg-header {
|
|
background-color: var(--vn-header-color);
|
|
color: var(--vn-text-color);
|
|
}
|
|
|
|
.color-vn-text {
|
|
color: var(--vn-text-color);
|
|
}
|
|
|
|
.q-table--dark .q-table__bottom,
|
|
.q-table--dark thead,
|
|
.q-table--dark tr,
|
|
.q-table--dark th,
|
|
.q-table--dark td {
|
|
border-color: var(--vn-section-color);
|
|
}
|
|
|
|
.q-table__container > div:first-child {
|
|
background-color: var(--vn-page-color);
|
|
}
|
|
|
|
.grid-three {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, max-content));
|
|
max-width: 100%;
|
|
grid-gap: 20px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.grid-create {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
|
|
max-width: 100%;
|
|
grid-gap: 20px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.flex-one {
|
|
display: flex;
|
|
flex-flow: row wrap;
|
|
div.fields {
|
|
width: 100%;
|
|
.vn-label-value {
|
|
display: flex;
|
|
gap: 2%;
|
|
}
|
|
}
|
|
}
|
|
|
|
.q-table th {
|
|
padding: 0;
|
|
}
|
|
|
|
.vnTable {
|
|
thead tr th {
|
|
position: sticky;
|
|
z-index: 2;
|
|
}
|
|
thead tr:first-child th {
|
|
top: 0;
|
|
}
|
|
.q-table__top {
|
|
top: 0;
|
|
}
|
|
tbody {
|
|
.q-checkbox {
|
|
display: flex;
|
|
margin-bottom: 9px;
|
|
& .q-checkbox__label {
|
|
margin-left: 31px;
|
|
color: var(--vn-text-color);
|
|
}
|
|
& .q-checkbox__inner {
|
|
position: absolute;
|
|
left: 0;
|
|
color: var(--vn-label-color);
|
|
}
|
|
}
|
|
}
|
|
.sticky {
|
|
position: sticky;
|
|
right: 0;
|
|
}
|
|
td.sticky {
|
|
background-color: var(--vn-section-color);
|
|
z-index: 1;
|
|
}
|
|
}
|
|
|
|
.vn-label-value {
|
|
display: flex;
|
|
flex-direction: row;
|
|
color: var(--vn-text-color);
|
|
.value {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
pointer-events: all;
|
|
cursor: text;
|
|
user-select: all;
|
|
}
|
|
}
|
|
|
|
.cardEllipsis {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.grid-two {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, max-content));
|
|
max-width: 100%;
|
|
margin: 0 auto;
|
|
overflow: scroll;
|
|
white-space: wrap;
|
|
width: 100%;
|
|
}
|
|
|
|
.w-80 {
|
|
width: 80%;
|
|
}
|
|
|
|
.w-20 {
|
|
width: 20%;
|
|
}
|
|
|
|
.cursor-text {
|
|
pointer-events: all;
|
|
cursor: text;
|
|
user-select: all;
|
|
}
|
|
</style>
|