Files
crm/apps/web/src/views/ContactsView.vue
T

1078 lines
25 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { RouterLink, useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { api } from '@/lib/api';
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
import CustomerMoodFace from '@/components/CustomerMoodFace.vue';
import UiIcon from '@/components/UiIcon.vue';
import type {
CompanyDto,
ImportContactsResult,
PaginatedResponse,
PersonDto,
PersonMood,
} from '@mento/shared';
import { PERSON_MOOD_LABELS, PERSON_MOODS } from '@/lib/person-mood';
type Tab = 'people' | 'companies';
type SortKey = 'name' | 'newest';
const PAGE_SIZE = 10;
const route = useRoute();
const router = useRouter();
const tab = computed<Tab>(() =>
route.query.tab === 'companies' ? 'companies' : 'people',
);
const page = computed(() => {
const raw = Number(route.query.page);
return Number.isFinite(raw) && raw > 0 ? raw : 1;
});
const sort = computed<SortKey>(() => (route.query.sort === 'newest' ? 'newest' : 'name'));
const moodFilter = computed<PersonMood | 'all'>(() => {
const value = typeof route.query.mood === 'string' ? route.query.mood : 'all';
return PERSON_MOODS.includes(value as PersonMood) ? (value as PersonMood) : 'all';
});
const q = ref(typeof route.query.q === 'string' ? route.query.q : '');
const loading = ref(false);
const busy = ref(false);
const error = ref('');
const importMsg = ref('');
const people = ref<PaginatedResponse<PersonDto> | null>(null);
const companies = ref<PaginatedResponse<CompanyDto> | null>(null);
const fileInput = ref<HTMLInputElement | null>(null);
const menuId = ref<string | null>(null);
const sortOpen = ref(false);
const filterOpen = ref(false);
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 pageFrom = computed(() => (total.value ? (page.value - 1) * PAGE_SIZE + 1 : 0));
const pageTo = computed(() => Math.min(page.value * PAGE_SIZE, total.value));
const pageList = computed(() => {
const count = pageCount.value;
const current = page.value;
const pages: number[] = [];
for (let i = 1; i <= count; i += 1) {
if (i === 1 || i === count || Math.abs(i - current) <= 2) pages.push(i);
else if (pages[pages.length - 1] !== -1) pages.push(-1);
}
return pages;
});
function moodOf(person: PersonDto): PersonMood {
return person.mood || 'newcomer';
}
function moodLabel(mood: PersonMood) {
return PERSON_MOOD_LABELS[mood];
}
function formatDate(value: string) {
try {
return formatJalaliNumeric(new Date(value));
} catch {
return value;
}
}
function closePopups() {
menuId.value = null;
sortOpen.value = false;
filterOpen.value = false;
}
async function load() {
loading.value = true;
error.value = '';
closePopups();
try {
if (tab.value === 'people') {
const { data } = await api.get<PaginatedResponse<PersonDto>>('/people', {
params: {
q: q.value || undefined,
page: page.value,
limit: PAGE_SIZE,
sort: sort.value,
mood: moodFilter.value === 'all' ? undefined : moodFilter.value,
},
});
people.value = data;
} else {
const { data } = await api.get<PaginatedResponse<CompanyDto>>('/companies', {
params: { q: q.value || undefined, page: page.value, limit: PAGE_SIZE },
});
companies.value = data;
}
} catch (e) {
if (axios.isAxiosError(e) && !e.response) {
error.value = 'اتصال به API برقرار نشد.';
} else {
error.value = 'بارگذاری مخاطبین ناموفق بود.';
}
} finally {
loading.value = false;
}
}
function patchQuery(next: Record<string, string | undefined>) {
const query = { ...route.query, ...next };
for (const key of Object.keys(query)) {
if (query[key] == null || query[key] === '') delete query[key];
}
void router.replace({ name: 'contacts', query });
}
function setTab(next: Tab) {
patchQuery({
tab: next === 'people' ? undefined : next,
page: undefined,
mood: next === 'people' ? (moodFilter.value === 'all' ? undefined : moodFilter.value) : undefined,
});
}
function onSearch() {
patchQuery({ q: q.value.trim() || undefined, page: undefined });
}
function setSort(next: SortKey) {
sortOpen.value = false;
patchQuery({ sort: next === 'name' ? undefined : next, page: undefined });
}
function setMood(next: PersonMood | 'all') {
filterOpen.value = false;
patchQuery({ mood: next === 'all' ? undefined : next, page: undefined });
}
function setPage(next: number) {
if (next < 1 || next > pageCount.value) return;
patchQuery({ page: next === 1 ? undefined : String(next) });
}
async function setMoodFor(person: PersonDto, mood: PersonMood) {
menuId.value = null;
try {
await api.patch(`/people/${person.id}`, { mood });
await load();
} catch {
error.value = 'تغییر وضعیت مشتری ناموفق بود.';
}
}
async function exportExcel() {
busy.value = true;
importMsg.value = '';
error.value = '';
try {
const path = tab.value === 'people' ? '/people/export' : '/companies/export';
const { data } = await api.get<Blob>(path, {
params: { q: q.value || undefined },
responseType: 'blob',
});
const url = URL.createObjectURL(data);
const a = document.createElement('a');
a.href = url;
a.download =
tab.value === 'people' ? 'mento-people.xlsx' : 'mento-companies.xlsx';
a.click();
URL.revokeObjectURL(url);
} catch {
error.value = 'خروجی اکسل ناموفق بود.';
} finally {
busy.value = false;
}
}
function pickImportFile() {
fileInput.value?.click();
}
async function onImportFile(ev: Event) {
const input = ev.target as HTMLInputElement;
const file = input.files?.[0];
input.value = '';
if (!file) return;
busy.value = true;
importMsg.value = '';
error.value = '';
try {
const form = new FormData();
form.append('file', file);
const path = tab.value === 'people' ? '/people/import' : '/companies/import';
const { data } = await api.post<ImportContactsResult>(path, form, {
headers: { 'Content-Type': undefined },
});
importMsg.value = `وارد شد: ${data.created} · تکراری رد شد: ${data.skipped}${
data.errors.length ? ` · خطا: ${data.errors.length}` : ''
}`;
if (data.errors.length) {
const sample = data.errors
.slice(0, 3)
.map((e) => `ردیف ${e.row}: ${e.message}`)
.join(' | ');
error.value = sample;
}
await load();
} catch (e) {
if (axios.isAxiosError(e)) {
const msg = (e.response?.data as { message?: string | string[] })?.message;
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'ایمپورت ناموفق بود');
} else {
error.value = 'ایمپورت ناموفق بود';
}
} finally {
busy.value = false;
}
}
watch(
() => [route.query.tab, route.query.q, route.query.page, route.query.sort, route.query.mood],
() => {
q.value = typeof route.query.q === 'string' ? route.query.q : '';
void load();
},
);
onMounted(load);
</script>
<template>
<section class="page" @click="closePopups">
<header class="page-head">
<div class="head-copy">
<h1>مخاطبین</h1>
<p class="lead">مدیریت و مشاهده اشخاص و شرکت‌ها</p>
</div>
<div class="actions">
<button class="btn btn-ghost excel-btn" type="button" :disabled="busy" @click.stop="exportExcel">
<span class="btn-full">خروجی اکسل</span>
<span class="btn-short">خروجی</span>
</button>
<button class="btn btn-ghost excel-btn" type="button" :disabled="busy" @click.stop="pickImportFile">
<span class="btn-full">ورود از اکسل</span>
<span class="btn-short">ورود</span>
</button>
<input
ref="fileInput"
type="file"
accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
hidden
@change="onImportFile"
/>
<RouterLink
class="add-btn"
:to="tab === 'companies' ? { name: 'company-new' } : { name: 'person-new' }"
:title="tab === 'companies' ? 'شرکت جدید' : 'شخص جدید'"
>
<UiIcon name="plus" :size="22" />
</RouterLink>
</div>
</header>
<nav class="tabs">
<button type="button" :class="{ on: tab === 'people' }" @click.stop="setTab('people')">
<UiIcon name="user" :size="16" />
اشخاص
<em v-if="people">{{ toFaDigits(people.total) }}</em>
</button>
<button type="button" :class="{ on: tab === 'companies' }" @click.stop="setTab('companies')">
<UiIcon name="building" :size="16" />
شرکت‌ها
<em v-if="companies">{{ toFaDigits(companies.total) }}</em>
</button>
</nav>
<div class="toolbar">
<form class="search" @submit.prevent="onSearch">
<UiIcon name="search" :size="16" />
<input
v-model="q"
type="search"
:placeholder="tab === 'people' ? 'جستجو در اشخاص…' : 'جستجو در شرکت‌ها…'"
/>
</form>
<div class="toolbar-tools">
<div class="pop">
<button class="tool" type="button" @click.stop="sortOpen = !sortOpen; filterOpen = false">
<UiIcon name="trend" :size="16" />
<span class="tool-label">مرتب‌سازی</span>
</button>
<ul v-if="sortOpen" class="menu">
<li><button type="button" :class="{ on: sort === 'name' }" @click.stop="setSort('name')">نام</button></li>
<li><button type="button" :class="{ on: sort === 'newest' }" @click.stop="setSort('newest')">جدیدترین</button></li>
</ul>
</div>
<div v-if="tab === 'people'" class="pop">
<button class="tool" type="button" @click.stop="filterOpen = !filterOpen; sortOpen = false">
<UiIcon name="filter" :size="16" />
<span class="tool-label">فیلتر</span>
</button>
<ul v-if="filterOpen" class="menu">
<li>
<button type="button" :class="{ on: moodFilter === 'all' }" @click.stop="setMood('all')">همه</button>
</li>
<li v-for="mood in PERSON_MOODS" :key="mood">
<button type="button" :class="{ on: moodFilter === mood }" @click.stop="setMood(mood)">
{{ moodLabel(mood) }}
</button>
</li>
</ul>
</div>
</div>
</div>
<p v-if="importMsg" class="ok">{{ importMsg }}</p>
<p v-if="error" class="error">{{ error }}</p>
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
<template v-else-if="tab === 'people'">
<div v-if="people?.items.length" class="cards">
<article v-for="p in people.items" :key="`m-${p.id}`" class="m-card">
<CustomerMoodFace :mood="moodOf(p)" :size="48" />
<div class="m-body">
<div class="m-top">
<RouterLink class="name" :to="{ name: 'person-detail', params: { id: p.id } }">
{{ p.fullName }}
</RouterLink>
<button class="more" type="button" @click.stop="menuId = menuId === p.id ? null : p.id">
<UiIcon name="dots-v" :size="16" />
</button>
</div>
<p class="role">
{{ [p.position, p.company?.name].filter(Boolean).join(' • ') || 'بدون سمت' }}
</p>
<p v-if="p.mobile || p.phone" class="line" dir="ltr">
<UiIcon name="phone" :size="13" />
<span>{{ p.mobile || p.phone }}</span>
</p>
<p v-if="p.email" class="line" dir="ltr">
<UiIcon name="mail" :size="13" />
<span>{{ p.email }}</span>
</p>
<span class="badge" :class="moodOf(p)">{{ moodLabel(moodOf(p)) }}</span>
</div>
<ul v-if="menuId === p.id" class="menu row-menu">
<li>
<RouterLink :to="{ name: 'person-detail', params: { id: p.id } }">پرونده</RouterLink>
</li>
<li>
<RouterLink :to="{ name: 'person-edit', params: { id: p.id } }">ویرایش</RouterLink>
</li>
<li v-for="mood in PERSON_MOODS" :key="mood">
<button type="button" @click.stop="setMoodFor(p, mood)">{{ moodLabel(mood) }}</button>
</li>
</ul>
</article>
</div>
<div v-if="people?.items.length" class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th>وضعیت</th>
<th>نام شخص</th>
<th>سمت</th>
<th>شرکت</th>
<th>تلفن</th>
<th>ایمیل</th>
<th>تاریخ ثبت</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="p in people.items" :key="p.id">
<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>
</td>
<td>
<RouterLink class="name" :to="{ name: 'person-detail', params: { id: p.id } }">
{{ p.fullName }}
</RouterLink>
</td>
<td>{{ p.position || '—' }}</td>
<td>
<RouterLink
v-if="p.company"
:to="{ name: 'company-detail', params: { id: p.company.id } }"
>
{{ p.company.name }}
</RouterLink>
<span v-else class="muted">—</span>
</td>
<td>{{ p.mobile || p.phone || '—' }}</td>
<td>
<span class="mail">
{{ p.email || '—' }}
<UiIcon v-if="p.email" name="mail" :size="14" />
</span>
</td>
<td>{{ formatDate(p.createdAt) }}</td>
<td class="ops">
<button type="button" class="more" @click.stop="menuId = menuId === p.id ? null : p.id">
<UiIcon name="dots-v" :size="16" />
</button>
<ul v-if="menuId === p.id" class="menu row-menu">
<li>
<RouterLink :to="{ name: 'person-detail', params: { id: p.id } }">پرونده</RouterLink>
</li>
<li>
<RouterLink :to="{ name: 'person-edit', params: { id: p.id } }">ویرایش</RouterLink>
</li>
<li v-for="mood in PERSON_MOODS" :key="mood">
<button type="button" @click.stop="setMoodFor(p, mood)">{{ moodLabel(mood) }}</button>
</li>
</ul>
</td>
</tr>
</tbody>
</table>
</div>
<p v-else class="empty">شخصی ثبت نشده. اولین نفر را اضافه کنید.</p>
</template>
<template v-else>
<div v-if="companies?.items.length" class="cards">
<article v-for="c in companies.items" :key="`mc-${c.id}`" class="m-card">
<span class="co-ico"><UiIcon name="building" :size="22" /></span>
<div class="m-body">
<div class="m-top">
<RouterLink class="name" :to="{ name: 'company-detail', params: { id: c.id } }">
{{ c.name }}
</RouterLink>
</div>
<p v-if="c.phone" class="line" dir="ltr">
<UiIcon name="phone" :size="13" />
<span>{{ c.phone }}</span>
</p>
<p v-if="c.email" class="line" dir="ltr">
<UiIcon name="mail" :size="13" />
<span>{{ c.email }}</span>
</p>
<p class="role">افراد: {{ toFaDigits(c.peopleCount) }}</p>
</div>
</article>
</div>
<div v-if="companies?.items.length" class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th>نام شرکت</th>
<th>تلفن</th>
<th>ایمیل</th>
<th>افراد</th>
</tr>
</thead>
<tbody>
<tr v-for="c in companies.items" :key="c.id">
<td>
<RouterLink class="name" :to="{ name: 'company-detail', params: { id: c.id } }">
{{ c.name }}
</RouterLink>
</td>
<td>{{ c.phone || '—' }}</td>
<td>{{ c.email || '—' }}</td>
<td>{{ toFaDigits(c.peopleCount) }}</td>
</tr>
</tbody>
</table>
</div>
<p v-else class="empty">شرکتی ثبت نشده. اولین شرکت را اضافه کنید.</p>
</template>
<footer v-if="total" class="pager">
<span>
نمایش {{ toFaDigits(pageFrom) }} تا {{ toFaDigits(pageTo) }} از {{ toFaDigits(total) }} مورد
</span>
<div class="pages">
<button type="button" :disabled="page <= 1" @click="setPage(page - 1)">‹</button>
<template v-for="(n, idx) in pageList" :key="`${n}-${idx}`">
<span v-if="n < 0">…</span>
<button v-else type="button" :class="{ on: n === page }" @click="setPage(n)">
{{ toFaDigits(n) }}
</button>
</template>
<button type="button" :disabled="page >= pageCount" @click="setPage(page + 1)">›</button>
</div>
</footer>
</section>
</template>
<style scoped>
.page {
display: grid;
gap: 1rem;
width: 100%;
min-width: 0;
max-width: 100%;
overflow-x: clip;
box-sizing: border-box;
}
.page > * {
min-width: 0;
max-width: 100%;
}
.page-head {
display: flex;
justify-content: space-between;
gap: 0.75rem 1rem;
align-items: flex-start;
flex-wrap: wrap;
}
.head-copy {
min-width: 0;
flex: 1 1 12rem;
}
h1 {
margin: 0;
font-size: clamp(1.3rem, 4vw, 1.7rem);
}
.lead,
.muted,
.role,
.line {
color: #64748b;
overflow-wrap: anywhere;
word-break: break-word;
}
.lead {
margin: 0.25rem 0 0;
}
.actions {
display: flex;
gap: 0.45rem;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
flex: 0 1 auto;
min-width: 0;
}
.btn-short {
display: none;
}
.add-btn {
width: 46px;
height: 46px;
border-radius: 50%;
background: #14b8a6;
color: #fff;
display: grid;
place-items: center;
flex-shrink: 0;
box-shadow: 0 8px 18px rgba(13, 148, 136, 0.28);
}
.tabs {
display: flex;
gap: 0.35rem 1.2rem;
border-bottom: 1px solid #e2e8f0;
overflow-x: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
}
.tabs::-webkit-scrollbar {
display: none;
}
.tabs button {
border: 0;
background: none;
padding: 0.55rem 0.1rem;
font: inherit;
color: #94a3b8;
display: inline-flex;
align-items: center;
gap: 0.35rem;
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
}
.tabs .on {
color: #0f766e;
font-weight: 800;
box-shadow: inset 0 -2px 0 #14b8a6;
}
.tabs em {
font-style: normal;
font-size: 0.75rem;
color: #94a3b8;
}
.toolbar {
display: flex;
gap: 0.55rem;
align-items: center;
flex-wrap: wrap;
}
.toolbar-tools {
display: flex;
gap: 0.45rem;
align-items: center;
flex-shrink: 0;
}
.search {
flex: 1 1 12rem;
min-width: 0;
display: flex;
align-items: center;
gap: 0.45rem;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 0 0.75rem;
min-height: 44px;
color: #94a3b8;
}
.search input {
flex: 1;
min-width: 0;
border: 0;
background: transparent;
font: inherit;
color: #0f172a;
}
.tool {
min-height: 44px;
border: 1px solid #e2e8f0;
background: #fff;
border-radius: 12px;
padding: 0 0.75rem;
display: inline-flex;
align-items: center;
gap: 0.35rem;
font: inherit;
color: #475569;
cursor: pointer;
white-space: nowrap;
}
.pop {
position: relative;
}
.menu {
position: absolute;
inset-inline-start: 0;
top: calc(100% + 6px);
z-index: 8;
margin: 0;
padding: 0.35rem;
list-style: none;
background: #fff;
border-radius: 12px;
box-shadow: 0 14px 36px rgba(15, 23, 42, 0.14);
min-width: min(12rem, calc(100vw - 2rem));
max-width: calc(100vw - 1.5rem);
}
.menu button,
.menu a {
width: 100%;
display: block;
border: 0;
background: none;
padding: 0.5rem 0.65rem;
border-radius: 8px;
font: inherit;
text-align: start;
color: inherit;
cursor: pointer;
}
.menu .on,
.menu button:hover,
.menu a:hover {
background: #f0fdfa;
color: #0f766e;
}
.ok {
margin: 0;
color: #0f766e;
}
.error {
margin: 0;
color: #b91c1c;
}
.table-wrap {
background: #fff;
border-radius: 18px;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.data-table {
width: 100%;
border-collapse: collapse;
min-width: 640px;
}
th,
td {
text-align: start;
padding: 0.85rem 1rem;
border-bottom: 1px solid #f1f5f9;
white-space: nowrap;
}
th {
font-size: 0.8rem;
color: #94a3b8;
background: #f8fafc;
}
.status {
display: flex;
align-items: center;
gap: 0.55rem;
}
.status strong {
font-size: 0.82rem;
}
.name {
color: #0f766e;
font-weight: 800;
min-width: 0;
overflow-wrap: anywhere;
}
.mail {
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
.ops {
position: relative;
}
.more {
border: 0;
background: none;
color: #94a3b8;
cursor: pointer;
display: grid;
place-items: center;
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: 8px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
flex-shrink: 0;
}
.dot.happy,
strong.happy {
color: #7c3aed;
}
.dot.happy {
background: #8b5cf6;
}
.dot.unhappy,
strong.unhappy {
color: #e11d48;
}
.dot.unhappy {
background: #fb7185;
}
.dot.special,
strong.special {
color: #d97706;
}
.dot.special {
background: #f59e0b;
}
.dot.newcomer,
strong.newcomer {
color: #0f766e;
}
.cards {
display: none;
gap: 0.7rem;
}
.m-card {
position: relative;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 0.7rem;
align-items: start;
background: #fff;
border-radius: 18px;
padding: 0.85rem;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
min-width: 0;
overflow: hidden;
}
.m-top {
display: flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
}
.m-top .name {
flex: 1 1 auto;
min-width: 0;
line-height: 1.35;
}
.m-body {
min-width: 0;
display: grid;
gap: 0.28rem;
}
.role,
.line {
margin: 0;
font-size: 0.8rem;
display: flex;
align-items: flex-start;
gap: 0.3rem;
min-width: 0;
}
.line :deep(svg),
.role :deep(svg) {
flex-shrink: 0;
margin-top: 0.15rem;
}
.line span {
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.badge {
justify-self: start;
border-radius: 999px;
padding: 0.28rem 0.6rem;
font-size: 0.72rem;
font-weight: 800;
margin-top: 0.25rem;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.badge.newcomer {
background: #ecfdf5;
color: #0f766e;
}
.badge.happy {
background: #f3e8ff;
color: #7c3aed;
}
.badge.unhappy {
background: #ffe4e6;
color: #e11d48;
}
.badge.special {
background: #fef3c7;
color: #d97706;
}
.co-ico {
width: 48px;
height: 48px;
border-radius: 50%;
display: grid;
place-items: center;
background: #ecfdf5;
color: #0f766e;
flex-shrink: 0;
}
.row-menu {
inset-inline-start: auto;
inset-inline-end: 0;
top: 2.4rem;
}
.empty {
margin: 0;
padding: 1.4rem;
color: #64748b;
background: #fff;
border-radius: 16px;
}
.pager {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.8rem;
flex-wrap: wrap;
color: #64748b;
font-size: 0.8rem;
}
.pages {
display: flex;
gap: 0.3rem;
align-items: center;
flex-wrap: wrap;
}
.pages button {
min-width: 32px;
height: 32px;
border: 0;
background: #fff;
border-radius: 8px;
cursor: pointer;
}
.pages button.on {
background: #14b8a6;
color: #fff;
}
.pages button:disabled {
opacity: 0.4;
}
@media (max-width: 900px) {
.page {
gap: 0.85rem;
}
.page-head {
align-items: center;
}
.actions .excel-btn .btn-full {
display: none;
}
.actions .excel-btn .btn-short {
display: inline;
}
.actions .excel-btn {
padding-inline: 0.7rem;
font-size: 0.82rem;
}
.add-btn {
width: 40px;
height: 40px;
}
.toolbar {
flex-direction: column;
align-items: stretch;
}
.search {
flex: 0 0 auto;
width: 100%;
}
.toolbar-tools {
width: 100%;
}
.toolbar-tools .pop {
flex: 1 1 auto;
}
.toolbar-tools .tool {
width: 100%;
justify-content: center;
}
.table-wrap {
display: none;
}
.cards {
display: grid;
}
.pager {
flex-direction: column;
align-items: stretch;
text-align: center;
gap: 0.65rem;
}
.pages {
justify-content: center;
}
}
@media (max-width: 520px) {
.actions .excel-btn {
display: none;
}
.tool-label {
display: none;
}
.toolbar-tools .tool {
width: auto;
flex: 1 1 auto;
min-width: 44px;
justify-content: center;
padding-inline: 0.65rem;
}
.m-card {
padding: 0.75rem;
gap: 0.6rem;
}
}
</style>