Add column visibility menu to activities and API keys tables, and make related person optional when creating an activity
This commit is contained in:
@@ -159,6 +159,15 @@ declare module 'vue' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** برای جدولهای غیر `<table>` (مثلاً گرید div) که ستونهایشان را خودشان پنهان میکنند. */
|
||||||
|
export function registerTableColumns(key: string, labels: string[]) {
|
||||||
|
ensureInit();
|
||||||
|
const prev = labelsByTable[key];
|
||||||
|
if (!prev || prev.length !== labels.length || prev.some((v, i) => v !== labels[i])) {
|
||||||
|
labelsByTable[key] = labels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useTableColumns(table: () => string) {
|
export function useTableColumns(table: () => string) {
|
||||||
ensureInit();
|
ensureInit();
|
||||||
const columns = () =>
|
const columns = () =>
|
||||||
|
|||||||
@@ -45,6 +45,34 @@ import { brand } from '@mento/shared';
|
|||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { canViewAllRecords } from '@/lib/roles';
|
import { canViewAllRecords } from '@/lib/roles';
|
||||||
import { PAGE_SIZE_OPTIONS, usePageSize } from '@/lib/pageSize';
|
import { PAGE_SIZE_OPTIONS, usePageSize } from '@/lib/pageSize';
|
||||||
|
import { registerTableColumns, useTableColumns } from '@/lib/tableColumns';
|
||||||
|
import TableColumnsMenu from '@/components/TableColumnsMenu.vue';
|
||||||
|
|
||||||
|
const ACT_TABLE_KEY = 'activities';
|
||||||
|
const ACT_COLUMNS = [
|
||||||
|
{ key: 'title', label: 'مشتری / عنوان فعالیت', width: 'minmax(0, 2.2fr)' },
|
||||||
|
{ key: 'type', label: 'نوع فعالیت', width: '7.6rem' },
|
||||||
|
{ key: 'when', label: 'زمان شروع', width: '7.2rem' },
|
||||||
|
{ key: 'who', label: 'کارشناس', width: '8.4rem' },
|
||||||
|
{ key: 'status', label: 'وضعیت', width: '8.6rem' },
|
||||||
|
] as const;
|
||||||
|
type ActColKey = (typeof ACT_COLUMNS)[number]['key'];
|
||||||
|
registerTableColumns(
|
||||||
|
ACT_TABLE_KEY,
|
||||||
|
ACT_COLUMNS.map((c) => c.label),
|
||||||
|
);
|
||||||
|
const actCols = useTableColumns(() => ACT_TABLE_KEY);
|
||||||
|
function colOn(key: ActColKey) {
|
||||||
|
const col = ACT_COLUMNS.find((c) => c.key === key)!;
|
||||||
|
return actCols.isVisible(col.label);
|
||||||
|
}
|
||||||
|
const actGridStyle = computed(() => ({
|
||||||
|
gridTemplateColumns: [
|
||||||
|
'2.4rem',
|
||||||
|
...ACT_COLUMNS.filter((c) => actCols.isVisible(c.label)).map((c) => c.width),
|
||||||
|
'2.4rem',
|
||||||
|
].join(' '),
|
||||||
|
}));
|
||||||
|
|
||||||
type TypeFilter = 'all' | ActivityType;
|
type TypeFilter = 'all' | ActivityType;
|
||||||
type StatusFilter = 'all' | ActivityStatus;
|
type StatusFilter = 'all' | ActivityStatus;
|
||||||
@@ -698,19 +726,20 @@ onUnmounted(() => {
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="list-head">
|
<div class="list-head">
|
||||||
<strong>{{ filterLabel }} — {{ toFaDigits(total.toLocaleString('en-US')) }} فعالیت</strong>
|
<strong>{{ filterLabel }} — {{ toFaDigits(total.toLocaleString('en-US')) }} فعالیت</strong>
|
||||||
<RouterLink class="cal-mini" :to="{ name: 'activities-calendar' }" aria-label="تقویم">
|
<div class="list-head-tools">
|
||||||
<UiIcon name="calendar" :size="16" />
|
<TableColumnsMenu class="desktop-only" :table-key="ACT_TABLE_KEY" align="end" />
|
||||||
</RouterLink>
|
<RouterLink class="cal-mini" :to="{ name: 'activities-calendar' }" aria-label="تقویم">
|
||||||
|
<UiIcon name="calendar" :size="16" />
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table desktop-only" role="table">
|
<div class="table desktop-only" role="table">
|
||||||
<div class="thead" role="row">
|
<div class="thead" role="row" :style="actGridStyle">
|
||||||
<span class="row-num">#</span>
|
<span class="row-num">#</span>
|
||||||
<span>مشتری / عنوان فعالیت</span>
|
<template v-for="col in ACT_COLUMNS" :key="col.key">
|
||||||
<span>نوع فعالیت</span>
|
<span v-if="colOn(col.key)">{{ col.label }}</span>
|
||||||
<span>زمان شروع</span>
|
</template>
|
||||||
<span>کارشناس</span>
|
|
||||||
<span>وضعیت</span>
|
|
||||||
<span />
|
<span />
|
||||||
</div>
|
</div>
|
||||||
<article
|
<article
|
||||||
@@ -718,9 +747,10 @@ onUnmounted(() => {
|
|||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="trow"
|
class="trow"
|
||||||
role="row"
|
role="row"
|
||||||
|
:style="actGridStyle"
|
||||||
>
|
>
|
||||||
<span class="row-num">{{ toFaDigits((page - 1) * pageSize + idx + 1) }}</span>
|
<span class="row-num">{{ toFaDigits((page - 1) * pageSize + idx + 1) }}</span>
|
||||||
<div class="title-cell">
|
<div v-if="colOn('title')" class="title-cell">
|
||||||
<UiIcon name="star" :size="16" class="star" />
|
<UiIcon name="star" :size="16" class="star" />
|
||||||
<div>
|
<div>
|
||||||
<RouterLink class="title" :to="{ name: 'activity-detail', params: { id: item.id } }">
|
<RouterLink class="title" :to="{ name: 'activity-detail', params: { id: item.id } }">
|
||||||
@@ -729,17 +759,17 @@ onUnmounted(() => {
|
|||||||
<p v-if="subtitle(item)" class="sub">{{ subtitle(item) }}</p>
|
<p v-if="subtitle(item)" class="sub">{{ subtitle(item) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="type-cell">
|
<div v-if="colOn('type')" class="type-cell">
|
||||||
<span class="type-pill" :class="item.type">
|
<span class="type-pill" :class="item.type">
|
||||||
<UiIcon :name="typeIcon[item.type]" :size="13" />
|
<UiIcon :name="typeIcon[item.type]" :size="13" />
|
||||||
{{ typeLabel[item.type] }}
|
{{ typeLabel[item.type] }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="when" :class="{ late: uiStatus(item) === 'overdue' }">
|
<div v-if="colOn('when')" class="when" :class="{ late: uiStatus(item) === 'overdue' }">
|
||||||
<strong>{{ clockText(item) }}</strong>
|
<strong>{{ clockText(item) }}</strong>
|
||||||
<span>{{ dayText(item) }}</span>
|
<span>{{ dayText(item) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="who">
|
<div v-if="colOn('who')" class="who">
|
||||||
<strong>{{
|
<strong>{{
|
||||||
item.assignees?.length
|
item.assignees?.length
|
||||||
? item.assignees.map((a) => a.fullName).join('، ')
|
? item.assignees.map((a) => a.fullName).join('، ')
|
||||||
@@ -750,7 +780,7 @@ onUnmounted(() => {
|
|||||||
{{ item.company?.name || item.person?.fullName }}
|
{{ item.company?.name || item.person?.fullName }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="colOn('status')">
|
||||||
<span class="status" :class="uiStatus(item)">
|
<span class="status" :class="uiStatus(item)">
|
||||||
<UiIcon :name="statusIcon[uiStatus(item)]" :size="13" />
|
<UiIcon :name="statusIcon[uiStatus(item)]" :size="13" />
|
||||||
{{ statusLabel[uiStatus(item)] }}
|
{{ statusLabel[uiStatus(item)] }}
|
||||||
@@ -1225,6 +1255,12 @@ h1 {
|
|||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-head-tools {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
|||||||
@@ -128,6 +128,11 @@ function pickPerson(p: PersonDto) {
|
|||||||
personOpen.value = false;
|
personOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearPerson() {
|
||||||
|
personId.value = '';
|
||||||
|
personOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
function openNewPerson() {
|
function openNewPerson() {
|
||||||
personOpen.value = false;
|
personOpen.value = false;
|
||||||
newPersonOpen.value = true;
|
newPersonOpen.value = true;
|
||||||
@@ -389,7 +394,6 @@ function validate() {
|
|||||||
if (title.value.trim().length < 2) return 'عنوان فعالیت را بنویسید.';
|
if (title.value.trim().length < 2) return 'عنوان فعالیت را بنویسید.';
|
||||||
if (!dueDate.value || !dueTime.value) return 'تاریخ و ساعت را مشخص کنید.';
|
if (!dueDate.value || !dueTime.value) return 'تاریخ و ساعت را مشخص کنید.';
|
||||||
if (!assigneeIds.value.length) return 'حداقل یک مسئول فعالیت را انتخاب کنید.';
|
if (!assigneeIds.value.length) return 'حداقل یک مسئول فعالیت را انتخاب کنید.';
|
||||||
if (isNew.value && !personId.value) return 'شخص یا مشتری مرتبط را انتخاب کنید.';
|
|
||||||
if (needsNote.value && !description.value.trim()) return 'توضیح این فعالیت را بنویسید.';
|
if (needsNote.value && !description.value.trim()) return 'توضیح این فعالیت را بنویسید.';
|
||||||
if (reminderAt.value && !reminderNotifyApp.value && !reminderNotifySms.value) {
|
if (reminderAt.value && !reminderNotifyApp.value && !reminderNotifySms.value) {
|
||||||
return 'برای یادآوری، اعلان یا پیامک را انتخاب کنید.';
|
return 'برای یادآوری، اعلان یا پیامک را انتخاب کنید.';
|
||||||
@@ -644,7 +648,7 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="label-row">
|
<div class="label-row">
|
||||||
<label>شخص / مشتری مرتبط <i>*</i></label>
|
<label>شخص / مشتری مرتبط <small class="optional">(اختیاری)</small></label>
|
||||||
<button type="button" class="person-add" @click.stop="openNewPerson">
|
<button type="button" class="person-add" @click.stop="openNewPerson">
|
||||||
<UiIcon name="user-plus" :size="14" />
|
<UiIcon name="user-plus" :size="14" />
|
||||||
افزودن شخص جدید
|
افزودن شخص جدید
|
||||||
@@ -655,7 +659,7 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
<UiIcon name="user" :size="16" />
|
<UiIcon name="user" :size="16" />
|
||||||
</span>
|
</span>
|
||||||
<span class="who">
|
<span class="who">
|
||||||
<b :class="{ ghost: !selectedPerson }">{{ selectedPerson?.fullName || 'انتخاب کنید' }}</b>
|
<b :class="{ ghost: !selectedPerson }">{{ selectedPerson?.fullName || 'بدون شخص — انتخاب کنید' }}</b>
|
||||||
<small v-if="personSub">{{ personSub }}</small>
|
<small v-if="personSub">{{ personSub }}</small>
|
||||||
</span>
|
</span>
|
||||||
<UiIcon name="chevron" :size="16" />
|
<UiIcon name="chevron" :size="16" />
|
||||||
@@ -672,6 +676,14 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<ul class="person-list">
|
<ul class="person-list">
|
||||||
|
<li v-if="personId">
|
||||||
|
<button type="button" class="person-row" @click="clearPerson">
|
||||||
|
<span class="row-text">
|
||||||
|
<b>بدون شخص</b>
|
||||||
|
<small>حذف شخص انتخابشده</small>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
<li v-for="p in personOptions" :key="p.id">
|
<li v-for="p in personOptions" :key="p.id">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1179,6 +1191,12 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label-row .optional {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.person-add {
|
.person-add {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -210,7 +210,10 @@ onMounted(() => {
|
|||||||
description="برای دسترسی خارجی یک کلید بسازید."
|
description="برای دسترسی خارجی یک کلید بسازید."
|
||||||
/>
|
/>
|
||||||
<div v-else class="table-wrap">
|
<div v-else class="table-wrap">
|
||||||
<table>
|
<div class="tc-row">
|
||||||
|
<TableColumnsMenu table-key="public-api.keys" align="end" />
|
||||||
|
</div>
|
||||||
|
<table v-table-cols="'public-api.keys'">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>نام</th>
|
<th>نام</th>
|
||||||
|
|||||||
Reference in New Issue
Block a user