-
- {{
- item.name
- .split(' ')
- .map((p: string) => p[0])
- .join('')
- .slice(0, 2)
- }}
-
-
-
{{ item.name }}
-
{{ item.phone }}
-
+
+ {{ item.phone }}
+
+
+
+
+ {{ item.project }}
+ {{
+ signalLabel(item.signalType)
+ }}
+
+ {{ item.city || '—' }}
+
+
-
-
-
-
- {{ item.manager.initials }}
-
- {{ item.manager.name }}
-
+
+ {{
+ formatDateTime(item.nextReminderAt)
+ }}
-
- {{ formatCost(item.cost) }}
+
+
-
- {{ formatRelative(item.receivedMinutesAgo) }}
+
+ {{ formatDateTime(item.receivedAt) }}
@@ -135,8 +117,8 @@ function formatCost(cost: number): string {
toggleSelect(internalItem)"
+ :aria-label="`Выбрать сделку «${(item as MockDeal).phone}»`"
+ @update:model-value="() => toggleSelect(internalItem)"
/>
@@ -151,34 +133,32 @@ function formatCost(cost: number): string {
.deals-table-card {
background: #fff;
}
-
.num {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-feature-settings: 'tnum';
- font-weight: 500;
}
-
-.cell-deal {
+.cell-source {
display: flex;
- align-items: center;
- padding: 6px 0;
+ flex-direction: column;
+ line-height: 1.3;
}
-
-.deal-name {
+.source-project {
font-weight: 500;
color: #081319;
}
-
-.cell-manager {
- display: flex;
- align-items: center;
+.source-signal {
+ font-size: 11px;
+ color: #6b6356;
}
-
-.status-dot {
+.cell-comment {
display: inline-block;
- width: 6px;
- height: 6px;
- border-radius: 50%;
- margin-right: 6px;
+ max-width: 240px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ vertical-align: bottom;
+}
+:deep(.deals-row-active) {
+ background: rgba(15, 110, 86, 0.07);
}
diff --git a/app/tests/Frontend/DealsTable.spec.ts b/app/tests/Frontend/DealsTable.spec.ts
index cda6baff..c73f52a0 100644
--- a/app/tests/Frontend/DealsTable.spec.ts
+++ b/app/tests/Frontend/DealsTable.spec.ts
@@ -1,7 +1,6 @@
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
-
import DealsTable from '../../resources/js/components/deals/DealsTable.vue';
import type { MockDeal } from '../../resources/js/composables/mockDeals';
@@ -9,51 +8,55 @@ const vuetify = createVuetify();
const sampleDeals: MockDeal[] = [
{
- id: 1,
- name: 'Иванов И.',
- phone: '+7 (916) 100-00-01',
- statusSlug: 'new',
- project: 'B1 site',
- manager: { initials: 'AD', name: 'Admin' },
- cost: 1000,
- receivedMinutesAgo: 5,
+ id: 1, name: '+7 (916) 100-00-01', phone: '+7 (916) 100-00-01', statusSlug: 'new',
+ project: 'Окна', manager: { initials: 'AD', name: 'Admin' }, cost: 0, receivedMinutesAgo: 5,
+ signalType: 'call', city: 'Москва', comment: 'звонил', receivedAt: '2026-05-15T09:00:00+00:00',
+ nextReminderAt: '2026-05-18T07:00:00+00:00',
},
{
- id: 2,
- name: 'Петров П.',
- phone: '+7 (916) 100-00-02',
- statusSlug: 'new',
- project: 'B1 call',
- manager: { initials: 'AD', name: 'Admin' },
- cost: 1500,
- receivedMinutesAgo: 30,
+ id: 2, name: '+7 (916) 100-00-02', phone: '+7 (916) 100-00-02', statusSlug: 'new',
+ project: 'Двери', manager: { initials: 'AD', name: 'Admin' }, cost: 0, receivedMinutesAgo: 30,
+ signalType: 'site', city: null, comment: null, receivedAt: '2026-05-14T09:00:00+00:00',
+ nextReminderAt: null,
},
];
-describe('DealsTable a11y (Q.DEFER.004 sub-A)', () => {
- it('select-all header checkbox has aria-label', () => {
- const wrapper = mount(DealsTable, {
+describe('DealsTable', () => {
+ it('рендерит колонки реестра лидов', () => {
+ const w = mount(DealsTable, {
props: { deals: sampleDeals, selectedIds: [], statusBySlug: new Map() },
global: { plugins: [vuetify] },
});
- const headerCheckbox = wrapper.find(
- 'th .v-selection-control input[type="checkbox"][aria-label="Выбрать все сделки"]',
- );
- expect(headerCheckbox.exists()).toBe(true);
+ const headers = w.findAll('thead th').map((h) => h.text());
+ ['Телефон', 'Источник', 'Город', 'Статус', 'Напоминание', 'Комментарий', 'Поставлен'].forEach((label) => {
+ expect(headers.some((h) => h.includes(label))).toBe(true);
+ });
});
- it('each row checkbox has aria-label referencing deal name', () => {
- const wrapper = mount(DealsTable, {
+ it('город без значения рендерится как «—»', () => {
+ const w = mount(DealsTable, {
props: { deals: sampleDeals, selectedIds: [], statusBySlug: new Map() },
global: { plugins: [vuetify] },
});
- const rowCheckbox1 = wrapper.find(
- 'tbody tr:nth-of-type(1) input[type="checkbox"][aria-label="Выбрать сделку «Иванов И.»"]',
- );
- const rowCheckbox2 = wrapper.find(
- 'tbody tr:nth-of-type(2) input[type="checkbox"][aria-label="Выбрать сделку «Петров П.»"]',
- );
- expect(rowCheckbox1.exists()).toBe(true);
- expect(rowCheckbox2.exists()).toBe(true);
+ expect(w.text()).toContain('—');
+ });
+
+ it('select-all чекбокс имеет aria-label', () => {
+ const w = mount(DealsTable, {
+ props: { deals: sampleDeals, selectedIds: [], statusBySlug: new Map() },
+ global: { plugins: [vuetify] },
+ });
+ expect(
+ w.find('th .v-selection-control input[type="checkbox"][aria-label="Выбрать все сделки"]').exists(),
+ ).toBe(true);
+ });
+
+ it('клик по строке эмитит row-click с deal', async () => {
+ const w = mount(DealsTable, {
+ props: { deals: sampleDeals, selectedIds: [], statusBySlug: new Map() },
+ global: { plugins: [vuetify] },
+ });
+ await w.find('tbody tr').trigger('click');
+ expect(w.emitted('row-click')?.[0]?.[0]).toMatchObject({ id: 1 });
});
});