feat(activities): filter the desk from each column
Assignees are searched instead of listed in full, and start time only filters whether a time is set. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -193,6 +193,9 @@ export class ActivitiesController {
|
|||||||
typeDefId: query.typeDefId,
|
typeDefId: query.typeDefId,
|
||||||
status: query.status,
|
status: query.status,
|
||||||
assigneeId: query.assigneeId,
|
assigneeId: query.assigneeId,
|
||||||
|
title: query.title,
|
||||||
|
customer: query.customer,
|
||||||
|
due: query.due,
|
||||||
boardId: query.boardId,
|
boardId: query.boardId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -226,6 +229,9 @@ export class ActivitiesController {
|
|||||||
typeDefId: query.typeDefId,
|
typeDefId: query.typeDefId,
|
||||||
status: query.status,
|
status: query.status,
|
||||||
assigneeId: query.assigneeId,
|
assigneeId: query.assigneeId,
|
||||||
|
title: query.title,
|
||||||
|
customer: query.customer,
|
||||||
|
due: query.due,
|
||||||
boardId: query.boardId,
|
boardId: query.boardId,
|
||||||
},
|
},
|
||||||
query.stageId ? { stageId: query.stageId } : undefined,
|
query.stageId ? { stageId: query.stageId } : undefined,
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ type ListFacets = {
|
|||||||
typeDefId?: string;
|
typeDefId?: string;
|
||||||
status?: ActivityDto['status'];
|
status?: ActivityDto['status'];
|
||||||
assigneeId?: string;
|
assigneeId?: string;
|
||||||
|
title?: string;
|
||||||
|
customer?: string;
|
||||||
|
/** set = زمان شروع دارد، unset = ندارد */
|
||||||
|
due?: 'set' | 'unset';
|
||||||
/** گروه فعالیت؛ خالی = همهٔ گروهها */
|
/** گروه فعالیت؛ خالی = همهٔ گروهها */
|
||||||
boardId?: string;
|
boardId?: string;
|
||||||
};
|
};
|
||||||
@@ -864,11 +868,15 @@ export class ActivitiesService {
|
|||||||
: activityAccessWhere(actor);
|
: activityAccessWhere(actor);
|
||||||
const facetAssignee = facets?.assigneeId?.trim();
|
const facetAssignee = facets?.assigneeId?.trim();
|
||||||
const facetTypeDef = facets?.typeDefId?.trim();
|
const facetTypeDef = facets?.typeDefId?.trim();
|
||||||
|
const facetTitle = facets?.title?.trim();
|
||||||
|
const facetCustomer = facets?.customer?.trim();
|
||||||
const andParts: Prisma.ActivityWhereInput[] = [
|
const andParts: Prisma.ActivityWhereInput[] = [
|
||||||
scope,
|
scope,
|
||||||
...(facets?.type ? [{ type: facets.type }] : []),
|
...(facets?.type ? [{ type: facets.type }] : []),
|
||||||
...(facetTypeDef ? [this.types.whereForType(typeRows, facetTypeDef)] : []),
|
...(facetTypeDef ? [this.types.whereForType(typeRows, facetTypeDef)] : []),
|
||||||
...(facets?.status ? [{ status: facets.status }] : []),
|
...(facets?.status ? [{ status: facets.status }] : []),
|
||||||
|
...(facetTitle ? [{ title: ci(facetTitle) }] : []),
|
||||||
|
...(facetCustomer ? [this.customerWhere(facetCustomer)] : []),
|
||||||
...(facetAssignee
|
...(facetAssignee
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -915,15 +923,18 @@ export class ActivitiesService {
|
|||||||
const fromDate = range?.from ? new Date(range.from) : null;
|
const fromDate = range?.from ? new Date(range.from) : null;
|
||||||
const toDate = range?.to ? new Date(range.to) : null;
|
const toDate = range?.to ? new Date(range.to) : null;
|
||||||
if (fromDate || toDate) {
|
if (fromDate || toDate) {
|
||||||
return {
|
return this.withDue(
|
||||||
...base,
|
{
|
||||||
dueAt: {
|
...base,
|
||||||
...(fromDate && !Number.isNaN(fromDate.getTime())
|
dueAt: {
|
||||||
? { gte: fromDate }
|
...(fromDate && !Number.isNaN(fromDate.getTime())
|
||||||
: {}),
|
? { gte: fromDate }
|
||||||
...(toDate && !Number.isNaN(toDate.getTime()) ? { lt: toDate } : {}),
|
: {}),
|
||||||
|
...(toDate && !Number.isNaN(toDate.getTime()) ? { lt: toDate } : {}),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
facets?.due,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicit status facet overrides the status implied by time tabs.
|
// Explicit status facet overrides the status implied by time tabs.
|
||||||
@@ -937,28 +948,62 @@ export class ActivitiesService {
|
|||||||
|
|
||||||
switch (filter) {
|
switch (filter) {
|
||||||
case 'today':
|
case 'today':
|
||||||
return { ...base, ...impliedOpen, dueAt: { gte: start, lt: end } };
|
return this.withDue(
|
||||||
|
{ ...base, ...impliedOpen, dueAt: { gte: start, lt: end } },
|
||||||
|
facets?.due,
|
||||||
|
);
|
||||||
case 'overdue':
|
case 'overdue':
|
||||||
// عقبافتاده ذاتاً یعنی باز و موعدگذشته
|
// عقبافتاده ذاتاً یعنی باز و موعدگذشته
|
||||||
return {
|
return this.withDue(
|
||||||
...base,
|
{
|
||||||
...(facets?.status ? { status: facets.status } : { status: 'open' as const }),
|
...base,
|
||||||
dueAt: { lt: start },
|
...(facets?.status ? { status: facets.status } : { status: 'open' as const }),
|
||||||
};
|
dueAt: { lt: start },
|
||||||
|
},
|
||||||
|
facets?.due,
|
||||||
|
);
|
||||||
case 'upcoming':
|
case 'upcoming':
|
||||||
return {
|
return this.withDue(
|
||||||
...base,
|
{
|
||||||
...impliedOpen,
|
...base,
|
||||||
OR: [{ dueAt: { gte: end } }, { dueAt: null }],
|
...impliedOpen,
|
||||||
};
|
OR: [{ dueAt: { gte: end } }, { dueAt: null }],
|
||||||
|
},
|
||||||
|
facets?.due,
|
||||||
|
);
|
||||||
case 'done':
|
case 'done':
|
||||||
return { ...base, ...impliedDone };
|
return this.withDue({ ...base, ...impliedDone }, facets?.due);
|
||||||
case 'all':
|
case 'all':
|
||||||
default:
|
default:
|
||||||
return base;
|
return this.withDue(base, facets?.due);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** نام شخص (چندکلمهای) یا نام شرکت */
|
||||||
|
private customerWhere(term: string): Prisma.ActivityWhereInput {
|
||||||
|
const parts = term.split(/\s+/).filter(Boolean);
|
||||||
|
const person: Prisma.PersonWhereInput =
|
||||||
|
parts.length <= 1
|
||||||
|
? { OR: [{ firstName: ci(term) }, { lastName: ci(term) }] }
|
||||||
|
: {
|
||||||
|
AND: parts.map((part) => ({
|
||||||
|
OR: [{ firstName: ci(part) }, { lastName: ci(part) }],
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
OR: [{ person }, { company: { name: ci(term) } }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** وجود یا نبود زمان شروع، روی هر فیلتر زمانی */
|
||||||
|
private withDue(
|
||||||
|
where: Prisma.ActivityWhereInput,
|
||||||
|
due?: 'set' | 'unset',
|
||||||
|
): Prisma.ActivityWhereInput {
|
||||||
|
if (due !== 'set' && due !== 'unset') return where;
|
||||||
|
return { AND: [where, due === 'set' ? { dueAt: { not: null } } : { dueAt: null }] };
|
||||||
|
}
|
||||||
|
|
||||||
private async fireDone(
|
private async fireDone(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
row: {
|
row: {
|
||||||
|
|||||||
@@ -69,6 +69,23 @@ export class ListActivitiesQueryDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
assigneeId?: string;
|
assigneeId?: string;
|
||||||
|
|
||||||
|
/** فیلتر عنوان فعالیت، جدا از جستجوی کلی */
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(120)
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
/** نام شخص یا شرکتِ مشتری */
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(120)
|
||||||
|
customer?: string;
|
||||||
|
|
||||||
|
/** زمان شروع: set = دارد، unset = ندارد */
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['set', 'unset'])
|
||||||
|
due?: 'set' | 'unset';
|
||||||
|
|
||||||
/** نوع فعالیت (تعریفشده در سازمان) */
|
/** نوع فعالیت (تعریفشده در سازمان) */
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ const route = useRoute();
|
|||||||
|
|
||||||
function openActivity(id: string) {
|
function openActivity(id: string) {
|
||||||
const query: Record<string, string> = {};
|
const query: Record<string, string> = {};
|
||||||
for (const key of ['board', 'view', 'desk', 'filter', 'stage', 'q', 'typeDefId', 'status', 'assigneeId']) {
|
for (const key of ['board', 'view', 'desk', 'filter', 'stage', 'q', 'typeDefId', 'status', 'assigneeId', 'title', 'customer', 'due']) {
|
||||||
const value = route.query[key];
|
const value = route.query[key];
|
||||||
if (typeof value === 'string' && value) query[key] = value;
|
if (typeof value === 'string' && value) query[key] = value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,492 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
export type DeskFilterField = 'title' | 'who' | 'customer' | 'type' | 'when' | 'status';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/** یک ستون، یا پنل کامل بالای صفحه */
|
||||||
|
field: DeskFilterField | 'all';
|
||||||
|
title: string;
|
||||||
|
customer: string;
|
||||||
|
due: '' | 'set' | 'unset';
|
||||||
|
typeKey: string;
|
||||||
|
statusKey: string;
|
||||||
|
assigneeId: string;
|
||||||
|
showAssignee: boolean;
|
||||||
|
users: Array<{ id: string; fullName: string }>;
|
||||||
|
types: Array<{ key: string; label: string }>;
|
||||||
|
statuses: Array<{ key: string; label: string }>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
title: [value: string];
|
||||||
|
customer: [value: string];
|
||||||
|
due: [value: '' | 'set' | 'unset'];
|
||||||
|
type: [value: string];
|
||||||
|
status: [value: string];
|
||||||
|
assignee: [value: string];
|
||||||
|
clear: [];
|
||||||
|
done: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const LIST_CAP = 40;
|
||||||
|
const titleDraft = ref(props.title);
|
||||||
|
const customerDraft = ref(props.customer);
|
||||||
|
const whoQuery = ref('');
|
||||||
|
const typeQuery = ref('');
|
||||||
|
const openSection = ref<DeskFilterField | null>(null);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.title,
|
||||||
|
(value) => {
|
||||||
|
titleDraft.value = value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => props.customer,
|
||||||
|
(value) => {
|
||||||
|
customerDraft.value = value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function norm(value: string) {
|
||||||
|
return value.trim().replace(/ي/g, 'ی').replace(/ك/g, 'ک').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredUsers = computed(() => {
|
||||||
|
const q = norm(whoQuery.value);
|
||||||
|
const matches = props.users.filter((user) => !q || norm(user.fullName).includes(q));
|
||||||
|
const selected = props.assigneeId
|
||||||
|
? props.users.find((user) => user.id === props.assigneeId)
|
||||||
|
: undefined;
|
||||||
|
const head = selected && !matches.slice(0, LIST_CAP).some((user) => user.id === selected.id)
|
||||||
|
? [selected]
|
||||||
|
: [];
|
||||||
|
return [...head, ...matches.filter((user) => user.id !== selected?.id)].slice(0, LIST_CAP);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hiddenUsers = computed(() => {
|
||||||
|
const q = norm(whoQuery.value);
|
||||||
|
const count = props.users.filter((user) => !q || norm(user.fullName).includes(q)).length;
|
||||||
|
return Math.max(0, count - filteredUsers.value.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
const showTypeSearch = computed(() => props.types.length > 8);
|
||||||
|
|
||||||
|
const visibleTypes = computed(() => {
|
||||||
|
const q = norm(typeQuery.value);
|
||||||
|
const all = props.types.filter((item) => item.key === 'all');
|
||||||
|
const rest = props.types.filter(
|
||||||
|
(item) => item.key !== 'all' && (!q || norm(item.label).includes(q)),
|
||||||
|
);
|
||||||
|
return [...all, ...rest];
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasAny = computed(
|
||||||
|
() =>
|
||||||
|
Boolean(props.title.trim() || props.customer.trim() || props.due || props.assigneeId) ||
|
||||||
|
props.typeKey !== 'all' ||
|
||||||
|
props.statusKey !== 'all',
|
||||||
|
);
|
||||||
|
|
||||||
|
const dueOptions: Array<{ key: '' | 'set' | 'unset'; label: string }> = [
|
||||||
|
{ key: '', label: 'همه' },
|
||||||
|
{ key: 'set', label: 'دارد' },
|
||||||
|
{ key: 'unset', label: 'ندارد' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const whoLabel = computed(
|
||||||
|
() => props.users.find((user) => user.id === props.assigneeId)?.fullName || 'همه',
|
||||||
|
);
|
||||||
|
const typeLabel = computed(
|
||||||
|
() => props.types.find((item) => item.key === props.typeKey)?.label || 'همه',
|
||||||
|
);
|
||||||
|
const statusLabel = computed(
|
||||||
|
() => props.statuses.find((item) => item.key === props.statusKey)?.label || 'همه',
|
||||||
|
);
|
||||||
|
const dueLabel = computed(() => dueOptions.find((item) => item.key === props.due)?.label ?? 'همه');
|
||||||
|
|
||||||
|
function opened(name: DeskFilterField) {
|
||||||
|
return props.field !== 'all' || openSection.value === name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSection(name: DeskFilterField) {
|
||||||
|
openSection.value = openSection.value === name ? null : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finish() {
|
||||||
|
if (props.field === 'all') openSection.value = null;
|
||||||
|
else emit('done');
|
||||||
|
}
|
||||||
|
|
||||||
|
function shows(name: DeskFilterField) {
|
||||||
|
if (name === 'who' && !props.showAssignee) return false;
|
||||||
|
return props.field === 'all' || props.field === name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTitle() {
|
||||||
|
emit('title', titleDraft.value.trim());
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCustomer() {
|
||||||
|
emit('customer', customerDraft.value.trim());
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickDue(next: '' | 'set' | 'unset') {
|
||||||
|
emit('due', props.due === next ? '' : next);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickType(next: string) {
|
||||||
|
emit('type', props.typeKey === next && next !== 'all' ? 'all' : next);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickStatus(next: string) {
|
||||||
|
emit('status', props.statusKey === next && next !== 'all' ? 'all' : next);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickAssignee(next: string) {
|
||||||
|
emit('assignee', props.assigneeId === next ? '' : next);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="filters" @click.stop>
|
||||||
|
<section v-if="shows('title')">
|
||||||
|
<button
|
||||||
|
v-if="field === 'all'"
|
||||||
|
type="button"
|
||||||
|
class="acc"
|
||||||
|
:class="{ on: Boolean(title.trim()) }"
|
||||||
|
@click="toggleSection('title')"
|
||||||
|
>
|
||||||
|
<span>عنوان فعالیت</span>
|
||||||
|
<em>{{ title.trim() || 'همه' }}</em>
|
||||||
|
</button>
|
||||||
|
<form v-if="opened('title')" class="draft" @submit.prevent="applyTitle">
|
||||||
|
<input
|
||||||
|
v-model="titleDraft"
|
||||||
|
type="search"
|
||||||
|
placeholder="بخشی از عنوان"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<button type="submit">اعمال</button>
|
||||||
|
<button v-if="title" type="button" class="quiet" @click="titleDraft = ''; applyTitle()">
|
||||||
|
پاک
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="shows('who')">
|
||||||
|
<button
|
||||||
|
v-if="field === 'all'"
|
||||||
|
type="button"
|
||||||
|
class="acc"
|
||||||
|
:class="{ on: Boolean(assigneeId) }"
|
||||||
|
@click="toggleSection('who')"
|
||||||
|
>
|
||||||
|
<span>کارشناس</span>
|
||||||
|
<em>{{ whoLabel }}</em>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
v-if="opened('who')"
|
||||||
|
v-model="whoQuery"
|
||||||
|
class="find"
|
||||||
|
type="search"
|
||||||
|
placeholder="جستجوی نام کارشناس"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<div v-if="opened('who')" class="pick" role="listbox" aria-label="کارشناس">
|
||||||
|
<button type="button" :class="{ on: !assigneeId }" @click="pickAssignee('')">
|
||||||
|
همه کارشناسان
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-for="user in filteredUsers"
|
||||||
|
:key="user.id"
|
||||||
|
type="button"
|
||||||
|
:class="{ on: assigneeId === user.id }"
|
||||||
|
@click="pickAssignee(user.id)"
|
||||||
|
>
|
||||||
|
{{ user.fullName }}
|
||||||
|
</button>
|
||||||
|
<p v-if="whoQuery.trim() && !filteredUsers.length" class="empty">کارشناسی با این نام نیست</p>
|
||||||
|
</div>
|
||||||
|
<p v-if="opened('who') && hiddenUsers" class="hint">برای دیدن بقیه، نام را دقیقتر بنویسید</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="shows('customer')">
|
||||||
|
<button
|
||||||
|
v-if="field === 'all'"
|
||||||
|
type="button"
|
||||||
|
class="acc"
|
||||||
|
:class="{ on: Boolean(customer.trim()) }"
|
||||||
|
@click="toggleSection('customer')"
|
||||||
|
>
|
||||||
|
<span>مشتری</span>
|
||||||
|
<em>{{ customer.trim() || 'همه' }}</em>
|
||||||
|
</button>
|
||||||
|
<form v-if="opened('customer')" class="draft" @submit.prevent="applyCustomer">
|
||||||
|
<input
|
||||||
|
v-model="customerDraft"
|
||||||
|
type="search"
|
||||||
|
placeholder="نام شخص یا شرکت"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<button type="submit">اعمال</button>
|
||||||
|
<button
|
||||||
|
v-if="customer"
|
||||||
|
type="button"
|
||||||
|
class="quiet"
|
||||||
|
@click="customerDraft = ''; applyCustomer()"
|
||||||
|
>
|
||||||
|
پاک
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="shows('type')">
|
||||||
|
<button
|
||||||
|
v-if="field === 'all'"
|
||||||
|
type="button"
|
||||||
|
class="acc"
|
||||||
|
:class="{ on: typeKey !== 'all' }"
|
||||||
|
@click="toggleSection('type')"
|
||||||
|
>
|
||||||
|
<span>نوع فعالیت</span>
|
||||||
|
<em>{{ typeLabel }}</em>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
v-if="opened('type') && showTypeSearch"
|
||||||
|
v-model="typeQuery"
|
||||||
|
class="find"
|
||||||
|
type="search"
|
||||||
|
placeholder="جستجوی نوع"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<div v-if="opened('type')" class="pick" role="listbox" aria-label="نوع فعالیت">
|
||||||
|
<button
|
||||||
|
v-for="opt in visibleTypes"
|
||||||
|
:key="opt.key"
|
||||||
|
type="button"
|
||||||
|
:class="{ on: typeKey === opt.key }"
|
||||||
|
@click="pickType(opt.key)"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</button>
|
||||||
|
<p v-if="visibleTypes.length <= 1 && typeQuery.trim()" class="empty">این نوع پیدا نشد</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="shows('when')">
|
||||||
|
<button
|
||||||
|
v-if="field === 'all'"
|
||||||
|
type="button"
|
||||||
|
class="acc"
|
||||||
|
:class="{ on: Boolean(due) }"
|
||||||
|
@click="toggleSection('when')"
|
||||||
|
>
|
||||||
|
<span>زمان شروع</span>
|
||||||
|
<em>{{ dueLabel }}</em>
|
||||||
|
</button>
|
||||||
|
<p v-else class="hint tight">فقط بودن یا نبودن زمان شروع</p>
|
||||||
|
<div v-if="opened('when')" class="seg" role="radiogroup" aria-label="زمان شروع">
|
||||||
|
<button
|
||||||
|
v-for="opt in dueOptions"
|
||||||
|
:key="opt.key || 'any'"
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
:aria-checked="due === opt.key"
|
||||||
|
:class="{ on: due === opt.key }"
|
||||||
|
@click="pickDue(opt.key)"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="shows('status')">
|
||||||
|
<button
|
||||||
|
v-if="field === 'all'"
|
||||||
|
type="button"
|
||||||
|
class="acc"
|
||||||
|
:class="{ on: statusKey !== 'all' }"
|
||||||
|
@click="toggleSection('status')"
|
||||||
|
>
|
||||||
|
<span>وضعیت</span>
|
||||||
|
<em>{{ statusLabel }}</em>
|
||||||
|
</button>
|
||||||
|
<div v-if="opened('status')" class="pick" role="listbox" aria-label="وضعیت">
|
||||||
|
<button
|
||||||
|
v-for="opt in statuses"
|
||||||
|
:key="opt.key"
|
||||||
|
type="button"
|
||||||
|
:class="{ on: statusKey === opt.key }"
|
||||||
|
@click="pickStatus(opt.key)"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="field === 'all' && hasAny"
|
||||||
|
type="button"
|
||||||
|
class="clear"
|
||||||
|
@click="openSection = null; emit('clear')"
|
||||||
|
>
|
||||||
|
پاک کردن فیلترها
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.filters {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.acc {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.6rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.28rem 0.15rem;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
text-align: start;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.acc em {
|
||||||
|
font-style: normal;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
max-width: 9.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.acc.on em {
|
||||||
|
color: #0f766e;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft input,
|
||||||
|
.find {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0.4rem 0.55rem;
|
||||||
|
font: inherit;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft button,
|
||||||
|
.pick button,
|
||||||
|
.seg button,
|
||||||
|
.clear {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft button {
|
||||||
|
flex: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0f766e;
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.4rem 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft button.quiet {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick {
|
||||||
|
max-height: 11.5rem;
|
||||||
|
overflow: auto;
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick button {
|
||||||
|
text-align: start;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.42rem 0.5rem;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick button:hover,
|
||||||
|
.pick button.on {
|
||||||
|
background: #f0fdfa;
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pick button.on {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seg {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seg button {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.4rem 0.2rem;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seg button.on {
|
||||||
|
background: #fff;
|
||||||
|
color: #0f766e;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty,
|
||||||
|
.hint {
|
||||||
|
margin: 0.25rem 0.2rem 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint.tight {
|
||||||
|
margin: 0 0 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
padding: 0.55rem 0.2rem 0.15rem;
|
||||||
|
text-align: start;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import ExcelImportFlow from '@/components/ExcelImportFlow.vue';
|
import ExcelImportFlow from '@/components/ExcelImportFlow.vue';
|
||||||
import ActivitiesKanban from '@/components/activities/ActivitiesKanban.vue';
|
import ActivitiesKanban from '@/components/activities/ActivitiesKanban.vue';
|
||||||
|
import ActivityDeskFilters from '@/components/activities/ActivityDeskFilters.vue';
|
||||||
import ActivityStagesManager from '@/components/activities/ActivityStagesManager.vue';
|
import ActivityStagesManager from '@/components/activities/ActivityStagesManager.vue';
|
||||||
import ActivityTypesManager from '@/components/activities/ActivityTypesManager.vue';
|
import ActivityTypesManager from '@/components/activities/ActivityTypesManager.vue';
|
||||||
import { PIPELINE_ACCENT } from '@/lib/pipeline-board';
|
import { PIPELINE_ACCENT } from '@/lib/pipeline-board';
|
||||||
@@ -62,9 +63,9 @@ const ACT_COLUMNS = [
|
|||||||
{ key: 'title', label: 'عنوان فعالیت', width: 'minmax(0, 2fr)' },
|
{ key: 'title', label: 'عنوان فعالیت', width: 'minmax(0, 2fr)' },
|
||||||
{ key: 'who', label: 'کارشناس', width: 'minmax(7rem, 0.9fr)' },
|
{ key: 'who', label: 'کارشناس', width: 'minmax(7rem, 0.9fr)' },
|
||||||
{ key: 'customer', label: 'مشتری', width: 'minmax(7rem, 1fr)' },
|
{ key: 'customer', label: 'مشتری', width: 'minmax(7rem, 1fr)' },
|
||||||
{ key: 'type', label: 'نوع فعالیت', width: '7.6rem' },
|
{ key: 'type', label: 'نوع فعالیت', width: '9.2rem' },
|
||||||
{ key: 'when', label: 'زمان شروع', width: '7.2rem' },
|
{ key: 'when', label: 'زمان شروع', width: '8.6rem' },
|
||||||
{ key: 'status', label: 'وضعیت', width: '8.6rem' },
|
{ key: 'status', label: 'وضعیت', width: '9.2rem' },
|
||||||
] as const;
|
] as const;
|
||||||
type ActColKey = (typeof ACT_COLUMNS)[number]['key'];
|
type ActColKey = (typeof ACT_COLUMNS)[number]['key'];
|
||||||
registerTableColumns(
|
registerTableColumns(
|
||||||
@@ -204,6 +205,7 @@ const data = ref<PaginatedResponse<ActivityDto> | null>(null);
|
|||||||
const q = ref(typeof route.query.q === 'string' ? route.query.q : '');
|
const q = ref(typeof route.query.q === 'string' ? route.query.q : '');
|
||||||
const users = ref<OrgUserDto[]>([]);
|
const users = ref<OrgUserDto[]>([]);
|
||||||
const filterOpen = ref(false);
|
const filterOpen = ref(false);
|
||||||
|
const openCol = ref<ActColKey | null>(null);
|
||||||
const ideasOpen = ref(false);
|
const ideasOpen = ref(false);
|
||||||
|
|
||||||
const ACTIVITY_TYPES: ActivityType[] = ['call', 'meeting', 'task', 'note'];
|
const ACTIVITY_TYPES: ActivityType[] = ['call', 'meeting', 'task', 'note'];
|
||||||
@@ -240,6 +242,16 @@ const statusFilter = computed<StatusFilter>(() => {
|
|||||||
const assigneeFilter = computed(() =>
|
const assigneeFilter = computed(() =>
|
||||||
typeof route.query.assigneeId === 'string' ? route.query.assigneeId : '',
|
typeof route.query.assigneeId === 'string' ? route.query.assigneeId : '',
|
||||||
);
|
);
|
||||||
|
const titleFilter = computed(() =>
|
||||||
|
typeof route.query.title === 'string' ? route.query.title : '',
|
||||||
|
);
|
||||||
|
const customerFilter = computed(() =>
|
||||||
|
typeof route.query.customer === 'string' ? route.query.customer : '',
|
||||||
|
);
|
||||||
|
const dueFilter = computed<'' | 'set' | 'unset'>(() => {
|
||||||
|
const value = route.query.due;
|
||||||
|
return value === 'set' || value === 'unset' ? value : '';
|
||||||
|
});
|
||||||
const activityModal = ref<{
|
const activityModal = ref<{
|
||||||
type?: ActivityType;
|
type?: ActivityType;
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -305,6 +317,9 @@ const sharedParams = computed<Record<string, string | undefined>>(() => ({
|
|||||||
: undefined,
|
: undefined,
|
||||||
status: statusFilter.value === 'all' ? undefined : statusFilter.value,
|
status: statusFilter.value === 'all' ? undefined : statusFilter.value,
|
||||||
assigneeId: assigneeFilter.value || undefined,
|
assigneeId: assigneeFilter.value || undefined,
|
||||||
|
title: titleFilter.value.trim() || undefined,
|
||||||
|
customer: customerFilter.value.trim() || undefined,
|
||||||
|
due: dueFilter.value || undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function stageDotStyle(s: ActivityStageDto) {
|
function stageDotStyle(s: ActivityStageDto) {
|
||||||
@@ -315,6 +330,9 @@ const activeFacetCount = computed(() => {
|
|||||||
if (typeFilter.value !== 'all') n += 1;
|
if (typeFilter.value !== 'all') n += 1;
|
||||||
if (statusFilter.value !== 'all') n += 1;
|
if (statusFilter.value !== 'all') n += 1;
|
||||||
if (assigneeFilter.value) n += 1;
|
if (assigneeFilter.value) n += 1;
|
||||||
|
if (titleFilter.value.trim()) n += 1;
|
||||||
|
if (customerFilter.value.trim()) n += 1;
|
||||||
|
if (dueFilter.value) n += 1;
|
||||||
return n;
|
return n;
|
||||||
});
|
});
|
||||||
const filterBtnLabel = computed(() =>
|
const filterBtnLabel = computed(() =>
|
||||||
@@ -400,7 +418,7 @@ async function loadStages() {
|
|||||||
|
|
||||||
function activityRoute(id: string, edit = false) {
|
function activityRoute(id: string, edit = false) {
|
||||||
const query: Record<string, string> = {};
|
const query: Record<string, string> = {};
|
||||||
for (const key of ['board', 'view', 'desk', 'filter', 'stage', 'q', 'typeDefId', 'status', 'assigneeId'] as const) {
|
for (const key of ['board', 'view', 'desk', 'filter', 'stage', 'q', 'typeDefId', 'status', 'assigneeId', 'title', 'customer', 'due'] as const) {
|
||||||
const value = route.query[key];
|
const value = route.query[key];
|
||||||
if (typeof value === 'string' && value) query[key] = value;
|
if (typeof value === 'string' && value) query[key] = value;
|
||||||
}
|
}
|
||||||
@@ -721,8 +739,10 @@ function setTypeFilter(next: TypeFilter) {
|
|||||||
patchFacetQuery({ typeDefId: next === 'all' ? undefined : next, type: undefined });
|
patchFacetQuery({ typeDefId: next === 'all' ? undefined : next, type: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStatusFilter(next: StatusFilter) {
|
function setStatusFilter(next: string) {
|
||||||
patchFacetQuery({ status: next === 'all' ? undefined : next });
|
const value: StatusFilter =
|
||||||
|
next === 'open' || next === 'done' || next === 'canceled' ? next : 'all';
|
||||||
|
patchFacetQuery({ status: value === 'all' ? undefined : value });
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAssigneeFilter(next: string) {
|
function setAssigneeFilter(next: string) {
|
||||||
@@ -732,14 +752,55 @@ function setAssigneeFilter(next: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setTitleFilter(next: string) {
|
||||||
|
patchFacetQuery({ title: next.trim() || undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCustomerFilter(next: string) {
|
||||||
|
patchFacetQuery({ customer: next.trim() || undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDueFilter(next: '' | 'set' | 'unset') {
|
||||||
|
patchFacetQuery({ due: next || undefined });
|
||||||
|
}
|
||||||
|
|
||||||
function clearFacets() {
|
function clearFacets() {
|
||||||
patchFacetQuery({
|
patchFacetQuery({
|
||||||
type: undefined,
|
type: undefined,
|
||||||
|
typeDefId: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
assigneeId: undefined,
|
assigneeId: undefined,
|
||||||
|
title: undefined,
|
||||||
|
customer: undefined,
|
||||||
|
due: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function colFiltered(key: ActColKey) {
|
||||||
|
if (key === 'title') return Boolean(titleFilter.value.trim());
|
||||||
|
if (key === 'who') return Boolean(assigneeFilter.value);
|
||||||
|
if (key === 'customer') return Boolean(customerFilter.value.trim());
|
||||||
|
if (key === 'type') return typeFilter.value !== 'all';
|
||||||
|
if (key === 'when') return Boolean(dueFilter.value);
|
||||||
|
if (key === 'status') return statusFilter.value !== 'all';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFilter() {
|
||||||
|
openCol.value = null;
|
||||||
|
filterOpen.value = !filterOpen.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCol(key: ActColKey) {
|
||||||
|
if (key === 'who' && !showDeskTabs.value) return;
|
||||||
|
filterOpen.value = false;
|
||||||
|
openCol.value = openCol.value === key ? null : key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFacetDone() {
|
||||||
|
openCol.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
async function complete(id: string) {
|
async function complete(id: string) {
|
||||||
menuId.value = null;
|
menuId.value = null;
|
||||||
boardMenuId.value = null;
|
boardMenuId.value = null;
|
||||||
@@ -754,6 +815,7 @@ async function complete(id: string) {
|
|||||||
|
|
||||||
function closePopovers() {
|
function closePopovers() {
|
||||||
filterOpen.value = false;
|
filterOpen.value = false;
|
||||||
|
openCol.value = null;
|
||||||
menuId.value = null;
|
menuId.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,6 +997,9 @@ watch(
|
|||||||
route.query.typeDefId,
|
route.query.typeDefId,
|
||||||
route.query.status,
|
route.query.status,
|
||||||
route.query.assigneeId,
|
route.query.assigneeId,
|
||||||
|
route.query.title,
|
||||||
|
route.query.customer,
|
||||||
|
route.query.due,
|
||||||
route.query.stage,
|
route.query.stage,
|
||||||
route.query.view,
|
route.query.view,
|
||||||
],
|
],
|
||||||
@@ -1286,63 +1351,33 @@ onUnmounted(() => {
|
|||||||
:class="{ on: activeFacetCount > 0 }"
|
:class="{ on: activeFacetCount > 0 }"
|
||||||
:title="filterBtnLabel"
|
:title="filterBtnLabel"
|
||||||
:aria-label="filterBtnLabel"
|
:aria-label="filterBtnLabel"
|
||||||
@click="filterOpen = !filterOpen"
|
@click="toggleFilter"
|
||||||
>
|
>
|
||||||
<UiIcon name="filter" :size="15" />
|
<UiIcon name="filter" :size="15" />
|
||||||
<span class="filter-label">{{ filterBtnLabel }}</span>
|
<span class="filter-label">{{ filterBtnLabel }}</span>
|
||||||
<UiIcon name="chevron" :size="13" class="chev" />
|
<UiIcon name="chevron" :size="13" class="chev" />
|
||||||
</button>
|
</button>
|
||||||
<div v-if="filterOpen" class="popover filter-popover">
|
<div v-if="filterOpen" class="popover filter-popover">
|
||||||
<p class="facet-title">نوع کار</p>
|
<ActivityDeskFilters
|
||||||
<button
|
field="all"
|
||||||
v-for="opt in TYPE_FILTERS"
|
:title="titleFilter"
|
||||||
:key="`type-${opt.key}`"
|
:customer="customerFilter"
|
||||||
type="button"
|
:due="dueFilter"
|
||||||
:class="{ on: typeFilter === opt.key }"
|
:type-key="typeFilter"
|
||||||
@click="setTypeFilter(opt.key)"
|
:status-key="statusFilter"
|
||||||
>
|
:assignee-id="assigneeFilter"
|
||||||
{{ opt.label }}
|
:show-assignee="showDeskTabs"
|
||||||
</button>
|
:users="users"
|
||||||
|
:types="TYPE_FILTERS"
|
||||||
<p class="facet-title">وضعیت</p>
|
:statuses="STATUS_FILTERS"
|
||||||
<button
|
@title="setTitleFilter"
|
||||||
v-for="opt in STATUS_FILTERS"
|
@customer="setCustomerFilter"
|
||||||
:key="`status-${opt.key}`"
|
@due="setDueFilter"
|
||||||
type="button"
|
@type="setTypeFilter"
|
||||||
:class="{ on: statusFilter === opt.key }"
|
@status="setStatusFilter"
|
||||||
@click="setStatusFilter(opt.key)"
|
@assignee="setAssigneeFilter"
|
||||||
>
|
@clear="clearFacets"
|
||||||
{{ opt.label }}
|
/>
|
||||||
</button>
|
|
||||||
|
|
||||||
<template v-if="showDeskTabs">
|
|
||||||
<p class="facet-title">کارشناس</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
:class="{ on: !assigneeFilter }"
|
|
||||||
@click="setAssigneeFilter('')"
|
|
||||||
>
|
|
||||||
همه کارشناسان
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-for="user in users"
|
|
||||||
:key="user.id"
|
|
||||||
type="button"
|
|
||||||
:class="{ on: assigneeFilter === user.id }"
|
|
||||||
@click="setAssigneeFilter(user.id)"
|
|
||||||
>
|
|
||||||
{{ user.fullName }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="activeFacetCount"
|
|
||||||
type="button"
|
|
||||||
class="clear-facets"
|
|
||||||
@click="clearFacets"
|
|
||||||
>
|
|
||||||
پاک کردن فیلترها
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1522,7 +1557,46 @@ onUnmounted(() => {
|
|||||||
<span class="row-num">#</span>
|
<span class="row-num">#</span>
|
||||||
<span v-if="selectMode" class="tick-col" />
|
<span v-if="selectMode" class="tick-col" />
|
||||||
<template v-for="col in ACT_COLUMNS" :key="col.key">
|
<template v-for="col in ACT_COLUMNS" :key="col.key">
|
||||||
<span v-if="colOn(col.key)">{{ col.label }}</span>
|
<div v-if="colOn(col.key)" class="th-cell" @click.stop>
|
||||||
|
<span>{{ col.label }}</span>
|
||||||
|
<button
|
||||||
|
v-if="col.key !== 'who' || showDeskTabs"
|
||||||
|
type="button"
|
||||||
|
class="th-filter"
|
||||||
|
:class="{ on: colFiltered(col.key) }"
|
||||||
|
:aria-label="`فیلتر ${col.label}`"
|
||||||
|
:aria-expanded="openCol === col.key"
|
||||||
|
@click="toggleCol(col.key)"
|
||||||
|
>
|
||||||
|
<UiIcon name="filter" :size="12" />
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="openCol === col.key"
|
||||||
|
class="popover col-popover"
|
||||||
|
:class="{ 'align-end': col.key === 'when' || col.key === 'status' }"
|
||||||
|
>
|
||||||
|
<ActivityDeskFilters
|
||||||
|
:field="col.key"
|
||||||
|
:title="titleFilter"
|
||||||
|
:customer="customerFilter"
|
||||||
|
:due="dueFilter"
|
||||||
|
:type-key="typeFilter"
|
||||||
|
:status-key="statusFilter"
|
||||||
|
:assignee-id="assigneeFilter"
|
||||||
|
:show-assignee="showDeskTabs"
|
||||||
|
:users="users"
|
||||||
|
:types="TYPE_FILTERS"
|
||||||
|
:statuses="STATUS_FILTERS"
|
||||||
|
@title="setTitleFilter"
|
||||||
|
@customer="setCustomerFilter"
|
||||||
|
@due="setDueFilter"
|
||||||
|
@type="setTypeFilter"
|
||||||
|
@status="setStatusFilter"
|
||||||
|
@assignee="setAssigneeFilter"
|
||||||
|
@done="onFacetDone"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<span />
|
<span />
|
||||||
</div>
|
</div>
|
||||||
@@ -2500,9 +2574,10 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filter-popover {
|
.filter-popover {
|
||||||
min-width: 14rem;
|
width: min(20rem, calc(100vw - 1.5rem));
|
||||||
max-height: min(70vh, 28rem);
|
max-height: min(70vh, 32rem);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
padding: 0.7rem 0.75rem 0.55rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.facet-title {
|
.facet-title {
|
||||||
@@ -2619,13 +2694,15 @@ h1 {
|
|||||||
.thead,
|
.thead,
|
||||||
.trow {
|
.trow {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2.4rem minmax(0, 2fr) minmax(7rem, 0.9fr) minmax(7rem, 1fr) 7.6rem 7.2rem 8.6rem 2.4rem;
|
grid-template-columns: 2.4rem minmax(0, 2fr) minmax(7rem, 0.9fr) minmax(7rem, 1fr) 9.2rem 8.6rem 9.2rem 2.4rem;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.95rem 1.1rem;
|
padding: 0.95rem 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thead {
|
.thead {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -2633,6 +2710,55 @@ h1 {
|
|||||||
border-radius: var(--radius) var(--radius) 0 0;
|
border-radius: var(--radius) var(--radius) 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.th-cell {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.th-cell > span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.th-filter {
|
||||||
|
flex: none;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 1.35rem;
|
||||||
|
height: 1.35rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: #94a3b8;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.th-filter:hover,
|
||||||
|
.th-filter.on,
|
||||||
|
.th-filter[aria-expanded='true'] {
|
||||||
|
color: #0f766e;
|
||||||
|
background: #f0fdfa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-popover {
|
||||||
|
inset-inline-start: 0;
|
||||||
|
inset-inline-end: auto;
|
||||||
|
width: 16.5rem;
|
||||||
|
padding: 0.65rem 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-popover.align-end {
|
||||||
|
inset-inline-start: auto;
|
||||||
|
inset-inline-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.trow {
|
.trow {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-bottom: 1px solid #f8fafc;
|
border-bottom: 1px solid #f8fafc;
|
||||||
|
|||||||
Reference in New Issue
Block a user