feat(leads): gather filters in one popover like activities
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -360,7 +360,7 @@ function place() {
|
|||||||
|
|
||||||
function liftAboveOverlays() {
|
function liftAboveOverlays() {
|
||||||
let z = 120;
|
let z = 120;
|
||||||
for (const el of document.querySelectorAll('.ant-modal-wrap, .ant-drawer')) {
|
for (const el of document.querySelectorAll('.ant-modal-wrap, .ant-drawer, .ant-popover')) {
|
||||||
const zi = Number.parseInt(window.getComputedStyle(el).zIndex, 10);
|
const zi = Number.parseInt(window.getComputedStyle(el).zIndex, 10);
|
||||||
if (Number.isFinite(zi)) z = Math.max(z, zi + 20);
|
if (Number.isFinite(zi)) z = Math.max(z, zi + 20);
|
||||||
}
|
}
|
||||||
|
|||||||
+432
-202
@@ -5,12 +5,10 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
Alert as AAlert,
|
Alert as AAlert,
|
||||||
Avatar as AAvatar,
|
Avatar as AAvatar,
|
||||||
Badge as ABadge,
|
|
||||||
Button as AButton,
|
Button as AButton,
|
||||||
Card as ACard,
|
Card as ACard,
|
||||||
Checkbox as ACheckbox,
|
Checkbox as ACheckbox,
|
||||||
Col as ACol,
|
Col as ACol,
|
||||||
Drawer as ADrawer,
|
|
||||||
Dropdown as ADropdown,
|
Dropdown as ADropdown,
|
||||||
Empty as AEmpty,
|
Empty as AEmpty,
|
||||||
Flex as AFlex,
|
Flex as AFlex,
|
||||||
@@ -24,6 +22,7 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
Modal as AModal,
|
Modal as AModal,
|
||||||
Pagination as APagination,
|
Pagination as APagination,
|
||||||
|
Popover as APopover,
|
||||||
RadioButton as ARadioButton,
|
RadioButton as ARadioButton,
|
||||||
RadioGroup as ARadioGroup,
|
RadioGroup as ARadioGroup,
|
||||||
Row as ARow,
|
Row as ARow,
|
||||||
@@ -41,6 +40,7 @@ import type { TableColumnsType, UploadProps } from 'ant-design-vue';
|
|||||||
import {
|
import {
|
||||||
AppstoreOutlined,
|
AppstoreOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
|
DownOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
ExportOutlined,
|
ExportOutlined,
|
||||||
FileExcelOutlined,
|
FileExcelOutlined,
|
||||||
@@ -55,6 +55,7 @@ import { api } from '@/lib/api';
|
|||||||
import {
|
import {
|
||||||
formatJalaliNumeric,
|
formatJalaliNumeric,
|
||||||
formatRelativeContact,
|
formatRelativeContact,
|
||||||
|
parseLocalInput,
|
||||||
toFaDigits,
|
toFaDigits,
|
||||||
} from '@/lib/jalali';
|
} from '@/lib/jalali';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
@@ -298,8 +299,12 @@ const assigneeSelectValue = computed(() => {
|
|||||||
if (unassigned.value) return UNASSIGNED_VALUE;
|
if (unassigned.value) return UNASSIGNED_VALUE;
|
||||||
return assigneeId.value || undefined;
|
return assigneeId.value || undefined;
|
||||||
});
|
});
|
||||||
const moreFiltersCount = computed(() =>
|
/** شمارش فیلترهای داخل پاپآور، مثل فعالیتها. */
|
||||||
|
const mobileFilterBadgeCount = computed(() =>
|
||||||
[
|
[
|
||||||
|
stageId.value,
|
||||||
|
assigneeSelectValue.value ? '1' : '',
|
||||||
|
followUpFilter.value,
|
||||||
cityFilter.value,
|
cityFilter.value,
|
||||||
regionFilter.value,
|
regionFilter.value,
|
||||||
tradeFilter.value,
|
tradeFilter.value,
|
||||||
@@ -310,17 +315,204 @@ const moreFiltersCount = computed(() =>
|
|||||||
convertedFilter.value ? '1' : '',
|
convertedFilter.value ? '1' : '',
|
||||||
].filter(Boolean).length,
|
].filter(Boolean).length,
|
||||||
);
|
);
|
||||||
/** در موبایل مرتبسازی و مسئول و پیگیری هم داخل کشو هستند. */
|
const filterBtnLabel = computed(() =>
|
||||||
const mobileFilterBadgeCount = computed(() => {
|
mobileFilterBadgeCount.value
|
||||||
const extra = isMobile.value
|
? `فیلتر (${toFaDigits(mobileFilterBadgeCount.value)})`
|
||||||
? [
|
: 'فیلتر',
|
||||||
sort.value !== 'updated' ? '1' : '',
|
);
|
||||||
assigneeSelectValue.value ? '1' : '',
|
|
||||||
followUpFilter.value,
|
type LeadFacetKey = 'assignee' | 'stage' | 'source' | 'followUp' | 'city' | 'region' | 'trade';
|
||||||
].filter(Boolean).length
|
const filterSection = ref<LeadFacetKey | 'createdFrom' | 'createdTo' | 'converted' | 'archived' | null>(
|
||||||
: 0;
|
null,
|
||||||
return moreFiltersCount.value + extra;
|
);
|
||||||
});
|
const filterQuery = ref('');
|
||||||
|
const FACET_LIST_CAP = 40;
|
||||||
|
|
||||||
|
function normFilter(value: string) {
|
||||||
|
return value.trim().replace(/ي/g, 'ی').replace(/ك/g, 'ک').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFilterSection(name: NonNullable<typeof filterSection.value>) {
|
||||||
|
filterSection.value = filterSection.value === name ? null : name;
|
||||||
|
filterQuery.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterAnchor = ref<HTMLElement | null>(null);
|
||||||
|
const filterPopoverMaxHeight = ref('min(70vh, 32rem)');
|
||||||
|
|
||||||
|
function placeFilterPopover() {
|
||||||
|
const el = filterAnchor.value;
|
||||||
|
if (!el) return;
|
||||||
|
const space = Math.floor(window.innerHeight - el.getBoundingClientRect().bottom - 12);
|
||||||
|
filterPopoverMaxHeight.value = `${Math.max(220, space)}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFilterPopover(open: boolean) {
|
||||||
|
if (!open && document.getElementById('jalali-picker')) return;
|
||||||
|
if (open) placeFilterPopover();
|
||||||
|
moreFiltersOpen.value = open;
|
||||||
|
if (open) {
|
||||||
|
window.addEventListener('resize', placeFilterPopover);
|
||||||
|
window.addEventListener('scroll', placeFilterPopover, true);
|
||||||
|
} else {
|
||||||
|
filterSection.value = null;
|
||||||
|
window.removeEventListener('resize', placeFilterPopover);
|
||||||
|
window.removeEventListener('scroll', placeFilterPopover, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateFacetLabel(value: string) {
|
||||||
|
if (!value) return 'همه';
|
||||||
|
const parsed = parseLocalInput(value);
|
||||||
|
return parsed ? formatJalaliNumeric(parsed) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function facetValueStyle(active: boolean) {
|
||||||
|
return {
|
||||||
|
color: active ? token.value.colorPrimary : token.value.colorTextQuaternary,
|
||||||
|
fontWeight: active ? 700 : 400,
|
||||||
|
maxWidth: '9.5rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textFacets = computed(() => [
|
||||||
|
{
|
||||||
|
key: 'assignee' as const,
|
||||||
|
label: 'کارشناس',
|
||||||
|
value: unassigned.value
|
||||||
|
? 'بدون کارشناس'
|
||||||
|
: users.value.find((user) => user.id === assigneeId.value)?.fullName || 'همه',
|
||||||
|
active: Boolean(assigneeSelectValue.value),
|
||||||
|
current: assigneeSelectValue.value || '',
|
||||||
|
searchable: true,
|
||||||
|
options: [
|
||||||
|
{ value: '', label: 'همه' },
|
||||||
|
{ value: UNASSIGNED_VALUE, label: 'بدون کارشناس' },
|
||||||
|
...users.value.map((user) => ({ value: user.id, label: user.fullName })),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'stage' as const,
|
||||||
|
label: 'مرحله',
|
||||||
|
value: stages.value.find((stage) => stage.id === stageId.value)?.name || 'همه',
|
||||||
|
active: Boolean(stageId.value),
|
||||||
|
current: stageId.value,
|
||||||
|
searchable: stages.value.length > 8,
|
||||||
|
options: [
|
||||||
|
{ value: '', label: 'همه' },
|
||||||
|
...stages.value.map((stage) => ({ value: stage.id, label: stage.name })),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'source' as const,
|
||||||
|
label: 'منبع',
|
||||||
|
value: sourceFilter.value || 'همه',
|
||||||
|
active: Boolean(sourceFilter.value),
|
||||||
|
current: sourceFilter.value,
|
||||||
|
searchable: sourceOptions.value.length > 8,
|
||||||
|
options: [
|
||||||
|
{ value: '', label: 'همه' },
|
||||||
|
...sourceOptions.value.map((item) => ({ value: item, label: item })),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'followUp' as const,
|
||||||
|
label: 'پیگیری',
|
||||||
|
value:
|
||||||
|
FOLLOW_UP_OPTIONS.find((item) => item.value === followUpFilter.value && item.value)?.label ||
|
||||||
|
'همه',
|
||||||
|
active: Boolean(followUpFilter.value),
|
||||||
|
current: followUpFilter.value,
|
||||||
|
searchable: false,
|
||||||
|
options: [
|
||||||
|
{ value: '', label: 'همه' },
|
||||||
|
...FOLLOW_UP_OPTIONS.filter((item) => item.value).map((item) => ({
|
||||||
|
value: item.value,
|
||||||
|
label: item.label,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'city' as const,
|
||||||
|
label: 'شهر',
|
||||||
|
value: cityFilter.value || 'همه',
|
||||||
|
active: Boolean(cityFilter.value),
|
||||||
|
current: cityFilter.value,
|
||||||
|
searchable: cityOptions.value.length > 8,
|
||||||
|
options: [
|
||||||
|
{ value: '', label: 'همه' },
|
||||||
|
...cityOptions.value.map((item) => ({ value: item, label: item })),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'region' as const,
|
||||||
|
label: 'منطقه',
|
||||||
|
value: regionFilter.value || 'همه',
|
||||||
|
active: Boolean(regionFilter.value),
|
||||||
|
current: regionFilter.value,
|
||||||
|
searchable: regionOptions.value.length > 8,
|
||||||
|
options: [
|
||||||
|
{ value: '', label: 'همه' },
|
||||||
|
...regionOptions.value.map((item) => ({ value: item, label: item })),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'trade' as const,
|
||||||
|
label: 'صنف',
|
||||||
|
value: tradeFilter.value || 'همه',
|
||||||
|
active: Boolean(tradeFilter.value),
|
||||||
|
current: tradeFilter.value,
|
||||||
|
searchable: tradeOptions.value.length > 8,
|
||||||
|
options: [
|
||||||
|
{ value: '', label: 'همه' },
|
||||||
|
...tradeOptions.value.map((item) => ({ value: item, label: item })),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
function visibleFacetOptions(options: Array<{ value: string; label: string }>) {
|
||||||
|
const query = normFilter(filterQuery.value);
|
||||||
|
const rest = options.filter(
|
||||||
|
(item) => item.value !== '' && (!query || normFilter(item.label).includes(query)),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
options: [...options.filter((item) => item.value === ''), ...rest.slice(0, FACET_LIST_CAP)],
|
||||||
|
hidden: Math.max(0, rest.length - FACET_LIST_CAP),
|
||||||
|
empty: Boolean(query) && rest.length === 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFacet(key: LeadFacetKey, value: string) {
|
||||||
|
const facet = textFacets.value.find((item) => item.key === key);
|
||||||
|
const next = facet && facet.current === value ? '' : value;
|
||||||
|
if (key === 'assignee') setAssigneeFilter(next || undefined);
|
||||||
|
else if (key === 'stage') setStageFilter(next);
|
||||||
|
else if (key === 'source') setSourceFilter(next);
|
||||||
|
else if (key === 'followUp') setFollowUpFilter(next || undefined);
|
||||||
|
else if (key === 'city') setCityFilter(next);
|
||||||
|
else if (key === 'region') setRegionFilter(next);
|
||||||
|
else if (key === 'trade') setTradeFilter(next);
|
||||||
|
filterSection.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFlag(which: 'converted' | 'archived', on: boolean) {
|
||||||
|
const current = which === 'converted' ? convertedFilter.value : archivedFilter.value;
|
||||||
|
const next = on ? !current : false;
|
||||||
|
if (which === 'converted') setConvertedFilter(next);
|
||||||
|
else setArchivedFilter(next);
|
||||||
|
filterSection.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function facetOptionStyle(selected: boolean) {
|
||||||
|
return {
|
||||||
|
textAlign: 'start' as const,
|
||||||
|
background: selected ? token.value.colorPrimaryBg : 'transparent',
|
||||||
|
color: selected ? token.value.colorPrimary : token.value.colorText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const total = computed(() => data.value?.total ?? 0);
|
const total = computed(() => data.value?.total ?? 0);
|
||||||
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / PAGE_SIZE)));
|
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / PAGE_SIZE)));
|
||||||
@@ -647,6 +839,10 @@ function setConvertedFilter(checked: boolean) {
|
|||||||
|
|
||||||
function clearMoreFilters() {
|
function clearMoreFilters() {
|
||||||
patchQuery({
|
patchQuery({
|
||||||
|
stageId: undefined,
|
||||||
|
assigneeId: undefined,
|
||||||
|
unassigned: undefined,
|
||||||
|
followUp: undefined,
|
||||||
city: undefined,
|
city: undefined,
|
||||||
region: undefined,
|
region: undefined,
|
||||||
trade: undefined,
|
trade: undefined,
|
||||||
@@ -655,16 +851,9 @@ function clearMoreFilters() {
|
|||||||
createdTo: undefined,
|
createdTo: undefined,
|
||||||
archived: undefined,
|
archived: undefined,
|
||||||
converted: undefined,
|
converted: undefined,
|
||||||
...(isMobile.value
|
|
||||||
? {
|
|
||||||
sort: undefined,
|
|
||||||
assigneeId: undefined,
|
|
||||||
unassigned: undefined,
|
|
||||||
followUp: undefined,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
page: undefined,
|
page: undefined,
|
||||||
});
|
});
|
||||||
|
filterSection.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPage(next: number) {
|
function setPage(next: number) {
|
||||||
@@ -1342,6 +1531,8 @@ onMounted(async () => {
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stopImportPoll();
|
stopImportPoll();
|
||||||
revokeLogoUrls();
|
revokeLogoUrls();
|
||||||
|
window.removeEventListener('resize', placeFilterPopover);
|
||||||
|
window.removeEventListener('scroll', placeFilterPopover, true);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -1507,12 +1698,12 @@ onBeforeUnmount(() => {
|
|||||||
</a-space>
|
</a-space>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
|
|
||||||
<a-flex v-if="isMobile" vertical :gap="8">
|
<a-flex :vertical="isMobile" align="center" wrap="wrap" :gap="8">
|
||||||
<a-input-search
|
<a-input-search
|
||||||
v-model:value="q"
|
v-model:value="q"
|
||||||
allow-clear
|
allow-clear
|
||||||
placeholder="جستجو نام، موبایل، شهر…"
|
placeholder="جستجو نام، موبایل، شهر…"
|
||||||
style="width: 100%"
|
:style="isMobile ? { width: '100%' } : { width: '15rem', maxWidth: '100%' }"
|
||||||
@search="onSearch"
|
@search="onSearch"
|
||||||
>
|
>
|
||||||
<template #enterButton>
|
<template #enterButton>
|
||||||
@@ -1521,195 +1712,234 @@ onBeforeUnmount(() => {
|
|||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
</a-input-search>
|
</a-input-search>
|
||||||
<a-badge
|
<div ref="filterAnchor" :style="isMobile ? { width: '100%' } : undefined">
|
||||||
:count="mobileFilterBadgeCount"
|
<a-popover
|
||||||
:offset="[-2, 2]"
|
:open="moreFiltersOpen"
|
||||||
:style="{ width: '100%', display: 'block' }"
|
trigger="click"
|
||||||
>
|
placement="bottomRight"
|
||||||
<a-button block @click="moreFiltersOpen = true">
|
:arrow="false"
|
||||||
<template #icon><FilterOutlined /></template>
|
:auto-adjust-overflow="false"
|
||||||
فیلترها
|
:overlay-inner-style="{
|
||||||
</a-button>
|
width: 'min(20rem, calc(100vw - 1.5rem))',
|
||||||
</a-badge>
|
maxHeight: filterPopoverMaxHeight,
|
||||||
</a-flex>
|
overflow: 'auto',
|
||||||
<a-flex v-else align="center" wrap="wrap" :gap="8">
|
}"
|
||||||
<a-input-search
|
@open-change="onFilterPopover"
|
||||||
v-model:value="q"
|
>
|
||||||
allow-clear
|
<a-button
|
||||||
placeholder="جستجو نام، موبایل، شهر…"
|
:block="isMobile"
|
||||||
style="width: 15rem; max-width: 100%"
|
:style="mobileFilterBadgeCount
|
||||||
@search="onSearch"
|
? {
|
||||||
>
|
borderColor: token.colorPrimary,
|
||||||
<template #enterButton>
|
color: token.colorPrimary,
|
||||||
<a-button type="primary">
|
background: token.colorPrimaryBg,
|
||||||
<template #icon><SearchOutlined /></template>
|
}
|
||||||
|
: undefined"
|
||||||
|
>
|
||||||
|
<a-flex align="center" justify="center" :gap="6">
|
||||||
|
<FilterOutlined />
|
||||||
|
<span>{{ filterBtnLabel }}</span>
|
||||||
|
<DownOutlined />
|
||||||
|
</a-flex>
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
<template #content>
|
||||||
</a-input-search>
|
<a-flex vertical :gap="10" @click.stop>
|
||||||
<a-badge :count="mobileFilterBadgeCount" :offset="[-2, 2]">
|
<div v-for="facet in textFacets" :key="facet.key">
|
||||||
<a-button @click="moreFiltersOpen = true">
|
<div
|
||||||
<template #icon><FilterOutlined /></template>
|
role="button"
|
||||||
فیلترها
|
tabindex="0"
|
||||||
</a-button>
|
:style="{ cursor: 'pointer' }"
|
||||||
</a-badge>
|
@click="toggleFilterSection(facet.key)"
|
||||||
|
@keydown.enter.prevent="toggleFilterSection(facet.key)"
|
||||||
|
>
|
||||||
|
<a-flex justify="space-between" align="center">
|
||||||
|
<span>{{ facet.label }}</span>
|
||||||
|
<a-typography-text :style="facetValueStyle(facet.active)">
|
||||||
|
{{ facet.value }}
|
||||||
|
</a-typography-text>
|
||||||
|
</a-flex>
|
||||||
|
</div>
|
||||||
|
<a-flex
|
||||||
|
v-if="filterSection === facet.key"
|
||||||
|
vertical
|
||||||
|
:gap="4"
|
||||||
|
:style="{ marginTop: '6px' }"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-if="facet.searchable"
|
||||||
|
v-model:value="filterQuery"
|
||||||
|
allow-clear
|
||||||
|
placeholder="جستجو"
|
||||||
|
/>
|
||||||
|
<div :style="{ maxHeight: '11.5rem', overflow: 'auto' }">
|
||||||
|
<a-button
|
||||||
|
v-for="option in visibleFacetOptions(facet.options).options"
|
||||||
|
:key="option.value || 'all'"
|
||||||
|
type="text"
|
||||||
|
block
|
||||||
|
:style="facetOptionStyle(facet.current === option.value)"
|
||||||
|
@click="pickFacet(facet.key, option.value)"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</a-button>
|
||||||
|
<a-typography-text
|
||||||
|
v-if="visibleFacetOptions(facet.options).empty"
|
||||||
|
type="secondary"
|
||||||
|
>
|
||||||
|
موردی پیدا نشد
|
||||||
|
</a-typography-text>
|
||||||
|
</div>
|
||||||
|
<a-typography-text
|
||||||
|
v-if="visibleFacetOptions(facet.options).hidden"
|
||||||
|
type="secondary"
|
||||||
|
>
|
||||||
|
برای دیدن بقیه، نام را دقیقتر بنویسید
|
||||||
|
</a-typography-text>
|
||||||
|
</a-flex>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
:style="{ cursor: 'pointer' }"
|
||||||
|
@click="toggleFilterSection('createdFrom')"
|
||||||
|
@keydown.enter.prevent="toggleFilterSection('createdFrom')"
|
||||||
|
>
|
||||||
|
<a-flex justify="space-between" align="center">
|
||||||
|
<span>از تاریخ ثبت</span>
|
||||||
|
<a-typography-text :style="facetValueStyle(Boolean(createdFromFilter))">
|
||||||
|
{{ dateFacetLabel(createdFromFilter) }}
|
||||||
|
</a-typography-text>
|
||||||
|
</a-flex>
|
||||||
|
</div>
|
||||||
|
<div v-if="filterSection === 'createdFrom'" :style="{ marginTop: '6px' }">
|
||||||
|
<JalaliDateInput
|
||||||
|
:model-value="createdFromFilter"
|
||||||
|
placeholder="از تاریخ"
|
||||||
|
@update:model-value="(value: string) => { setCreatedFromFilter(value); filterSection = null }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
:style="{ cursor: 'pointer' }"
|
||||||
|
@click="toggleFilterSection('createdTo')"
|
||||||
|
@keydown.enter.prevent="toggleFilterSection('createdTo')"
|
||||||
|
>
|
||||||
|
<a-flex justify="space-between" align="center">
|
||||||
|
<span>تا تاریخ ثبت</span>
|
||||||
|
<a-typography-text :style="facetValueStyle(Boolean(createdToFilter))">
|
||||||
|
{{ dateFacetLabel(createdToFilter) }}
|
||||||
|
</a-typography-text>
|
||||||
|
</a-flex>
|
||||||
|
</div>
|
||||||
|
<div v-if="filterSection === 'createdTo'" :style="{ marginTop: '6px' }">
|
||||||
|
<JalaliDateInput
|
||||||
|
:model-value="createdToFilter"
|
||||||
|
placeholder="تا تاریخ"
|
||||||
|
@update:model-value="(value: string) => { setCreatedToFilter(value); filterSection = null }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
:style="{ cursor: 'pointer' }"
|
||||||
|
@click="toggleFilterSection('converted')"
|
||||||
|
@keydown.enter.prevent="toggleFilterSection('converted')"
|
||||||
|
>
|
||||||
|
<a-flex justify="space-between" align="center">
|
||||||
|
<span>تبدیلشده</span>
|
||||||
|
<a-typography-text :style="facetValueStyle(convertedFilter)">
|
||||||
|
{{ convertedFilter ? 'فقط' : 'همه' }}
|
||||||
|
</a-typography-text>
|
||||||
|
</a-flex>
|
||||||
|
</div>
|
||||||
|
<a-flex v-if="filterSection === 'converted'" vertical :style="{ marginTop: '6px' }">
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
block
|
||||||
|
:style="facetOptionStyle(!convertedFilter)"
|
||||||
|
@click="pickFlag('converted', false)"
|
||||||
|
>
|
||||||
|
همه
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
block
|
||||||
|
:style="facetOptionStyle(convertedFilter)"
|
||||||
|
@click="pickFlag('converted', true)"
|
||||||
|
>
|
||||||
|
فقط تبدیلشدهها
|
||||||
|
</a-button>
|
||||||
|
</a-flex>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
:style="{ cursor: 'pointer' }"
|
||||||
|
@click="toggleFilterSection('archived')"
|
||||||
|
@keydown.enter.prevent="toggleFilterSection('archived')"
|
||||||
|
>
|
||||||
|
<a-flex justify="space-between" align="center">
|
||||||
|
<span>بایگانی</span>
|
||||||
|
<a-typography-text :style="facetValueStyle(archivedFilter)">
|
||||||
|
{{ archivedFilter ? 'فقط' : 'همه' }}
|
||||||
|
</a-typography-text>
|
||||||
|
</a-flex>
|
||||||
|
</div>
|
||||||
|
<a-flex v-if="filterSection === 'archived'" vertical :style="{ marginTop: '6px' }">
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
block
|
||||||
|
:style="facetOptionStyle(!archivedFilter)"
|
||||||
|
@click="pickFlag('archived', false)"
|
||||||
|
>
|
||||||
|
همه
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
block
|
||||||
|
:style="facetOptionStyle(archivedFilter)"
|
||||||
|
@click="pickFlag('archived', true)"
|
||||||
|
>
|
||||||
|
فقط بایگانیشدهها
|
||||||
|
</a-button>
|
||||||
|
</a-flex>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-button
|
||||||
|
v-if="mobileFilterBadgeCount"
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
block
|
||||||
|
@click="clearMoreFilters"
|
||||||
|
>
|
||||||
|
پاک کردن فیلترها
|
||||||
|
</a-button>
|
||||||
|
</a-flex>
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
|
</div>
|
||||||
<a-select
|
<a-select
|
||||||
:value="sort"
|
:value="sort"
|
||||||
style="width: 10rem"
|
:style="isMobile ? { width: '100%' } : { width: '10rem' }"
|
||||||
:options="Object.entries(sortLabels).map(([value, label]) => ({ value, label }))"
|
:options="Object.entries(sortLabels).map(([value, label]) => ({ value, label }))"
|
||||||
@update:value="(v) => setSort(String(v ?? 'updated') as SortKey)"
|
@update:value="(v) => setSort(String(v ?? 'updated') as SortKey)"
|
||||||
/>
|
/>
|
||||||
<a-select
|
|
||||||
:value="assigneeSelectValue"
|
|
||||||
allow-clear
|
|
||||||
show-search
|
|
||||||
option-filter-prop="label"
|
|
||||||
placeholder="مسئول"
|
|
||||||
style="width: 10rem"
|
|
||||||
:options="[
|
|
||||||
{ value: UNASSIGNED_VALUE, label: 'بدون مسئول' },
|
|
||||||
...users.map((u) => ({ value: u.id, label: u.fullName })),
|
|
||||||
]"
|
|
||||||
@update:value="(v) => setAssigneeFilter(v == null ? undefined : String(v))"
|
|
||||||
/>
|
|
||||||
<a-select
|
|
||||||
:value="followUpFilter || undefined"
|
|
||||||
allow-clear
|
|
||||||
placeholder="پیگیری"
|
|
||||||
style="width: 13rem"
|
|
||||||
:options="FOLLOW_UP_OPTIONS.filter((o) => o.value).map((o) => ({ value: o.value, label: o.label }))"
|
|
||||||
@update:value="(v) => setFollowUpFilter(v == null ? undefined : String(v))"
|
|
||||||
/>
|
|
||||||
</a-flex>
|
</a-flex>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
<a-drawer
|
|
||||||
v-model:open="moreFiltersOpen"
|
|
||||||
:title="isMobile ? 'فیلترها' : 'فیلترهای بیشتر'"
|
|
||||||
placement="left"
|
|
||||||
:width="isMobile ? '100%' : 380"
|
|
||||||
:destroy-on-close="false"
|
|
||||||
>
|
|
||||||
<a-form layout="vertical">
|
|
||||||
<template v-if="isMobile">
|
|
||||||
<a-form-item label="مرتبسازی">
|
|
||||||
<a-select
|
|
||||||
:value="sort"
|
|
||||||
style="width: 100%"
|
|
||||||
:options="Object.entries(sortLabels).map(([value, label]) => ({ value, label }))"
|
|
||||||
@update:value="(v) => setSort(String(v ?? 'updated') as SortKey)"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="مسئول">
|
|
||||||
<a-select
|
|
||||||
:value="assigneeSelectValue"
|
|
||||||
allow-clear
|
|
||||||
show-search
|
|
||||||
option-filter-prop="label"
|
|
||||||
placeholder="همه مسئولان"
|
|
||||||
style="width: 100%"
|
|
||||||
:options="[
|
|
||||||
{ value: UNASSIGNED_VALUE, label: 'بدون مسئول' },
|
|
||||||
...users.map((u) => ({ value: u.id, label: u.fullName })),
|
|
||||||
]"
|
|
||||||
@update:value="(v) => setAssigneeFilter(v == null ? undefined : String(v))"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="پیگیری">
|
|
||||||
<a-select
|
|
||||||
:value="followUpFilter || undefined"
|
|
||||||
allow-clear
|
|
||||||
placeholder="همه پیگیریها"
|
|
||||||
style="width: 100%"
|
|
||||||
:options="FOLLOW_UP_OPTIONS.filter((o) => o.value).map((o) => ({ value: o.value, label: o.label }))"
|
|
||||||
@update:value="(v) => setFollowUpFilter(v == null ? undefined : String(v))"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
</template>
|
|
||||||
<a-form-item label="شهر">
|
|
||||||
<a-select
|
|
||||||
:value="cityFilter || undefined"
|
|
||||||
allow-clear
|
|
||||||
show-search
|
|
||||||
placeholder="همه شهرها"
|
|
||||||
:options="cityOptions.map((c) => ({ value: c, label: c }))"
|
|
||||||
@update:value="(v) => setCityFilter(v == null ? '' : String(v))"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="منطقه">
|
|
||||||
<a-select
|
|
||||||
:value="regionFilter || undefined"
|
|
||||||
allow-clear
|
|
||||||
show-search
|
|
||||||
placeholder="همه مناطق"
|
|
||||||
:options="regionOptions.map((r) => ({ value: r, label: r }))"
|
|
||||||
@update:value="(v) => setRegionFilter(v == null ? '' : String(v))"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="صنف">
|
|
||||||
<a-select
|
|
||||||
:value="tradeFilter || undefined"
|
|
||||||
allow-clear
|
|
||||||
show-search
|
|
||||||
placeholder="همه اصناف"
|
|
||||||
:options="tradeOptions.map((t) => ({ value: t, label: t }))"
|
|
||||||
@update:value="(v) => setTradeFilter(v == null ? '' : String(v))"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="منبع">
|
|
||||||
<a-select
|
|
||||||
:value="sourceFilter || undefined"
|
|
||||||
allow-clear
|
|
||||||
show-search
|
|
||||||
placeholder="همه منابع"
|
|
||||||
:options="sourceOptions.map((s) => ({ value: s, label: s }))"
|
|
||||||
@update:value="(v) => setSourceFilter(v == null ? '' : String(v))"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="از تاریخ ثبت">
|
|
||||||
<JalaliDateInput
|
|
||||||
:model-value="createdFromFilter"
|
|
||||||
placeholder="از تاریخ"
|
|
||||||
@update:model-value="setCreatedFromFilter"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="تا تاریخ ثبت">
|
|
||||||
<JalaliDateInput
|
|
||||||
:model-value="createdToFilter"
|
|
||||||
placeholder="تا تاریخ"
|
|
||||||
@update:model-value="setCreatedToFilter"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
|
||||||
<a-checkbox
|
|
||||||
:checked="convertedFilter"
|
|
||||||
@update:checked="setConvertedFilter"
|
|
||||||
>
|
|
||||||
فقط تبدیلشدهها
|
|
||||||
</a-checkbox>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
|
||||||
<a-checkbox
|
|
||||||
:checked="archivedFilter"
|
|
||||||
@update:checked="setArchivedFilter"
|
|
||||||
>
|
|
||||||
فقط بایگانیشدهها
|
|
||||||
</a-checkbox>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
<template #footer>
|
|
||||||
<a-space>
|
|
||||||
<a-button :disabled="!mobileFilterBadgeCount" @click="clearMoreFilters">
|
|
||||||
پاک کردن
|
|
||||||
</a-button>
|
|
||||||
<a-button type="primary" @click="moreFiltersOpen = false">
|
|
||||||
بستن
|
|
||||||
</a-button>
|
|
||||||
</a-space>
|
|
||||||
</template>
|
|
||||||
</a-drawer>
|
|
||||||
|
|
||||||
<a-col v-if="selectedIds.length" :span="24" @click.stop>
|
<a-col v-if="selectedIds.length" :span="24" @click.stop>
|
||||||
<a-card :bordered="false" :style="{ background: token.colorPrimaryBg }">
|
<a-card :bordered="false" :style="{ background: token.colorPrimaryBg }">
|
||||||
<a-space :size="8" wrap>
|
<a-space :size="8" wrap>
|
||||||
|
|||||||
Reference in New Issue
Block a user