143cc458c1
Q.DEFER.002 sub-B closure: manual Pa11y audit-pass via Playwright MCP login + axe-core CDN inject on 16 auth-required views. Found ~13 unique violation patterns, 12 fixed, 3 deferred to Q.DEFER.004. ROOT CAUSE found: AdminLayout `<v-navigation-drawer color="secondary" theme="dark">` resolved to Vuetify default-dark `secondary=#54b6b2` (Teal mid) instead of liderraForest `#012019` теало-нуар. Switching to direct hex preserves design intent + restores white-text contrast across all 8 admin views (~50 nodes color-contrast violations cleared). Patterns fixed: 1. AdminLayout sidebar palette (8 admin views): - color="secondary" → color="#012019" (root cause) - .brand-sub red #b94837 → #e06155 (3.41 → 5.08) - .nav-count gray #7a8c87 → #8a9c95 (4.26 → 5.34) - <v-list nav> + role="navigation" + aria-label (aria-required-children fix: <v-list role=list> had [role=link] children — undefined для list) 2. DashboardBalance .runway-bar — role="img" (aria-prohibited-attr fix) 3. DashboardKpiRow .delta-up — #2e8b57 → #1b6e3b (4.27 → 6.25) 4. TransactionsTable .tx-amount-up — #2e8b57 → #1b6e3b (same fix) 5. RemindersList .empty-hint — #9a9690 → #6b6356 (2.98 → 5.74; +liderra-muted alignment) 6. KanbanView .kanban-board — tabindex="0" role="region" aria-label (scrollable-region-focusable fix) 7. ProjectCard: - .v-progress-linear + :aria-label="Прогресс дневной нормы: N%" - icon menu :aria-label="Меню действий проекта «...»" - bulk-select .card-check input :aria-label="Выбрать проект «...»" 8. useStatusPill in_progress #3F7C95 → #2A5A6E (4.07 → 6.11); useStatusPill.spec.ts sync 9. ProjectsView toolbar select-all input aria-label 10. AdminTenants impersonate v-btn aria-label 11. Global app.css: `.v-messages, .v-field-label { --v-medium-emphasis-opacity: 0.7; }` Vuetify default ~0.52 → rendered #7a7a7a/#767471 fails 4.20-4.29:1; 0.7 → rendered ≈#595959 → 7.9:1+ passes WCAG AA. Re-verified post-fix via axe-core on all affected views: all clean except DEV-only `.dev-index-num` chip (tree-shaked в prod, not a real violation). Vitest verified post-fix: 79 files / 614 passed / 3 skipped / 0 failed (baseline preserved). 3 patterns deferred to Q.DEFER.004: - DealsTable VDataTable show-select bulk-checkboxes (6 nodes) — Vuetify slot rewrite needed - AdminSupplierPrices 9 form inputs — v-text-field/v-switch label props - Vuetify v-tooltip eager-mount aria-tooltip-name — library-level cosmetic Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
174 lines
6.0 KiB
Vue
174 lines
6.0 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* RemindersList — v-list напоминаний с действиями: complete (галочка),
|
||
* edit (диалог), delete (с confirm). При пустом списке — empty-state.
|
||
*
|
||
* Sprint 4 Phase B/2 — split RemindersView (audit O-refactor-04 хвост).
|
||
*
|
||
* State (items + loading) читается из reminders-store напрямую. Действия
|
||
* (edit/delete/complete/openDeal) проксируются эмитами в parent — там же
|
||
* подключены confirm-dialog и edit-dialog.
|
||
*/
|
||
import { useRemindersStore } from '../../stores/reminders';
|
||
import type { ApiReminder } from '../../api/reminders';
|
||
|
||
const store = useRemindersStore();
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'edit', reminder: ApiReminder): void;
|
||
(e: 'delete', id: number): void;
|
||
(e: 'complete', id: number): void;
|
||
(e: 'open-deal', dealId: number): void;
|
||
}>();
|
||
|
||
function formatRelative(iso: string | null): string {
|
||
if (!iso) return '—';
|
||
const ms = new Date(iso).getTime() - Date.now();
|
||
const abs = Math.abs(ms);
|
||
const min = Math.round(abs / 60_000);
|
||
const future = ms > 0;
|
||
if (min < 1) return future ? 'через минуту' : 'минуту назад';
|
||
if (min < 60) return future ? `через ${min} мин` : `${min} мин назад`;
|
||
const hr = Math.round(min / 60);
|
||
if (hr < 24) return future ? `через ${hr} ч` : `${hr} ч назад`;
|
||
const days = Math.round(hr / 24);
|
||
return future ? `через ${days} д` : `${days} д назад`;
|
||
}
|
||
|
||
function formatAbsolute(iso: string | null): string {
|
||
if (!iso) return '—';
|
||
return new Date(iso).toLocaleString('ru-RU', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<v-card variant="outlined" class="reminders-list-card">
|
||
<div v-if="store.items.length === 0 && !store.loading" class="empty-state" data-testid="reminders-empty">
|
||
<v-icon size="32" class="mb-2">mdi-clock-check-outline</v-icon>
|
||
<div>Нет напоминаний в этой группе</div>
|
||
<div class="empty-hint">Создавайте напоминания из карточки сделки.</div>
|
||
</div>
|
||
|
||
<v-list v-else density="compact" data-testid="reminders-list">
|
||
<v-list-item
|
||
v-for="r in store.items"
|
||
:key="r.id"
|
||
class="reminder-row"
|
||
data-testid="reminder-item"
|
||
:class="{
|
||
'reminder-overdue': r.completed_at === null && r.remind_at && new Date(r.remind_at) < new Date(),
|
||
'reminder-completed': r.completed_at !== null,
|
||
}"
|
||
>
|
||
<template #prepend>
|
||
<v-btn
|
||
v-if="r.completed_at === null"
|
||
icon="mdi-check-circle-outline"
|
||
size="small"
|
||
variant="text"
|
||
density="comfortable"
|
||
:data-testid="`complete-${r.id}`"
|
||
@click="emit('complete', r.id)"
|
||
/>
|
||
<v-icon v-else color="success" class="ml-2">mdi-check-circle</v-icon>
|
||
</template>
|
||
<v-list-item-title class="reminder-title">{{ r.text || 'Без описания' }}</v-list-item-title>
|
||
<v-list-item-subtitle class="reminder-meta">
|
||
<a class="deal-link" href="#" data-testid="deal-link" @click.prevent="emit('open-deal', r.deal_id)">
|
||
#{{ r.deal_id }}
|
||
</a>
|
||
·
|
||
<span :title="formatAbsolute(r.remind_at)">{{ formatRelative(r.remind_at) }}</span>
|
||
<span v-if="r.creator_name"> · {{ r.creator_name }}</span>
|
||
</v-list-item-subtitle>
|
||
<template #append>
|
||
<v-menu offset="4">
|
||
<template #activator="{ props: menuProps }">
|
||
<v-btn
|
||
v-bind="menuProps"
|
||
icon="mdi-dots-vertical"
|
||
size="small"
|
||
variant="text"
|
||
:data-testid="`menu-${r.id}`"
|
||
/>
|
||
</template>
|
||
<v-list density="compact">
|
||
<v-list-item
|
||
prepend-icon="mdi-pencil-outline"
|
||
title="Изменить"
|
||
:data-testid="`edit-${r.id}`"
|
||
@click="emit('edit', r)"
|
||
/>
|
||
<v-list-item
|
||
prepend-icon="mdi-delete-outline"
|
||
title="Удалить"
|
||
:data-testid="`delete-${r.id}`"
|
||
@click="emit('delete', r.id)"
|
||
/>
|
||
</v-list>
|
||
</v-menu>
|
||
</template>
|
||
</v-list-item>
|
||
</v-list>
|
||
</v-card>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.reminders-list-card {
|
||
min-height: 200px;
|
||
}
|
||
|
||
.empty-state {
|
||
padding: 40px 16px;
|
||
text-align: center;
|
||
color: #66635c;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.empty-hint {
|
||
font-size: 12px;
|
||
margin-top: 6px;
|
||
color: #6b6356;
|
||
}
|
||
|
||
.reminder-row {
|
||
border-bottom: 1px solid #f0ede4;
|
||
}
|
||
|
||
.reminder-overdue .reminder-meta span:first-of-type + span {
|
||
color: #b94837;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.reminder-completed .reminder-title {
|
||
text-decoration: line-through;
|
||
color: #9a9690;
|
||
}
|
||
|
||
.reminder-title {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.reminder-meta {
|
||
font-size: 12px;
|
||
color: #66635c;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.deal-link {
|
||
color: #0f6e56;
|
||
text-decoration: none;
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
}
|
||
|
||
.deal-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
</style>
|