diff --git a/apps/web/src/views/LeadsView.vue b/apps/web/src/views/LeadsView.vue index 203d439..6847b2c 100644 --- a/apps/web/src/views/LeadsView.vue +++ b/apps/web/src/views/LeadsView.vue @@ -3,28 +3,34 @@ import { computed, onBeforeUnmount, 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 { + formatRelativeContact, + toFaDigits, +} from '@/lib/jalali'; import { useAuthStore } from '@/stores/auth'; import { canManageSettings } from '@/lib/roles'; import { UiButton, UiEmptyState, + UiModal, UiPageHeader, - UiPagination, } from '@/components/ui'; import UiIcon from '@/components/UiIcon.vue'; import type { LeadDto, LeadImportJobDto, LeadStageDto, + LeadStatsDto, OrgUserDto, PaginatedResponse, } from '@mento/shared'; import { brand } from '@mento/shared'; -const PAGE_SIZE = 20; +const PAGE_SIZE = 10; type SortKey = 'updated' | 'newest' | 'follow_up' | 'last_contact'; +type StageTint = 'mint' | 'amber' | 'peach' | 'sky' | 'violet' | 'rose' | 'teal'; +type RowIcon = 'building' | 'bag' | 'briefcase' | 'users' | 'target'; const route = useRoute(); const router = useRouter(); @@ -38,16 +44,33 @@ const error = ref(''); const okMsg = ref(''); const data = ref | null>(null); const stages = ref([]); +const stats = ref(null); const users = ref([]); +const cityOptions = ref([]); +const sourceOptions = ref([]); const selected = ref>(new Set()); const sortOpen = ref(false); -const filterOpen = ref(false); +const moreOpen = ref(false); +const rowMenuId = ref(null); const assignOpen = ref(false); const stageOpen = ref(false); const assignUserId = ref(''); const bulkStageId = ref(''); const fileInput = ref(null); const importJob = ref(null); +const createOpen = ref(false); +const createBusy = ref(false); +const createError = ref(''); +const createForm = ref({ + title: '', + contactName: '', + mobile: '', + city: '', + region: '', + source: '', + stageId: '', + assigneeId: '', +}); let importTimer: ReturnType | null = null; const page = computed(() => { @@ -65,12 +88,24 @@ const stageId = computed(() => const assigneeId = computed(() => typeof route.query.assigneeId === 'string' ? route.query.assigneeId : '', ); +const cityFilter = computed(() => + typeof route.query.city === 'string' ? route.query.city : '', +); +const sourceFilter = computed(() => + typeof route.query.source === 'string' ? route.query.source : '', +); const unassigned = computed( () => route.query.unassigned === '1' || route.query.unassigned === 'true', ); const total = computed(() => data.value?.total ?? 0); const pageCount = computed(() => Math.max(1, Math.ceil(total.value / PAGE_SIZE))); +const rangeFrom = computed(() => + total.value === 0 ? 0 : (page.value - 1) * PAGE_SIZE + 1, +); +const rangeTo = computed(() => + Math.min(page.value * PAGE_SIZE, total.value), +); const allSelected = computed( () => Boolean(data.value?.items.length) && @@ -79,24 +114,116 @@ const allSelected = computed( const selectedIds = computed(() => [...selected.value]); const sortLabels: Record = { + newest: 'تاریخ جدیدترین', updated: 'آخرین به‌روزرسانی', - newest: 'جدیدترین', follow_up: 'پیگیری بعدی', last_contact: 'آخرین تماس', }; -function formatDate(value: string | null) { - if (!value) return '—'; - try { - return formatJalaliNumeric(new Date(value)); - } catch { - return value; +const stageCards = computed(() => { + const board = stages.value.filter( + (s) => s.showOnBoard && !s.isConverted && !s.isLost && !s.isUnqualified, + ); + return board.slice(0, 5); +}); + +const pageNumbers = computed(() => { + const totalPages = pageCount.value; + const current = page.value; + if (totalPages <= 5) { + return Array.from({ length: totalPages }, (_, i) => i + 1); } + let start = Math.max(1, current - 2); + let end = start + 4; + if (end > totalPages) { + end = totalPages; + start = Math.max(1, end - 4); + } + return Array.from({ length: end - start + 1 }, (_, i) => start + i); +}); + +function stageTint(color: string | null | undefined): StageTint { + const c = (color || '').toLowerCase(); + if (c === 'amber' || c === 'yellow' || c === 'orange') return 'amber'; + if (c === 'peach' || c === 'orange') return 'peach'; + if (c === 'sky' || c === 'blue') return 'sky'; + if (c === 'violet' || c === 'purple' || c === 'lavender') return 'violet'; + if (c === 'rose' || c === 'red' || c === 'pink') return 'rose'; + if (c === 'teal' || c === 'cyan') return 'teal'; + if (c === 'mint' || c === 'green') return 'mint'; + if (c.startsWith('#')) { + const hex = c.slice(1); + const n = parseInt(hex.length === 3 ? hex.replace(/(.)/g, '$1$1') : hex, 16); + if (!Number.isFinite(n)) return 'mint'; + const r = (n >> 16) & 255; + const g = (n >> 8) & 255; + const b = n & 255; + if (r > g && r > b) return 'rose'; + if (b > r && b > g) return 'sky'; + if (g > r && r > b) return 'amber'; + return 'mint'; + } + return 'mint'; +} + +function stageTintForLead(lead: LeadDto): StageTint { + const stage = stages.value.find((s) => s.id === lead.stageId); + return stageTint(stage?.color); +} + +function rowIcon(lead: LeadDto): RowIcon { + const key = `${lead.trade || ''}${lead.source || ''}${lead.title}`; + let hash = 0; + for (let i = 0; i < key.length; i++) hash = (hash + key.charCodeAt(i) * (i + 1)) % 5; + return (['building', 'bag', 'briefcase', 'users', 'target'] as const)[hash]!; +} + +function initials(name: string | null | undefined) { + if (!name?.trim()) return '؟'; + const parts = name.trim().split(/\s+/).filter(Boolean); + if (parts.length === 1) return parts[0]!.slice(0, 1); + return `${parts[0]!.slice(0, 1)}${parts[parts.length - 1]!.slice(0, 1)}`; +} + +function formatPhone(raw: string | null | undefined) { + if (!raw) return '—'; + const digits = raw.replace(/\D/g, ''); + if (digits.length === 11 && digits.startsWith('09')) { + return toFaDigits(`${digits.slice(0, 4)} ${digits.slice(4, 7)} ${digits.slice(7)}`); + } + return toFaDigits(raw); +} + +function formatCount(n: number) { + return toFaDigits(n.toLocaleString('en-US')); +} + +function lastActivity(lead: LeadDto) { + if (lead.lastContactAt) { + return { + label: 'تماس تلفنی', + when: formatRelativeContact(lead.lastContactAt), + icon: 'phone' as const, + }; + } + if (lead.nextFollowUpAt) { + return { + label: 'پیگیری بعدی', + when: formatRelativeContact(lead.nextFollowUpAt), + icon: 'calendar' as const, + }; + } + return { + label: 'ثبت لید', + when: formatRelativeContact(lead.createdAt), + icon: 'document' as const, + }; } function closePopups() { sortOpen.value = false; - filterOpen.value = false; + moreOpen.value = false; + rowMenuId.value = null; assignOpen.value = false; stageOpen.value = false; } @@ -119,26 +246,15 @@ function setSort(next: SortKey) { } function setStageFilter(id: string) { - filterOpen.value = false; patchQuery({ stageId: id || undefined, page: undefined }); } -function setAssigneeFilter(id: string) { - filterOpen.value = false; - patchQuery({ - assigneeId: id || undefined, - unassigned: undefined, - page: undefined, - }); +function setCityFilter(value: string) { + patchQuery({ city: value || undefined, page: undefined }); } -function setUnassigned(on: boolean) { - filterOpen.value = false; - patchQuery({ - unassigned: on ? '1' : undefined, - assigneeId: undefined, - page: undefined, - }); +function setSourceFilter(value: string) { + patchQuery({ source: value || undefined, page: undefined }); } function setPage(next: number) { @@ -161,13 +277,29 @@ function toggleAll(checked: boolean) { selected.value = new Set(data.value?.items.map((l) => l.id) ?? []); } +function mergeFacets(items: LeadDto[]) { + const cities = new Set(cityOptions.value); + const sources = new Set(sourceOptions.value); + for (const lead of items) { + if (lead.city?.trim()) cities.add(lead.city.trim()); + if (lead.source?.trim()) sources.add(lead.source.trim()); + } + cityOptions.value = [...cities].sort((a, b) => a.localeCompare(b, 'fa')); + sourceOptions.value = [...sources].sort((a, b) => a.localeCompare(b, 'fa')); +} + async function loadMeta() { - const [stagesRes, usersRes] = await Promise.all([ + const [stagesRes, usersRes, statsRes] = await Promise.all([ api.get('/leads/stages'), api.get('/users').catch(() => ({ data: [] as OrgUserDto[] })), + api.get('/leads/stats').catch(() => ({ data: null })), ]); stages.value = stagesRes.data; users.value = usersRes.data.filter((u) => u.isActive !== false); + stats.value = statsRes.data; + if (!createForm.value.stageId && stages.value[0]) { + createForm.value.stageId = stages.value[0].id; + } } async function load() { @@ -183,13 +315,22 @@ async function load() { sort: sort.value === 'updated' ? undefined : sort.value, stageId: stageId.value || undefined, assigneeId: assigneeId.value || undefined, + city: cityFilter.value || undefined, + source: sourceFilter.value || undefined, unassigned: unassigned.value ? '1' : undefined, }, }); data.value = res; + mergeFacets(res.items); selected.value = new Set( [...selected.value].filter((id) => res.items.some((l) => l.id === id)), ); + const { data: nextStats } = await api + .get('/leads/stats') + .catch(() => ({ data: stats.value })); + if (nextStats) stats.value = nextStats; + const { data: nextStages } = await api.get('/leads/stages'); + stages.value = nextStages; } catch (e) { error.value = axios.isAxiosError(e) && !e.response @@ -200,6 +341,53 @@ async function load() { } } +function openCreate() { + createError.value = ''; + createForm.value = { + title: '', + contactName: '', + mobile: '', + city: '', + region: '', + source: '', + stageId: stages.value[0]?.id ?? '', + assigneeId: '', + }; + createOpen.value = true; +} + +async function submitCreate() { + createError.value = ''; + if (!createForm.value.title.trim() || !createForm.value.mobile.trim()) { + createError.value = 'نام/شرکت و موبایل الزامی است.'; + return; + } + createBusy.value = true; + try { + const { data: created } = await api.post('/leads', { + title: createForm.value.title.trim(), + mobile: createForm.value.mobile.trim(), + contactName: createForm.value.contactName.trim() || null, + city: createForm.value.city.trim() || null, + region: createForm.value.region.trim() || null, + source: createForm.value.source.trim() || null, + stageId: createForm.value.stageId || undefined, + assigneeId: createForm.value.assigneeId || null, + }); + createOpen.value = false; + okMsg.value = 'لید جدید ثبت شد.'; + await load(); + void router.push({ name: 'lead-detail', params: { id: created.id } }); + } catch (e) { + createError.value = + axios.isAxiosError(e) && (e.response?.data as { message?: string })?.message + ? String((e.response?.data as { message?: string }).message) + : 'ثبت لید ناموفق بود.'; + } finally { + createBusy.value = false; + } +} + async function assignSelected() { if (!assignUserId.value || !selectedIds.value.length) return; busy.value = true; @@ -249,6 +437,7 @@ async function archiveSelected() { await api.post('/leads/archive', { leadIds: selectedIds.value }); okMsg.value = `${toFaDigits(selectedIds.value.length)} لید بایگانی شد.`; selected.value = new Set(); + moreOpen.value = false; await load(); } catch { error.value = 'بایگانی ناموفق بود.'; @@ -332,6 +521,12 @@ function goDetail(id: string) { void router.push({ name: 'lead-detail', params: { id } }); } +function callLead(lead: LeadDto) { + const mobile = lead.mobile?.replace(/\D/g, ''); + if (!mobile) return; + window.location.href = `tel:${mobile}`; +} + watch( () => [ route.query.q, @@ -339,6 +534,8 @@ watch( route.query.sort, route.query.stageId, route.query.assigneeId, + route.query.city, + route.query.source, route.query.unassigned, ], () => { @@ -359,53 +556,151 @@ onBeforeUnmount(stopImportPoll); + > + + + + +
+ + + +
+ + +
+ +
+ + +
+

{{ createError }}

+
+ +
@@ -612,38 +1023,204 @@ onBeforeUnmount(stopImportPoll); box-sizing: border-box; } -.link-btn { - text-decoration: none; -} - -.toolbar { +.header-actions { display: flex; - gap: 0.55rem; - align-items: center; flex-wrap: wrap; + gap: 0.5rem; + align-items: center; + justify-content: flex-end; } -.toolbar-tools { - display: flex; - gap: 0.45rem; +.excel-btn { + gap: 0.35rem; +} + +.more-btn { + width: var(--control-h, 2.5rem); + height: var(--control-h, 2.5rem); + border: 1px solid var(--line); + background: #fff; + border-radius: var(--radius); + display: inline-flex; align-items: center; + justify-content: center; + cursor: pointer; + color: var(--ink); +} + +.stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); + gap: 0.65rem; +} + +.stat { + display: flex; + align-items: center; + gap: 0.7rem; + text-align: start; + padding: 0.85rem 0.95rem; + border-radius: 12px; + border: 1px solid transparent; + background: #fff; + cursor: pointer; + font: inherit; + color: var(--ink); + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); + transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; +} + +.stat:hover { + transform: translateY(-1px); + box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08); +} + +.stat.on { + border-color: color-mix(in srgb, var(--accent) 45%, transparent); + box-shadow: 0 0 0 2px var(--accent-soft); +} + +.stat-ico { + width: 2.35rem; + height: 2.35rem; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.stat-body { + display: grid; + gap: 0.15rem; + min-width: 0; +} + +.stat-label { + font-size: 0.78rem; + color: var(--muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.stat-num { + font-size: 1.25rem; + font-weight: 800; + letter-spacing: -0.02em; + font-variant-numeric: tabular-nums; +} + +.stat.mint { + background: linear-gradient(180deg, #f0fdf6 0%, #fff 100%); +} +.stat.mint .stat-ico { + background: #d1fae5; + color: #059669; +} +.stat.mint .stat-num { + color: #047857; +} + +.stat.amber { + background: linear-gradient(180deg, #fffbeb 0%, #fff 100%); +} +.stat.amber .stat-ico { + background: #fef3c7; + color: #d97706; +} +.stat.amber .stat-num { + color: #b45309; +} + +.stat.peach { + background: linear-gradient(180deg, #fff7ed 0%, #fff 100%); +} +.stat.peach .stat-ico { + background: #ffedd5; + color: #ea580c; +} +.stat.peach .stat-num { + color: #c2410c; +} + +.stat.sky, +.stat.teal { + background: linear-gradient(180deg, #ecfdf5 0%, #fff 100%); +} +.stat.sky .stat-ico, +.stat.teal .stat-ico { + background: #ccfbf1; + color: #0d9488; +} +.stat.sky .stat-num, +.stat.teal .stat-num { + color: #0f766e; +} + +.stat.violet { + background: linear-gradient(180deg, #f5f3ff 0%, #fff 100%); +} +.stat.violet .stat-ico { + background: #ede9fe; + color: #7c3aed; +} +.stat.violet .stat-num { + color: #6d28d9; +} + +.stat.rose { + background: linear-gradient(180deg, #fef2f2 0%, #fff 100%); +} +.stat.rose .stat-ico { + background: #fee2e2; + color: #dc2626; +} +.stat.rose .stat-num { + color: #b91c1c; +} + +.filters { + display: flex; + flex-wrap: wrap; + gap: 0.65rem; + align-items: center; + justify-content: space-between; + padding: 0.7rem 0.85rem; + background: color-mix(in srgb, var(--surface-2, #f8fafc) 90%, #fff); + border: 1px solid var(--line); + border-radius: 12px; +} + +.filters-start, +.filters-end { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} + +.filters-label { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: 0.86rem; + font-weight: 600; + color: var(--ink); + padding-inline-end: 0.25rem; } .search { - flex: 1 1 14rem; - min-width: 180px; - max-width: 400px; display: flex; align-items: center; - gap: 0.45rem; + gap: 0.4rem; background: #fff; border: 1px solid var(--line); - border-radius: var(--radius); - padding: 0 0.75rem; - min-height: var(--control-h); - height: var(--control-h); + border-radius: 10px; + padding: 0 0.7rem; + min-height: 2.35rem; + min-width: 10rem; color: var(--muted); - box-sizing: border-box; } .search:focus-within { @@ -652,29 +1229,59 @@ onBeforeUnmount(stopImportPoll); } .search input { - flex: 1; - min-width: 0; border: 0; outline: none; background: transparent; font: inherit; - font-size: var(--fs-sm); + font-size: 0.88rem; color: var(--ink); + width: 8.5rem; } -.tool { - min-height: var(--control-h); - height: var(--control-h); +.filter-select, +.sort-btn { + min-height: 2.35rem; border: 1px solid var(--line); background: #fff; - border-radius: var(--radius); - padding: 0 0.75rem; + border-radius: 10px; + padding: 0 0.7rem; + font: inherit; + font-size: 0.86rem; + color: var(--ink); + cursor: pointer; +} + +.sort-btn { display: inline-flex; align-items: center; gap: 0.35rem; +} + +.view-toggle { + display: inline-flex; + border: 1px solid var(--line); + border-radius: 10px; + overflow: hidden; + background: #fff; +} + +.view-btn { + width: 2.35rem; + height: 2.35rem; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + background: transparent; + color: var(--muted); + text-decoration: none; cursor: pointer; - font: inherit; - color: var(--ink); +} + +.view-btn.on, +.view-btn:hover { + background: var(--accent-soft); + color: var(--accent); } .pop { @@ -685,21 +1292,15 @@ onBeforeUnmount(stopImportPoll); position: absolute; top: calc(100% + 4px); inset-inline-end: 0; - z-index: 20; + z-index: 30; margin: 0; padding: 0.35rem; list-style: none; background: #fff; border: 1px solid var(--line); - border-radius: var(--radius); - box-shadow: 0 10px 28px rgba(15, 23, 42, 0.12); + border-radius: 10px; + box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12); min-width: 11rem; - max-height: 18rem; - overflow: auto; -} - -.menu.wide { - min-width: 14rem; } .menu.panel { @@ -709,36 +1310,38 @@ onBeforeUnmount(stopImportPoll); min-width: 14rem; } +.menu.row-menu { + inset-inline-end: auto; + inset-inline-start: 0; +} + +.menu a, .menu button { + display: block; width: 100%; border: 0; background: none; text-align: start; padding: 0.45rem 0.55rem; - border-radius: calc(var(--radius) - 2px); + border-radius: 8px; font: inherit; cursor: pointer; color: var(--ink); + text-decoration: none; } -.menu button.on, -.menu button:hover { +.menu a:hover, +.menu button:hover, +.menu button.on { background: var(--accent-soft); color: var(--accent); } -.menu-label { - padding: 0.35rem 0.55rem 0.15rem; - font-size: 0.75rem; - color: var(--muted); - font-weight: 600; -} - .menu select { width: 100%; - min-height: var(--control-h); + min-height: 2.35rem; border: 1px solid var(--line); - border-radius: var(--radius); + border-radius: 8px; padding: 0 0.5rem; font: inherit; } @@ -750,35 +1353,37 @@ onBeforeUnmount(stopImportPoll); align-items: center; padding: 0.55rem 0.75rem; background: var(--accent-soft); - border-radius: var(--radius); + border-radius: 10px; border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent); } .table-wrap { overflow-x: auto; border: 1px solid var(--line); - border-radius: var(--radius); + border-radius: 12px; background: #fff; } .data-table { width: 100%; border-collapse: collapse; - font-size: var(--fs-sm); + font-size: 0.88rem; } .data-table th, .data-table td { - padding: 0.65rem 0.75rem; + padding: 0.85rem 0.75rem; text-align: start; border-bottom: 1px solid var(--line); - white-space: nowrap; + vertical-align: middle; } .data-table th { font-weight: 600; color: var(--muted); - background: color-mix(in srgb, var(--surface-2, #f8fafc) 80%, #fff); + background: #fafbfc; + white-space: nowrap; + font-size: 0.8rem; } .row-click { @@ -786,30 +1391,255 @@ onBeforeUnmount(stopImportPoll); } .row-click:hover { - background: color-mix(in srgb, var(--accent-soft) 55%, #fff); + background: #f8fafc; +} + +.icon-col { + width: 2.75rem; } .check-col { - width: 2.25rem; + width: 2.5rem; + text-align: center; +} + +.actions-col { + width: 7.5rem; +} + +.row-ico { + width: 2.1rem; + height: 2.1rem; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + background: #ecfdf5; + color: #059669; +} + +.row-ico[data-tint='amber'] { + background: #fef3c7; + color: #d97706; +} +.row-ico[data-tint='peach'] { + background: #ffedd5; + color: #ea580c; +} +.row-ico[data-tint='sky'], +.row-ico[data-tint='teal'] { + background: #e0f2fe; + color: #0284c7; +} +.row-ico[data-tint='violet'] { + background: #ede9fe; + color: #7c3aed; +} +.row-ico[data-tint='rose'] { + background: #fee2e2; + color: #dc2626; +} + +.name { + display: block; + font-weight: 700; + color: var(--ink); } .sub { display: block; color: var(--muted); - font-size: 0.8rem; + font-size: 0.78rem; font-weight: 400; + margin-top: 0.15rem; +} + +.phone { + font-variant-numeric: tabular-nums; + white-space: nowrap; + color: var(--ink); +} + +.city { + display: block; + font-weight: 600; +} + +.source-pill { + display: inline-block; + padding: 0.2rem 0.65rem; + border-radius: 999px; + background: #f1f5f9; + color: #475569; + font-size: 0.78rem; + font-weight: 600; } .stage-pill { display: inline-block; - padding: 0.15rem 0.55rem; + padding: 0.22rem 0.7rem; border-radius: 999px; - background: var(--accent-soft); - color: var(--accent); - font-size: 0.8rem; + font-size: 0.78rem; + font-weight: 700; + white-space: nowrap; + background: #d1fae5; + color: #047857; } -.muted { +.stage-pill[data-tint='amber'] { + background: #fef3c7; + color: #b45309; +} +.stage-pill[data-tint='peach'] { + background: #ffedd5; + color: #c2410c; +} +.stage-pill[data-tint='sky'], +.stage-pill[data-tint='teal'] { + background: #e0f2fe; + color: #0369a1; +} +.stage-pill[data-tint='violet'] { + background: #ede9fe; + color: #6d28d9; +} +.stage-pill[data-tint='rose'] { + background: #fee2e2; + color: #b91c1c; +} +.stage-pill[data-tint='mint'] { + background: #d1fae5; + color: #047857; +} + +.assignee { + display: inline-flex; + align-items: center; + gap: 0.45rem; + white-space: nowrap; +} + +.avatar { + width: 1.7rem; + height: 1.7rem; + border-radius: 999px; + background: linear-gradient(135deg, #5eead4, #34d399); + color: #064e3b; + font-size: 0.68rem; + font-weight: 800; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.activity { + display: inline-flex; + align-items: center; + gap: 0.55rem; + justify-content: space-between; + min-width: 8rem; +} + +.activity-text strong { + display: block; + font-size: 0.84rem; +} + +.activity-ico { + width: 1.7rem; + height: 1.7rem; + border-radius: 8px; + background: #f1f5f9; + color: #64748b; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.row-actions { + display: inline-flex; + align-items: center; + gap: 0.25rem; +} + +.act-btn { + width: 2rem; + height: 2rem; + border: 0; + border-radius: 999px; + background: transparent; + color: var(--muted); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.act-btn:hover { + background: #f1f5f9; + color: var(--ink); +} + +.act-btn.call { + background: #ecfdf5; + color: #059669; +} + +.act-btn.call:hover { + background: #d1fae5; +} + +.pager { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding-top: 0.25rem; +} + +.pager-pages { + display: inline-flex; + align-items: center; + gap: 0.25rem; +} + +.page-nav, +.page-num { + width: 2.1rem; + height: 2.1rem; + border: 0; + border-radius: 999px; + background: transparent; + color: var(--muted); + font: inherit; + font-weight: 600; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.page-num.on { + background: var(--accent, #0d9488); + color: #fff; +} + +.page-nav:disabled { + opacity: 0.35; + cursor: default; +} + +.pager-info { + margin: 0; + color: var(--muted); + font-size: 0.86rem; +} + +.muted, +.muted-dash { color: var(--muted); } @@ -823,9 +1653,54 @@ onBeforeUnmount(stopImportPoll); margin: 0; } -@media (max-width: 640px) { - .tool-label { - display: none; +.create-form { + display: grid; + gap: 0.75rem; +} + +.create-form label { + display: grid; + gap: 0.35rem; + font-size: 0.86rem; + font-weight: 600; + color: var(--ink); +} + +.create-form input, +.create-form select { + min-height: 2.45rem; + border: 1px solid var(--line); + border-radius: 10px; + padding: 0 0.75rem; + font: inherit; + font-weight: 400; +} + +.create-form .ltr { + direction: ltr; + text-align: left; +} + +.create-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; +} + +.empty-actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: center; +} + +@media (max-width: 720px) { + .create-grid { + grid-template-columns: 1fr; + } + + .search input { + width: 6rem; } }