forked from verdnatura/salix-front
653 lines
22 KiB
Vue
653 lines
22 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 FormModelPopup from 'components/FormModelPopup.vue';
|
|
import CrudModel from 'src/components/CrudModel.vue';
|
|
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
|
|
import VnLv from 'components/ui/VnLv.vue';
|
|
|
|
import VnTableColumn from 'components/VnTable/VnColumn.vue';
|
|
import VnTableFilter from 'components/VnTable/VnFilter.vue';
|
|
import VnTableChip from 'components/VnTable/VnChip.vue';
|
|
|
|
const $props = defineProps({
|
|
columns: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
defaultMode: {
|
|
type: String,
|
|
default: 'card', // 'table', 'card'
|
|
},
|
|
columnSearch: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
rightSearch: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
rowClick: {
|
|
type: Function,
|
|
default: null,
|
|
},
|
|
redirect: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
create: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
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,
|
|
},
|
|
});
|
|
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 CrudModelRef = ref({});
|
|
const showForm = ref(false);
|
|
const splittedColumns = ref({ columns: [] });
|
|
const tableModes = [
|
|
{
|
|
icon: 'view_column',
|
|
title: t('table view'),
|
|
value: TABLE_MODE,
|
|
},
|
|
{
|
|
icon: 'grid_view',
|
|
title: t('grid view'),
|
|
value: DEFAULT_MODE,
|
|
},
|
|
];
|
|
|
|
onMounted(() => {
|
|
mode.value = quasar.platform.is.mobile ? DEFAULT_MODE : $props.defaultMode;
|
|
stateStore.rightDrawer = true;
|
|
setUserParams(route.query[$props.searchUrl]);
|
|
});
|
|
|
|
watch(
|
|
() => $props.columns,
|
|
(value) => splitColumns(value),
|
|
{ immediate: true }
|
|
);
|
|
|
|
watch(
|
|
() => route.query[$props.searchUrl],
|
|
(val) => setUserParams(val)
|
|
);
|
|
|
|
function setUserParams(watchedParams) {
|
|
if (!watchedParams) return;
|
|
|
|
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
|
|
const where = JSON.parse(watchedParams?.filter)?.where;
|
|
watchedParams = { ...watchedParams, ...where };
|
|
delete watchedParams.filter;
|
|
params.value = { ...params.value, ...watchedParams };
|
|
}
|
|
|
|
function splitColumns(columns) {
|
|
splittedColumns.value = {
|
|
columns: [],
|
|
chips: [],
|
|
create: [],
|
|
visible: [],
|
|
};
|
|
|
|
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.visible.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,
|
|
});
|
|
}
|
|
}
|
|
|
|
const rowClickFunction = computed(() => {
|
|
if ($props.rowClick) 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');
|
|
}
|
|
|
|
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
|
|
defineExpose({
|
|
reload,
|
|
redirect: redirectFn,
|
|
selected,
|
|
});
|
|
</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"
|
|
:disable-submit-event="true"
|
|
:search-url="searchUrl"
|
|
>
|
|
<template #body>
|
|
<VnTableFilter
|
|
:column="col"
|
|
:data-key="$attrs['data-key']"
|
|
v-for="col of splittedColumns.columns"
|
|
:key="col.id"
|
|
v-model="params[columnName(col)]"
|
|
:search-url="searchUrl"
|
|
/>
|
|
</template>
|
|
<slot
|
|
name="moreFilterPanel"
|
|
:params="params"
|
|
:columns="splittedColumns.columns"
|
|
/>
|
|
</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="mode == TABLE_MODE"
|
|
@save-changes="reload"
|
|
:has-subtoolbar="$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="$attrs['q-table']"
|
|
class="vnTable"
|
|
:columns="splittedColumns.columns"
|
|
:rows="rows"
|
|
v-model:selected="selected"
|
|
:grid="mode != TABLE_MODE"
|
|
table-header-class="bg-header"
|
|
card-container-class="grid-three"
|
|
flat
|
|
:style="mode == TABLE_MODE && 'max-height: 90vh'"
|
|
virtual-scroll
|
|
@virtual-scroll="
|
|
(event) =>
|
|
event.index > rows.length - 2 &&
|
|
CrudModelRef.vnPaginateRef.paginate()
|
|
"
|
|
@row-click="(_, row) => rowClickFunction(row)"
|
|
@update:selected="emit('update:selected', $event)"
|
|
>
|
|
<template #top-left>
|
|
<slot name="top-left"></slot>
|
|
</template>
|
|
<template #top-right>
|
|
<!-- <QBtn
|
|
icon="visibility"
|
|
title="asd"
|
|
class="bg-vn-section-color q-mr-md"
|
|
dense
|
|
v-if="mode == 'table'"
|
|
/> -->
|
|
<QBtnToggle
|
|
v-model="mode"
|
|
toggle-color="primary"
|
|
class="bg-vn-section-color"
|
|
dense
|
|
:options="tableModes"
|
|
/>
|
|
<QBtn
|
|
icon="filter_alt"
|
|
title="asd"
|
|
class="bg-vn-section-color q-ml-md"
|
|
dense
|
|
@click="stateStore.toggleRightDrawer()"
|
|
/>
|
|
</template>
|
|
<template #header-cell="{ col }">
|
|
<QTh
|
|
auto-width
|
|
style="min-width: 100px"
|
|
v-if="$props.columnSearch"
|
|
>
|
|
<VnTableFilter
|
|
:column="col"
|
|
:show-title="true"
|
|
:data-key="$attrs['data-key']"
|
|
v-model="params[columnName(col)]"
|
|
:search-url="searchUrl"
|
|
/>
|
|
</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)"
|
|
>
|
|
<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"
|
|
/>
|
|
</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.visible"
|
|
: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 v-if="create" :offset="[20, 20]" style="z-index: 2">
|
|
<QBtn @click="showForm = !showForm" 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="(_, res) => create.onDataSaved(res)"
|
|
>
|
|
<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
|
|
es:
|
|
status: Estados
|
|
</i18n>
|
|
|
|
<style lang="scss">
|
|
.bg-chip-secondary {
|
|
background-color: var(--vn-page-color);
|
|
color: var(--vn-text-color);
|
|
}
|
|
|
|
.bg-header {
|
|
background-color: #5d5d5d;
|
|
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: #222222;
|
|
}
|
|
|
|
.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(--q-dark);
|
|
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>
|