Add row selection, bulk delete, row numbers and page size picker to tables; redesign person profile header
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -36,11 +36,12 @@ import {
|
||||
PERSON_IMPORT_SOURCES,
|
||||
} from '@mento/shared';
|
||||
import { PERSON_MOOD_LABELS, PERSON_MOODS } from '@/lib/person-mood';
|
||||
import { PAGE_SIZE_OPTIONS, usePageSize } from '@/lib/pageSize';
|
||||
|
||||
type Tab = 'people' | 'companies';
|
||||
type SortKey = 'name' | 'newest';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
const pageSize = usePageSize('contacts', 10);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -81,7 +82,53 @@ const contactModal = ref<'person' | 'company' | null>(null);
|
||||
const total = computed(() =>
|
||||
tab.value === 'people' ? people.value?.total ?? 0 : companies.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 / pageSize.value)));
|
||||
const rowOffset = computed(() => (page.value - 1) * pageSize.value);
|
||||
|
||||
const selectedIds = ref<string[]>([]);
|
||||
const pageIds = computed(() => people.value?.items.map((p) => p.id) ?? []);
|
||||
const allSelected = computed(
|
||||
() => pageIds.value.length > 0 && pageIds.value.every((id) => selectedIds.value.includes(id)),
|
||||
);
|
||||
const someSelected = computed(() => selectedIds.value.length > 0 && !allSelected.value);
|
||||
|
||||
function toggleAll() {
|
||||
selectedIds.value = allSelected.value ? [] : [...pageIds.value];
|
||||
}
|
||||
|
||||
function toggleOne(id: string) {
|
||||
selectedIds.value = selectedIds.value.includes(id)
|
||||
? selectedIds.value.filter((x) => x !== id)
|
||||
: [...selectedIds.value, id];
|
||||
}
|
||||
|
||||
async function deleteSelected() {
|
||||
const ids = [...selectedIds.value];
|
||||
if (!ids.length) return;
|
||||
if (!confirm(`${toFaDigits(ids.length)} شخص انتخابشده حذف شوند؟ این کار قابل بازگشت نیست.`)) {
|
||||
return;
|
||||
}
|
||||
busy.value = true;
|
||||
pageBusy.value = true;
|
||||
busyMessage.value = 'در حال حذف اشخاص…';
|
||||
try {
|
||||
const { data } = await api.post<{ deleted: number }>('/people/bulk-delete', { ids });
|
||||
importMsg.value = `${toFaDigits(data.deleted)} شخص حذف شد.`;
|
||||
selectedIds.value = [];
|
||||
if (data.deleted >= pageIds.value.length && page.value > 1) setPage(page.value - 1);
|
||||
else await load();
|
||||
} catch {
|
||||
error.value = 'حذف اشخاص ناموفق بود.';
|
||||
} finally {
|
||||
busy.value = false;
|
||||
pageBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onPageSize(size: number) {
|
||||
pageSize.value = size;
|
||||
if (page.value === 1) void load();
|
||||
}
|
||||
|
||||
const peopleSavedFilters = computed(() => ({
|
||||
q: q.value.trim() || undefined,
|
||||
@@ -171,7 +218,7 @@ async function load() {
|
||||
params: {
|
||||
q: q.value || undefined,
|
||||
page: page.value,
|
||||
limit: PAGE_SIZE,
|
||||
limit: pageSize.value,
|
||||
sort: sort.value,
|
||||
mood: moodFilter.value === 'all' ? undefined : moodFilter.value,
|
||||
importSource:
|
||||
@@ -184,9 +231,11 @@ async function load() {
|
||||
]);
|
||||
people.value = peopleRes.data;
|
||||
sourceStats.value = statsRes.data;
|
||||
const visible = new Set(peopleRes.data.items.map((p) => p.id));
|
||||
selectedIds.value = selectedIds.value.filter((id) => visible.has(id));
|
||||
} else {
|
||||
const { data } = await api.get<PaginatedResponse<CompanyDto>>('/companies', {
|
||||
params: { q: q.value || undefined, page: page.value, limit: PAGE_SIZE },
|
||||
params: { q: q.value || undefined, page: page.value, limit: pageSize.value },
|
||||
});
|
||||
companies.value = data;
|
||||
}
|
||||
@@ -547,10 +596,29 @@ onMounted(load);
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedIds.length" class="bulk-bar">
|
||||
<span>{{ toFaDigits(selectedIds.length) }} شخص انتخاب شده</span>
|
||||
<button type="button" class="bulk-clear" @click="selectedIds = []">لغو انتخاب</button>
|
||||
<UiButton variant="danger" size="sm" :disabled="busy" @click="deleteSelected">
|
||||
<UiIcon name="trash" :size="15" />
|
||||
حذف انتخابشدهها
|
||||
</UiButton>
|
||||
</div>
|
||||
|
||||
<div v-if="people?.items.length" class="table-wrap">
|
||||
<table v-table-cols="'contacts.people'" class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="check-col">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="allSelected"
|
||||
:indeterminate="someSelected"
|
||||
aria-label="انتخاب همه"
|
||||
@change="toggleAll"
|
||||
/>
|
||||
</th>
|
||||
<th class="num-col">#</th>
|
||||
<th>وضعیت</th>
|
||||
<th>نام شخص</th>
|
||||
<th>منبع</th>
|
||||
@@ -564,12 +632,19 @@ onMounted(load);
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="p in people.items" :key="p.id">
|
||||
<tr v-for="(p, idx) in people.items" :key="p.id" :class="{ picked: selectedIds.includes(p.id) }">
|
||||
<td class="check-col">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedIds.includes(p.id)"
|
||||
:aria-label="`انتخاب ${p.fullName}`"
|
||||
@change="toggleOne(p.id)"
|
||||
/>
|
||||
</td>
|
||||
<td class="num-col">{{ toFaDigits(rowOffset + idx + 1) }}</td>
|
||||
<td>
|
||||
<div class="status">
|
||||
<CustomerMoodFace :mood="moodOf(p)" :size="40" />
|
||||
<strong :class="moodOf(p)">{{ moodLabel(moodOf(p)) }}</strong>
|
||||
<i class="dot" :class="moodOf(p)" />
|
||||
<div class="status" :title="moodLabel(moodOf(p))">
|
||||
<CustomerMoodFace :mood="moodOf(p)" :size="34" />
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@@ -660,6 +735,7 @@ onMounted(load);
|
||||
<table v-table-cols="'contacts.companies'" class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="num-col">#</th>
|
||||
<th>نام شرکت</th>
|
||||
<th>تلفن</th>
|
||||
<th>ایمیل</th>
|
||||
@@ -667,7 +743,8 @@ onMounted(load);
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="c in companies.items" :key="c.id">
|
||||
<tr v-for="(c, idx) in companies.items" :key="c.id">
|
||||
<td class="num-col">{{ toFaDigits(rowOffset + idx + 1) }}</td>
|
||||
<td>
|
||||
<RouterLink class="name" :to="{ name: 'company-detail', params: { id: c.id } }">
|
||||
{{ c.name }}
|
||||
@@ -692,7 +769,10 @@ onMounted(load);
|
||||
:page="page"
|
||||
:total-pages="pageCount"
|
||||
:total-items="total"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="PAGE_SIZE_OPTIONS"
|
||||
@update:page="setPage"
|
||||
@update:page-size="onPageSize"
|
||||
/>
|
||||
|
||||
<UiModal
|
||||
@@ -1020,6 +1100,61 @@ th {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.check-col {
|
||||
width: 2.4rem;
|
||||
padding-inline: 0.75rem 0.25rem;
|
||||
}
|
||||
|
||||
.check-col input {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 0;
|
||||
accent-color: #0d9488;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.num-col {
|
||||
width: 2.6rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.8rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
padding-inline: 0.25rem;
|
||||
}
|
||||
|
||||
tr.picked td {
|
||||
background: #f0fdfa;
|
||||
}
|
||||
|
||||
.bulk-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.6rem 0.85rem;
|
||||
margin-bottom: 0.6rem;
|
||||
border: 1px solid #99f6e4;
|
||||
background: #f0fdfa;
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--fs-sm);
|
||||
color: #0f766e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bulk-bar > :last-child {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.bulk-clear {
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
font-size: var(--fs-xs);
|
||||
color: #64748b;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: #0f766e;
|
||||
font-weight: 600;
|
||||
|
||||
Reference in New Issue
Block a user