Files
portal/app/resources/js/components/reminders/RemindersList.vue
T
Дмитрий 143cc458c1 fix(a11y): Q.DEFER.002 sub-B — 12 patterns fixed across 16 auth views
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>
2026-05-12 22:09:48 +03:00

174 lines
6.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>