feat(leads): expand filters and tidy the list chrome on mobile
Owners can filter by assignee and follow-up status, with extra filters in a drawer, while mobile keeps a compact search-and-filter bar without the stage chip grid. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -246,7 +246,11 @@ export class LeadsController {
|
|||||||
@Query('source') source?: string,
|
@Query('source') source?: string,
|
||||||
@Query('unassigned') unassigned?: string,
|
@Query('unassigned') unassigned?: string,
|
||||||
@Query('archived') archived?: string,
|
@Query('archived') archived?: string,
|
||||||
|
@Query('converted') converted?: string,
|
||||||
@Query('hasMobile') hasMobile?: string,
|
@Query('hasMobile') hasMobile?: string,
|
||||||
|
@Query('followUp') followUp?: string,
|
||||||
|
@Query('createdFrom') createdFrom?: string,
|
||||||
|
@Query('createdTo') createdTo?: string,
|
||||||
) {
|
) {
|
||||||
return this.leads.listIds(user, {
|
return this.leads.listIds(user, {
|
||||||
q,
|
q,
|
||||||
@@ -258,7 +262,11 @@ export class LeadsController {
|
|||||||
source,
|
source,
|
||||||
unassigned: unassigned === '1' || unassigned === 'true',
|
unassigned: unassigned === '1' || unassigned === 'true',
|
||||||
archived: archived === '1' || archived === 'true',
|
archived: archived === '1' || archived === 'true',
|
||||||
|
converted: converted === '1' || converted === 'true',
|
||||||
hasMobile: hasMobile === '1' || hasMobile === 'true',
|
hasMobile: hasMobile === '1' || hasMobile === 'true',
|
||||||
|
followUp,
|
||||||
|
createdFrom,
|
||||||
|
createdTo,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,8 +284,12 @@ export class LeadsController {
|
|||||||
@Query('source') source?: string,
|
@Query('source') source?: string,
|
||||||
@Query('unassigned') unassigned?: string,
|
@Query('unassigned') unassigned?: string,
|
||||||
@Query('archived') archived?: string,
|
@Query('archived') archived?: string,
|
||||||
|
@Query('converted') converted?: string,
|
||||||
@Query('sort') sort?: string,
|
@Query('sort') sort?: string,
|
||||||
@Query('hasMobile') hasMobile?: string,
|
@Query('hasMobile') hasMobile?: string,
|
||||||
|
@Query('followUp') followUp?: string,
|
||||||
|
@Query('createdFrom') createdFrom?: string,
|
||||||
|
@Query('createdTo') createdTo?: string,
|
||||||
) {
|
) {
|
||||||
return this.leads.list(user, {
|
return this.leads.list(user, {
|
||||||
q,
|
q,
|
||||||
@@ -291,8 +303,12 @@ export class LeadsController {
|
|||||||
source,
|
source,
|
||||||
unassigned: unassigned === '1' || unassigned === 'true',
|
unassigned: unassigned === '1' || unassigned === 'true',
|
||||||
archived: archived === '1' || archived === 'true',
|
archived: archived === '1' || archived === 'true',
|
||||||
|
converted: converted === '1' || converted === 'true',
|
||||||
sort,
|
sort,
|
||||||
hasMobile: hasMobile === '1' || hasMobile === 'true',
|
hasMobile: hasMobile === '1' || hasMobile === 'true',
|
||||||
|
followUp,
|
||||||
|
createdFrom,
|
||||||
|
createdTo,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ import { StorageQuotaService } from '../billing/storage-quota.service';
|
|||||||
import { LeadStagesService } from './lead-stages.service';
|
import { LeadStagesService } from './lead-stages.service';
|
||||||
import { buildSearchWhere, ci } from '../common/search';
|
import { buildSearchWhere, ci } from '../common/search';
|
||||||
import { GamificationService } from '../gamification/gamification.service';
|
import { GamificationService } from '../gamification/gamification.service';
|
||||||
|
import {
|
||||||
|
addDays,
|
||||||
|
isDayKey,
|
||||||
|
tehranDayKey,
|
||||||
|
tehranDayStart,
|
||||||
|
} from '../routines/routine-dates';
|
||||||
|
|
||||||
const LOGO_MAX_BYTES = 2 * 1024 * 1024;
|
const LOGO_MAX_BYTES = 2 * 1024 * 1024;
|
||||||
const LOGO_EXT: Record<string, string> = {
|
const LOGO_EXT: Record<string, string> = {
|
||||||
@@ -142,7 +148,11 @@ export class LeadsService {
|
|||||||
source?: string;
|
source?: string;
|
||||||
unassigned?: boolean;
|
unassigned?: boolean;
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
|
converted?: boolean;
|
||||||
hasMobile?: boolean;
|
hasMobile?: boolean;
|
||||||
|
followUp?: string;
|
||||||
|
createdFrom?: string;
|
||||||
|
createdTo?: string;
|
||||||
},
|
},
|
||||||
): Prisma.LeadWhereInput {
|
): Prisma.LeadWhereInput {
|
||||||
const search = buildSearchWhere<Prisma.LeadWhereInput>(opts.q, (t) => [
|
const search = buildSearchWhere<Prisma.LeadWhereInput>(opts.q, (t) => [
|
||||||
@@ -161,10 +171,15 @@ export class LeadsService {
|
|||||||
if (opts.hasMobile) {
|
if (opts.hasMobile) {
|
||||||
and.push({ mobile: { not: '' } });
|
and.push({ mobile: { not: '' } });
|
||||||
}
|
}
|
||||||
|
const follow = this.followUpWhere(opts.followUp);
|
||||||
|
if (follow) and.push(follow);
|
||||||
|
const createdRange = this.createdAtWhere(opts.createdFrom, opts.createdTo);
|
||||||
|
if (createdRange) and.push(createdRange);
|
||||||
return {
|
return {
|
||||||
organizationId: actor.organizationId,
|
organizationId: actor.organizationId,
|
||||||
...leadAccessWhere(actor),
|
...leadAccessWhere(actor),
|
||||||
archivedAt: opts.archived ? { not: null } : null,
|
archivedAt: opts.archived ? { not: null } : null,
|
||||||
|
...(opts.converted ? { convertedPersonId: { not: null } } : {}),
|
||||||
...(opts.stageId ? { stageId: opts.stageId } : {}),
|
...(opts.stageId ? { stageId: opts.stageId } : {}),
|
||||||
...(opts.assigneeId ? { assigneeId: opts.assigneeId } : {}),
|
...(opts.assigneeId ? { assigneeId: opts.assigneeId } : {}),
|
||||||
...(opts.unassigned ? { assigneeId: null } : {}),
|
...(opts.unassigned ? { assigneeId: null } : {}),
|
||||||
@@ -180,6 +195,52 @@ export class LeadsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** مرز روز به وقت تهران برای فیلترهای «امروز». */
|
||||||
|
private tehranDayBounds(day = tehranDayKey()) {
|
||||||
|
const start = tehranDayStart(day);
|
||||||
|
const end = tehranDayStart(addDays(day, 1));
|
||||||
|
return { start, end };
|
||||||
|
}
|
||||||
|
|
||||||
|
private followUpWhere(followUp?: string): Prisma.LeadWhereInput | null {
|
||||||
|
if (!followUp) return null;
|
||||||
|
const { start, end } = this.tehranDayBounds();
|
||||||
|
switch (followUp) {
|
||||||
|
case 'due_today':
|
||||||
|
return { nextFollowUpAt: { gte: start, lt: end } };
|
||||||
|
case 'overdue':
|
||||||
|
return { nextFollowUpAt: { not: null, lt: start } };
|
||||||
|
case 'called_today':
|
||||||
|
return { lastContactAt: { gte: start, lt: end } };
|
||||||
|
case 'missed_today':
|
||||||
|
return {
|
||||||
|
nextFollowUpAt: { not: null, lt: end },
|
||||||
|
OR: [{ lastContactAt: null }, { lastContactAt: { lt: start } }],
|
||||||
|
};
|
||||||
|
case 'no_follow_up':
|
||||||
|
return { nextFollowUpAt: null };
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createdAtWhere(
|
||||||
|
createdFrom?: string,
|
||||||
|
createdTo?: string,
|
||||||
|
): Prisma.LeadWhereInput | null {
|
||||||
|
const from = isDayKey(createdFrom) ? tehranDayStart(createdFrom) : null;
|
||||||
|
const to = isDayKey(createdTo)
|
||||||
|
? tehranDayStart(addDays(createdTo, 1))
|
||||||
|
: null;
|
||||||
|
if (!from && !to) return null;
|
||||||
|
return {
|
||||||
|
createdAt: {
|
||||||
|
...(from ? { gte: from } : {}),
|
||||||
|
...(to ? { lt: to } : {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async list(
|
async list(
|
||||||
actor: Actor,
|
actor: Actor,
|
||||||
opts: {
|
opts: {
|
||||||
@@ -194,8 +255,12 @@ export class LeadsService {
|
|||||||
source?: string;
|
source?: string;
|
||||||
unassigned?: boolean;
|
unassigned?: boolean;
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
|
converted?: boolean;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
hasMobile?: boolean;
|
hasMobile?: boolean;
|
||||||
|
followUp?: string;
|
||||||
|
createdFrom?: string;
|
||||||
|
createdTo?: string;
|
||||||
},
|
},
|
||||||
): Promise<PaginatedResponse<LeadDto>> {
|
): Promise<PaginatedResponse<LeadDto>> {
|
||||||
await this.stages.ensureDefaults(actor.organizationId);
|
await this.stages.ensureDefaults(actor.organizationId);
|
||||||
@@ -243,7 +308,11 @@ export class LeadsService {
|
|||||||
source?: string;
|
source?: string;
|
||||||
unassigned?: boolean;
|
unassigned?: boolean;
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
|
converted?: boolean;
|
||||||
hasMobile?: boolean;
|
hasMobile?: boolean;
|
||||||
|
followUp?: string;
|
||||||
|
createdFrom?: string;
|
||||||
|
createdTo?: string;
|
||||||
},
|
},
|
||||||
max = FILTERED_IDS_MAX,
|
max = FILTERED_IDS_MAX,
|
||||||
): Promise<FilteredIdsResponse> {
|
): Promise<FilteredIdsResponse> {
|
||||||
|
|||||||
+495
-136
@@ -5,10 +5,12 @@ 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,
|
||||||
@@ -48,7 +50,6 @@ import {
|
|||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
UnorderedListOutlined,
|
UnorderedListOutlined,
|
||||||
UserOutlined,
|
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import {
|
import {
|
||||||
@@ -61,6 +62,7 @@ import { canManageSettings } from '@/lib/roles';
|
|||||||
import { canAssignToOthers } from '@/lib/assign-access';
|
import { canAssignToOthers } from '@/lib/assign-access';
|
||||||
import { registerTableColumns, useTableColumns } from '@/lib/tableColumns';
|
import { registerTableColumns, useTableColumns } from '@/lib/tableColumns';
|
||||||
import TableColumnsMenu from '@/components/TableColumnsMenu.vue';
|
import TableColumnsMenu from '@/components/TableColumnsMenu.vue';
|
||||||
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||||
import ExcelImportFlow from '@/components/ExcelImportFlow.vue';
|
import ExcelImportFlow from '@/components/ExcelImportFlow.vue';
|
||||||
import PhoneContactsImport from '@/components/PhoneContactsImport.vue';
|
import PhoneContactsImport from '@/components/PhoneContactsImport.vue';
|
||||||
import {
|
import {
|
||||||
@@ -92,8 +94,25 @@ const isMobile = computed(() => !screens.value.md);
|
|||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
type SortKey = 'updated' | 'newest' | 'follow_up' | 'last_contact';
|
type SortKey = 'updated' | 'newest' | 'follow_up' | 'last_contact';
|
||||||
|
type FollowUpKey =
|
||||||
|
| ''
|
||||||
|
| 'due_today'
|
||||||
|
| 'overdue'
|
||||||
|
| 'called_today'
|
||||||
|
| 'missed_today'
|
||||||
|
| 'no_follow_up';
|
||||||
type StageTint = 'mint' | 'amber' | 'peach' | 'sky' | 'violet' | 'rose' | 'teal';
|
type StageTint = 'mint' | 'amber' | 'peach' | 'sky' | 'violet' | 'rose' | 'teal';
|
||||||
|
|
||||||
|
const FOLLOW_UP_OPTIONS: Array<{ value: FollowUpKey; label: string }> = [
|
||||||
|
{ value: '', label: 'همه پیگیریها' },
|
||||||
|
{ value: 'due_today', label: 'قرار امروز' },
|
||||||
|
{ value: 'overdue', label: 'عقبافتاده' },
|
||||||
|
{ value: 'called_today', label: 'امروز تماس گرفته' },
|
||||||
|
{ value: 'missed_today', label: 'امروز باید میگرفت و نگرفته' },
|
||||||
|
{ value: 'no_follow_up', label: 'بدون پیگیری' },
|
||||||
|
];
|
||||||
|
const UNASSIGNED_VALUE = '__unassigned__';
|
||||||
|
|
||||||
/** ردیف آمار صفحه لیدها — ترتیب و نامها مطابق طرح */
|
/** ردیف آمار صفحه لیدها — ترتیب و نامها مطابق طرح */
|
||||||
const LEAD_STAT_DEFS: Array<{
|
const LEAD_STAT_DEFS: Array<{
|
||||||
label: string;
|
label: string;
|
||||||
@@ -162,8 +181,11 @@ const stages = ref<LeadStageDto[]>([]);
|
|||||||
const stats = ref<LeadStatsDto | null>(null);
|
const stats = ref<LeadStatsDto | null>(null);
|
||||||
const users = ref<OrgUserDto[]>([]);
|
const users = ref<OrgUserDto[]>([]);
|
||||||
const cityOptions = ref<string[]>([]);
|
const cityOptions = ref<string[]>([]);
|
||||||
|
const regionOptions = ref<string[]>([]);
|
||||||
|
const tradeOptions = ref<string[]>([]);
|
||||||
const sourceOptions = ref<string[]>([]);
|
const sourceOptions = ref<string[]>([]);
|
||||||
const selected = ref<Set<string>>(new Set());
|
const selected = ref<Set<string>>(new Set());
|
||||||
|
const moreFiltersOpen = ref(false);
|
||||||
const sortOpen = ref(false);
|
const sortOpen = ref(false);
|
||||||
const rowMenuId = ref<string | null>(null);
|
const rowMenuId = ref<string | null>(null);
|
||||||
const logoMenuId = ref<string | null>(null);
|
const logoMenuId = ref<string | null>(null);
|
||||||
@@ -235,12 +257,70 @@ const assigneeId = computed(() =>
|
|||||||
const cityFilter = computed(() =>
|
const cityFilter = computed(() =>
|
||||||
typeof route.query.city === 'string' ? route.query.city : '',
|
typeof route.query.city === 'string' ? route.query.city : '',
|
||||||
);
|
);
|
||||||
|
const regionFilter = computed(() =>
|
||||||
|
typeof route.query.region === 'string' ? route.query.region : '',
|
||||||
|
);
|
||||||
|
const tradeFilter = computed(() =>
|
||||||
|
typeof route.query.trade === 'string' ? route.query.trade : '',
|
||||||
|
);
|
||||||
const sourceFilter = computed(() =>
|
const sourceFilter = computed(() =>
|
||||||
typeof route.query.source === 'string' ? route.query.source : '',
|
typeof route.query.source === 'string' ? route.query.source : '',
|
||||||
);
|
);
|
||||||
|
const followUpFilter = computed<FollowUpKey>(() => {
|
||||||
|
const value = route.query.followUp;
|
||||||
|
if (
|
||||||
|
value === 'due_today' ||
|
||||||
|
value === 'overdue' ||
|
||||||
|
value === 'called_today' ||
|
||||||
|
value === 'missed_today' ||
|
||||||
|
value === 'no_follow_up'
|
||||||
|
) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
const createdFromFilter = computed(() =>
|
||||||
|
typeof route.query.createdFrom === 'string' ? route.query.createdFrom : '',
|
||||||
|
);
|
||||||
|
const createdToFilter = computed(() =>
|
||||||
|
typeof route.query.createdTo === 'string' ? route.query.createdTo : '',
|
||||||
|
);
|
||||||
const unassigned = computed(
|
const unassigned = computed(
|
||||||
() => route.query.unassigned === '1' || route.query.unassigned === 'true',
|
() => route.query.unassigned === '1' || route.query.unassigned === 'true',
|
||||||
);
|
);
|
||||||
|
const archivedFilter = computed(
|
||||||
|
() => route.query.archived === '1' || route.query.archived === 'true',
|
||||||
|
);
|
||||||
|
const convertedFilter = computed(
|
||||||
|
() => route.query.converted === '1' || route.query.converted === 'true',
|
||||||
|
);
|
||||||
|
const assigneeSelectValue = computed(() => {
|
||||||
|
if (unassigned.value) return UNASSIGNED_VALUE;
|
||||||
|
return assigneeId.value || undefined;
|
||||||
|
});
|
||||||
|
const moreFiltersCount = computed(() =>
|
||||||
|
[
|
||||||
|
cityFilter.value,
|
||||||
|
regionFilter.value,
|
||||||
|
tradeFilter.value,
|
||||||
|
sourceFilter.value,
|
||||||
|
createdFromFilter.value,
|
||||||
|
createdToFilter.value,
|
||||||
|
archivedFilter.value ? '1' : '',
|
||||||
|
convertedFilter.value ? '1' : '',
|
||||||
|
].filter(Boolean).length,
|
||||||
|
);
|
||||||
|
/** در موبایل مرتبسازی و مسئول و پیگیری هم داخل کشو هستند. */
|
||||||
|
const mobileFilterBadgeCount = computed(() => {
|
||||||
|
const extra = isMobile.value
|
||||||
|
? [
|
||||||
|
sort.value !== 'updated' ? '1' : '',
|
||||||
|
assigneeSelectValue.value ? '1' : '',
|
||||||
|
followUpFilter.value,
|
||||||
|
].filter(Boolean).length
|
||||||
|
: 0;
|
||||||
|
return moreFiltersCount.value + extra;
|
||||||
|
});
|
||||||
|
|
||||||
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)));
|
||||||
@@ -514,14 +594,79 @@ function setStageFilter(id: string) {
|
|||||||
patchQuery({ stageId: id || undefined, page: undefined });
|
patchQuery({ stageId: id || undefined, page: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setAssigneeFilter(value: string | undefined) {
|
||||||
|
if (value === UNASSIGNED_VALUE) {
|
||||||
|
patchQuery({ assigneeId: undefined, unassigned: '1', page: undefined });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
patchQuery({
|
||||||
|
assigneeId: value || undefined,
|
||||||
|
unassigned: undefined,
|
||||||
|
page: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFollowUpFilter(value: string | undefined) {
|
||||||
|
patchQuery({
|
||||||
|
followUp: value || undefined,
|
||||||
|
page: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setCityFilter(value: string) {
|
function setCityFilter(value: string) {
|
||||||
patchQuery({ city: value || undefined, page: undefined });
|
patchQuery({ city: value || undefined, page: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setRegionFilter(value: string) {
|
||||||
|
patchQuery({ region: value || undefined, page: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTradeFilter(value: string) {
|
||||||
|
patchQuery({ trade: value || undefined, page: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
function setSourceFilter(value: string) {
|
function setSourceFilter(value: string) {
|
||||||
patchQuery({ source: value || undefined, page: undefined });
|
patchQuery({ source: value || undefined, page: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCreatedFromFilter(value: string) {
|
||||||
|
patchQuery({ createdFrom: value || undefined, page: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCreatedToFilter(value: string) {
|
||||||
|
patchQuery({ createdTo: value || undefined, page: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setArchivedFilter(checked: boolean) {
|
||||||
|
patchQuery({ archived: checked ? '1' : undefined, page: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConvertedFilter(checked: boolean) {
|
||||||
|
patchQuery({ converted: checked ? '1' : undefined, page: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMoreFilters() {
|
||||||
|
patchQuery({
|
||||||
|
city: undefined,
|
||||||
|
region: undefined,
|
||||||
|
trade: undefined,
|
||||||
|
source: undefined,
|
||||||
|
createdFrom: undefined,
|
||||||
|
createdTo: undefined,
|
||||||
|
archived: undefined,
|
||||||
|
converted: undefined,
|
||||||
|
...(isMobile.value
|
||||||
|
? {
|
||||||
|
sort: undefined,
|
||||||
|
assigneeId: undefined,
|
||||||
|
unassigned: undefined,
|
||||||
|
followUp: undefined,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
page: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setPage(next: number) {
|
function setPage(next: number) {
|
||||||
if (next < 1 || next > pageCount.value) return;
|
if (next < 1 || next > pageCount.value) return;
|
||||||
patchQuery({ page: next === 1 ? undefined : String(next) });
|
patchQuery({ page: next === 1 ? undefined : String(next) });
|
||||||
@@ -594,12 +739,18 @@ function onCardClick(lead: LeadDto) {
|
|||||||
|
|
||||||
function mergeFacets(items: LeadDto[]) {
|
function mergeFacets(items: LeadDto[]) {
|
||||||
const cities = new Set(cityOptions.value);
|
const cities = new Set(cityOptions.value);
|
||||||
|
const regions = new Set(regionOptions.value);
|
||||||
|
const trades = new Set(tradeOptions.value);
|
||||||
const sources = new Set(sourceOptions.value);
|
const sources = new Set(sourceOptions.value);
|
||||||
for (const lead of items) {
|
for (const lead of items) {
|
||||||
if (lead.city?.trim()) cities.add(lead.city.trim());
|
if (lead.city?.trim()) cities.add(lead.city.trim());
|
||||||
|
if (lead.region?.trim()) regions.add(lead.region.trim());
|
||||||
|
if (lead.trade?.trim()) trades.add(lead.trade.trim());
|
||||||
if (lead.source?.trim()) sources.add(lead.source.trim());
|
if (lead.source?.trim()) sources.add(lead.source.trim());
|
||||||
}
|
}
|
||||||
cityOptions.value = [...cities].sort((a, b) => a.localeCompare(b, 'fa'));
|
cityOptions.value = [...cities].sort((a, b) => a.localeCompare(b, 'fa'));
|
||||||
|
regionOptions.value = [...regions].sort((a, b) => a.localeCompare(b, 'fa'));
|
||||||
|
tradeOptions.value = [...trades].sort((a, b) => a.localeCompare(b, 'fa'));
|
||||||
sourceOptions.value = [...sources].sort((a, b) => a.localeCompare(b, 'fa'));
|
sourceOptions.value = [...sources].sort((a, b) => a.localeCompare(b, 'fa'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,8 +782,15 @@ async function load() {
|
|||||||
stageId: stageId.value || undefined,
|
stageId: stageId.value || undefined,
|
||||||
assigneeId: assigneeId.value || undefined,
|
assigneeId: assigneeId.value || undefined,
|
||||||
city: cityFilter.value || undefined,
|
city: cityFilter.value || undefined,
|
||||||
|
region: regionFilter.value || undefined,
|
||||||
|
trade: tradeFilter.value || undefined,
|
||||||
source: sourceFilter.value || undefined,
|
source: sourceFilter.value || undefined,
|
||||||
unassigned: unassigned.value ? '1' : undefined,
|
unassigned: unassigned.value ? '1' : undefined,
|
||||||
|
archived: archivedFilter.value ? '1' : undefined,
|
||||||
|
converted: convertedFilter.value ? '1' : undefined,
|
||||||
|
followUp: followUpFilter.value || undefined,
|
||||||
|
createdFrom: createdFromFilter.value || undefined,
|
||||||
|
createdTo: createdToFilter.value || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
data.value = res;
|
data.value = res;
|
||||||
@@ -1114,8 +1272,15 @@ watch(
|
|||||||
route.query.stageId,
|
route.query.stageId,
|
||||||
route.query.assigneeId,
|
route.query.assigneeId,
|
||||||
route.query.city,
|
route.query.city,
|
||||||
|
route.query.region,
|
||||||
|
route.query.trade,
|
||||||
route.query.source,
|
route.query.source,
|
||||||
route.query.unassigned,
|
route.query.unassigned,
|
||||||
|
route.query.archived,
|
||||||
|
route.query.converted,
|
||||||
|
route.query.followUp,
|
||||||
|
route.query.createdFrom,
|
||||||
|
route.query.createdTo,
|
||||||
],
|
],
|
||||||
() => {
|
() => {
|
||||||
q.value = typeof route.query.q === 'string' ? route.query.q : '';
|
q.value = typeof route.query.q === 'string' ? route.query.q : '';
|
||||||
@@ -1181,176 +1346,370 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-row :gutter="[24, 24]" @click="closePopups">
|
<a-row :gutter="[16, 16]" @click="closePopups">
|
||||||
<a-col :span="24">
|
<a-col :span="24">
|
||||||
<a-flex justify="space-between" align="center" wrap="wrap" :gap="token.marginSM">
|
<a-flex justify="space-between" align="center" wrap="wrap" :gap="token.marginSM">
|
||||||
<div>
|
<a-typography-title :level="3" :style="{ margin: 0 }">لیدها</a-typography-title>
|
||||||
<a-typography-title :level="3" :style="{ margin: 0 }">لیدها</a-typography-title>
|
|
||||||
<a-typography-text type="secondary">
|
|
||||||
مدیریت و پیگیری تمام لیدهای ورودی
|
|
||||||
</a-typography-text>
|
|
||||||
</div>
|
|
||||||
<a-space :size="8" wrap>
|
<a-space :size="8" wrap>
|
||||||
<a-button :disabled="busy" :loading="pageBusy && busyMessage.includes('خروجی')" @click.stop="exportExcel">
|
<template v-if="isMobile">
|
||||||
<template #icon><ExportOutlined /></template>
|
<a-dropdown :trigger="['click']" placement="bottomLeft">
|
||||||
خروجی اکسل
|
<a-button :disabled="busy">
|
||||||
</a-button>
|
<template #icon><MoreOutlined /></template>
|
||||||
<ExcelImportFlow
|
بیشتر
|
||||||
preview-path="/leads/import/preview"
|
|
||||||
import-path="/leads/import/file"
|
|
||||||
:disabled="busy"
|
|
||||||
@start="onExcelImportStart"
|
|
||||||
@done="onExcelImportDone"
|
|
||||||
@fail="onExcelImportFail"
|
|
||||||
@settled="onExcelImportSettled"
|
|
||||||
>
|
|
||||||
<template #default="{ loading: excelLoading }">
|
|
||||||
<a-button :disabled="busy" :loading="excelLoading">
|
|
||||||
<template #icon><FileExcelOutlined /></template>
|
|
||||||
ورود از اکسل
|
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
<template #overlay>
|
||||||
</ExcelImportFlow>
|
<a-menu>
|
||||||
<PhoneContactsImport
|
<a-menu-item
|
||||||
target="leads"
|
key="export"
|
||||||
:disabled="busy"
|
:disabled="busy || (pageBusy && busyMessage.includes('خروجی'))"
|
||||||
@start="onPhoneImportStart"
|
@click="exportExcel"
|
||||||
@done="onPhoneImportDone"
|
>
|
||||||
@fail="onPhoneImportFail"
|
<ExportOutlined />
|
||||||
@settled="onPhoneImportSettled"
|
خروجی اکسل
|
||||||
>
|
</a-menu-item>
|
||||||
<a-button :disabled="busy">
|
<a-menu-item key="excel" :disabled="busy">
|
||||||
<template #icon><PhoneOutlined /></template>
|
<ExcelImportFlow
|
||||||
از مخاطبین گوشی
|
preview-path="/leads/import/preview"
|
||||||
|
import-path="/leads/import/file"
|
||||||
|
:disabled="busy"
|
||||||
|
@start="onExcelImportStart"
|
||||||
|
@done="onExcelImportDone"
|
||||||
|
@fail="onExcelImportFail"
|
||||||
|
@settled="onExcelImportSettled"
|
||||||
|
>
|
||||||
|
<template #default="{ loading: excelLoading }">
|
||||||
|
<a-flex align="center" :gap="8" :style="{ opacity: excelLoading ? 0.6 : 1 }">
|
||||||
|
<FileExcelOutlined />
|
||||||
|
ورود از اکسل
|
||||||
|
</a-flex>
|
||||||
|
</template>
|
||||||
|
</ExcelImportFlow>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="phone" :disabled="busy">
|
||||||
|
<PhoneContactsImport
|
||||||
|
target="leads"
|
||||||
|
:disabled="busy"
|
||||||
|
@start="onPhoneImportStart"
|
||||||
|
@done="onPhoneImportDone"
|
||||||
|
@fail="onPhoneImportFail"
|
||||||
|
@settled="onPhoneImportSettled"
|
||||||
|
>
|
||||||
|
<a-flex align="center" :gap="8">
|
||||||
|
<PhoneOutlined />
|
||||||
|
از مخاطبین گوشی
|
||||||
|
</a-flex>
|
||||||
|
</PhoneContactsImport>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-button :disabled="busy" :loading="pageBusy && busyMessage.includes('خروجی')" @click.stop="exportExcel">
|
||||||
|
<template #icon><ExportOutlined /></template>
|
||||||
|
خروجی اکسل
|
||||||
</a-button>
|
</a-button>
|
||||||
</PhoneContactsImport>
|
<ExcelImportFlow
|
||||||
|
preview-path="/leads/import/preview"
|
||||||
|
import-path="/leads/import/file"
|
||||||
|
:disabled="busy"
|
||||||
|
@start="onExcelImportStart"
|
||||||
|
@done="onExcelImportDone"
|
||||||
|
@fail="onExcelImportFail"
|
||||||
|
@settled="onExcelImportSettled"
|
||||||
|
>
|
||||||
|
<template #default="{ loading: excelLoading }">
|
||||||
|
<a-button :disabled="busy" :loading="excelLoading">
|
||||||
|
<template #icon><FileExcelOutlined /></template>
|
||||||
|
ورود از اکسل
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</ExcelImportFlow>
|
||||||
|
<PhoneContactsImport
|
||||||
|
target="leads"
|
||||||
|
:disabled="busy"
|
||||||
|
@start="onPhoneImportStart"
|
||||||
|
@done="onPhoneImportDone"
|
||||||
|
@fail="onPhoneImportFail"
|
||||||
|
@settled="onPhoneImportSettled"
|
||||||
|
>
|
||||||
|
<a-button :disabled="busy">
|
||||||
|
<template #icon><PhoneOutlined /></template>
|
||||||
|
از مخاطبین گوشی
|
||||||
|
</a-button>
|
||||||
|
</PhoneContactsImport>
|
||||||
|
</template>
|
||||||
|
<a-radio-group
|
||||||
|
v-if="isMobile"
|
||||||
|
value="list"
|
||||||
|
button-style="solid"
|
||||||
|
@change="(e: any) => { if (e?.target?.value === 'board') void router.push({ name: 'leads-board' }) }"
|
||||||
|
>
|
||||||
|
<a-radio-button value="list">
|
||||||
|
<UnorderedListOutlined />
|
||||||
|
</a-radio-button>
|
||||||
|
<a-radio-button value="board">
|
||||||
|
<AppstoreOutlined />
|
||||||
|
</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
<a-button type="primary" @click.stop="openCreate">
|
<a-button type="primary" @click.stop="openCreate">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon><PlusOutlined /></template>
|
||||||
لید جدید
|
{{ isMobile ? 'جدید' : 'لید جدید' }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
<a-col :span="24">
|
|
||||||
<a-row :gutter="[12, 12]">
|
|
||||||
<a-col :xs="12" :sm="8" :md="4">
|
|
||||||
<a-card
|
|
||||||
:bordered="true"
|
|
||||||
hoverable
|
|
||||||
:style="{
|
|
||||||
borderColor: !stageId ? token.colorPrimary : undefined,
|
|
||||||
cursor: 'pointer',
|
|
||||||
}"
|
|
||||||
@click="setStageFilter('')"
|
|
||||||
>
|
|
||||||
<a-flex align="center" :gap="12">
|
|
||||||
<a-avatar :style="{ background: token.colorPrimaryBg, color: token.colorPrimary }">
|
|
||||||
<template #icon><UserOutlined /></template>
|
|
||||||
</a-avatar>
|
|
||||||
<div>
|
|
||||||
<a-typography-text type="secondary">همه لیدها</a-typography-text>
|
|
||||||
<div>
|
|
||||||
<a-typography-text strong>{{ formatCount(totalLeadsCount) }}</a-typography-text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-flex>
|
|
||||||
</a-card>
|
|
||||||
</a-col>
|
|
||||||
<a-col
|
|
||||||
v-for="card in stageCards"
|
|
||||||
:key="card.key"
|
|
||||||
:xs="12"
|
|
||||||
:sm="8"
|
|
||||||
:md="4"
|
|
||||||
>
|
|
||||||
<a-card
|
|
||||||
:bordered="true"
|
|
||||||
hoverable
|
|
||||||
:style="{
|
|
||||||
borderColor: isStatActive(card) ? token.colorPrimary : undefined,
|
|
||||||
cursor: card.stageId ? 'pointer' : 'not-allowed',
|
|
||||||
opacity: card.stageId ? 1 : 0.55,
|
|
||||||
}"
|
|
||||||
@click="card.stageId && selectStatCard(card)"
|
|
||||||
>
|
|
||||||
<a-typography-text type="secondary">{{ card.label }}</a-typography-text>
|
|
||||||
<div>
|
|
||||||
<a-typography-text strong>{{ formatCount(card.count) }}</a-typography-text>
|
|
||||||
</div>
|
|
||||||
</a-card>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</a-col>
|
|
||||||
|
|
||||||
<a-col :span="24" @click.stop>
|
<a-col :span="24" @click.stop>
|
||||||
<a-card :bordered="true">
|
<a-card :bordered="true">
|
||||||
<a-flex justify="space-between" align="center" wrap="wrap" :gap="12">
|
<a-flex vertical :gap="12">
|
||||||
<a-flex align="center" wrap="wrap" :gap="8" flex="1 1 auto">
|
<a-flex v-if="!isMobile" justify="space-between" align="center" wrap="wrap" :gap="8">
|
||||||
|
<a-flex wrap="wrap" :gap="8" align="center" flex="1 1 auto">
|
||||||
|
<a-button :type="!stageId ? 'primary' : 'default'" @click="setStageFilter('')">
|
||||||
|
همه لیدها
|
||||||
|
<a-typography-text
|
||||||
|
:type="!stageId ? undefined : 'secondary'"
|
||||||
|
:style="{ color: !stageId ? 'inherit' : undefined, marginInlineStart: '6px' }"
|
||||||
|
>
|
||||||
|
{{ formatCount(totalLeadsCount) }}
|
||||||
|
</a-typography-text>
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
v-for="card in stageCards"
|
||||||
|
:key="card.key"
|
||||||
|
:type="isStatActive(card) ? 'primary' : 'default'"
|
||||||
|
:disabled="!card.stageId"
|
||||||
|
@click="selectStatCard(card)"
|
||||||
|
>
|
||||||
|
{{ card.label }}
|
||||||
|
<a-typography-text
|
||||||
|
:type="isStatActive(card) ? undefined : 'secondary'"
|
||||||
|
:style="{ color: isStatActive(card) ? 'inherit' : undefined, marginInlineStart: '6px' }"
|
||||||
|
>
|
||||||
|
{{ formatCount(card.count) }}
|
||||||
|
</a-typography-text>
|
||||||
|
</a-button>
|
||||||
|
</a-flex>
|
||||||
<a-space :size="8">
|
<a-space :size="8">
|
||||||
<FilterOutlined />
|
<TableColumnsMenu table-key="leads" align="end" />
|
||||||
<a-typography-text strong>فیلترها</a-typography-text>
|
<a-radio-group
|
||||||
|
value="list"
|
||||||
|
button-style="solid"
|
||||||
|
@change="(e: any) => { if (e?.target?.value === 'board') void router.push({ name: 'leads-board' }) }"
|
||||||
|
>
|
||||||
|
<a-radio-button value="list">
|
||||||
|
<UnorderedListOutlined />
|
||||||
|
</a-radio-button>
|
||||||
|
<a-radio-button value="board">
|
||||||
|
<AppstoreOutlined />
|
||||||
|
</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
</a-space>
|
</a-space>
|
||||||
<a-select
|
</a-flex>
|
||||||
:value="sort"
|
|
||||||
style="width: 11rem"
|
<a-flex v-if="isMobile" vertical :gap="8">
|
||||||
:options="Object.entries(sortLabels).map(([value, label]) => ({ value, label }))"
|
|
||||||
@update:value="(v) => setSort(String(v ?? 'updated') as SortKey)"
|
|
||||||
/>
|
|
||||||
<a-input-search
|
<a-input-search
|
||||||
v-model:value="q"
|
v-model:value="q"
|
||||||
allow-clear
|
allow-clear
|
||||||
placeholder="جستجو..."
|
placeholder="جستجو نام، موبایل، شهر…"
|
||||||
enter-button="جستجو"
|
style="width: 100%"
|
||||||
style="width: 16rem; max-width: 100%"
|
|
||||||
@search="onSearch"
|
@search="onSearch"
|
||||||
>
|
>
|
||||||
<template #prefix><SearchOutlined /></template>
|
<template #enterButton>
|
||||||
|
<a-button type="primary">
|
||||||
|
<template #icon><SearchOutlined /></template>
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
</a-input-search>
|
</a-input-search>
|
||||||
<a-select
|
<a-badge
|
||||||
:value="cityFilter || undefined"
|
:count="mobileFilterBadgeCount"
|
||||||
|
:offset="[-2, 2]"
|
||||||
|
:style="{ width: '100%', display: 'block' }"
|
||||||
|
>
|
||||||
|
<a-button block @click="moreFiltersOpen = true">
|
||||||
|
<template #icon><FilterOutlined /></template>
|
||||||
|
فیلترها
|
||||||
|
</a-button>
|
||||||
|
</a-badge>
|
||||||
|
</a-flex>
|
||||||
|
<a-flex v-else align="center" wrap="wrap" :gap="8">
|
||||||
|
<a-input-search
|
||||||
|
v-model:value="q"
|
||||||
allow-clear
|
allow-clear
|
||||||
placeholder="همه مناطق"
|
placeholder="جستجو نام، موبایل، شهر…"
|
||||||
style="width: 9.5rem"
|
style="width: 15rem; max-width: 100%"
|
||||||
:options="cityOptions.map((c) => ({ value: c, label: c }))"
|
@search="onSearch"
|
||||||
@update:value="(v) => setCityFilter(v == null ? '' : String(v))"
|
>
|
||||||
|
<template #enterButton>
|
||||||
|
<a-button type="primary">
|
||||||
|
<template #icon><SearchOutlined /></template>
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</a-input-search>
|
||||||
|
<a-badge :count="mobileFilterBadgeCount" :offset="[-2, 2]">
|
||||||
|
<a-button @click="moreFiltersOpen = true">
|
||||||
|
<template #icon><FilterOutlined /></template>
|
||||||
|
فیلترها
|
||||||
|
</a-button>
|
||||||
|
</a-badge>
|
||||||
|
<a-select
|
||||||
|
:value="sort"
|
||||||
|
style="width: 10rem"
|
||||||
|
:options="Object.entries(sortLabels).map(([value, label]) => ({ value, label }))"
|
||||||
|
@update:value="(v) => setSort(String(v ?? 'updated') as SortKey)"
|
||||||
/>
|
/>
|
||||||
<a-select
|
<a-select
|
||||||
:value="sourceFilter || undefined"
|
:value="assigneeSelectValue"
|
||||||
allow-clear
|
allow-clear
|
||||||
placeholder="همه منابع"
|
show-search
|
||||||
style="width: 9.5rem"
|
option-filter-prop="label"
|
||||||
:options="sourceOptions.map((s) => ({ value: s, label: s }))"
|
placeholder="مسئول"
|
||||||
@update:value="(v) => setSourceFilter(v == null ? '' : String(v))"
|
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
|
<a-select
|
||||||
:value="stageId || undefined"
|
:value="followUpFilter || undefined"
|
||||||
allow-clear
|
allow-clear
|
||||||
placeholder="همه وضعیت"
|
placeholder="پیگیری"
|
||||||
style="width: 9.5rem"
|
style="width: 13rem"
|
||||||
:options="stages.map((s) => ({ value: s.id, label: s.name }))"
|
:options="FOLLOW_UP_OPTIONS.filter((o) => o.value).map((o) => ({ value: o.value, label: o.label }))"
|
||||||
@update:value="(v) => setStageFilter(v == null ? '' : String(v))"
|
@update:value="(v) => setFollowUpFilter(v == null ? undefined : String(v))"
|
||||||
/>
|
/>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
<a-space :size="8">
|
|
||||||
<TableColumnsMenu table-key="leads" align="end" />
|
|
||||||
<a-radio-group
|
|
||||||
value="list"
|
|
||||||
button-style="solid"
|
|
||||||
@change="(e: any) => { if (e?.target?.value === 'board') void router.push({ name: 'leads-board' }) }"
|
|
||||||
>
|
|
||||||
<a-radio-button value="list">
|
|
||||||
<UnorderedListOutlined />
|
|
||||||
</a-radio-button>
|
|
||||||
<a-radio-button value="board">
|
|
||||||
<AppstoreOutlined />
|
|
||||||
</a-radio-button>
|
|
||||||
</a-radio-group>
|
|
||||||
</a-space>
|
|
||||||
</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