Files
portal/app/resources/js/components/deals/DealDetailTimeline.vue
T
Дмитрий 6c2f0ce682 refactor(frontend): Sprint 3 Phase C — Top-3 Vue components split (audit O-refactor-04)
Sprint 3 Phase C. Закрытие audit O-refactor-04 (частичное — Top-3 из 12):

- DealsView.vue: 852 → 560 строк. Выделены: DealsFilters (123), DealsBulkBar (150),
  DealsTable (165). CSV-utilities (csvEscape/triggerCsvDownload/triggerBlobDownload/
  buildCsvString) вынесены в composables/useCsvDownload.ts.
- ReportsView.vue: 592 → 261 строк. Выделены: ReportRequestForm (тип отчёта,
  даты, фильтры, формат, submit/reset), ReportJobsList (список заданий со
  статусами и actions retry/cancel/delete).
- DealDetailDrawer.vue: 580 → 386 строк. Выделены: DealDetailHero (header +
  phone-link + status-chip), DealDetailTimeline (activity log с MOCK_EVENTS).
  Comment- и Reminders-секции оставлены inline — связаны с API и defineExpose.

DealsView и DealDetailDrawer остались выше 350-целевого уровня: bulk-action
функции (applyBulkStatus/applyBulkDelete/applyBulkExport/undoBulkDelete/
applyBulkRestoreFromTrash) и comment/reminders fetch — экспонируются через
defineExpose в Vitest-тестах напрямую, дальнейшая декомпозиция требует
изменения тест-контракта (отдельным flow).

Layout-структуры (AppLayout 466, AuthLayout, AppShell) НЕ ТРОНУТЫ — R0.6 hard-стоп.
Остальные 9 components >300 строк (AdminTenantDetailView, BillingView,
AdminTenantsView, SecurityTab, RemindersView, ErrorView, DashboardView,
ImpersonationDialog, далее) — вне scope Sprint 3, отдельным flow по запросу.

vue-tsc: 0 errors. ESLint: 0. Vitest: 416/416 PASS. Build: success (1.15s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:28:25 +03:00

141 lines
3.8 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">
/**
* Activity timeline секция детального drawer'а (Sprint 3 Phase C — extraction).
*
* Список событий сделки (call, status_change, comment, и т.д.) с иконкой,
* типом, временем, детализацией и actor'ом. Banner при fetch-error.
*/
import { type DealEvent, eventTypeIcon, eventTypeLabel } from '../../composables/mockDealEvents';
defineProps<{
events: DealEvent[];
eventsFetchError: boolean;
}>();
function formatRelative(minutes: number): string {
if (minutes < 60) return `${minutes} мин назад`;
if (minutes < 60 * 24) return `${Math.floor(minutes / 60)} ч назад`;
return `${Math.floor(minutes / (60 * 24))} д назад`;
}
</script>
<template>
<section class="section pa-5">
<h3 class="section-title text-subtitle-2 mb-3">Активность</h3>
<v-alert
v-if="eventsFetchError"
type="warning"
variant="tonal"
density="compact"
closable
class="mb-3"
data-testid="events-fetch-error-alert"
>
Backend недоступен показаны mock-события.
</v-alert>
<ul class="timeline">
<li v-for="event in events" :key="event.id" class="timeline-item">
<div class="timeline-icon">
<v-icon size="16">{{ eventTypeIcon(event.type) }}</v-icon>
</div>
<div class="timeline-body">
<div class="timeline-head">
<span class="timeline-type text-caption text-medium-emphasis">
{{ eventTypeLabel(event.type) }}
</span>
<span class="timeline-time text-caption text-medium-emphasis">
{{ formatRelative(event.minutesAgo) }}
</span>
</div>
<div class="timeline-detail text-body-2">{{ event.detail }}</div>
<div v-if="event.actor" class="timeline-actor text-caption text-medium-emphasis">
<v-avatar size="16" color="secondary" class="mr-1">
<span class="text-caption">{{ event.actor.initials }}</span>
</v-avatar>
{{ event.actor.name }}
</div>
</div>
</li>
</ul>
</section>
</template>
<style scoped>
.section-title {
font-weight: 600;
color: #081319;
}
.timeline {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.timeline-item {
display: flex;
gap: 12px;
align-items: flex-start;
position: relative;
}
.timeline-item::before {
content: '';
position: absolute;
left: 11px;
top: 28px;
bottom: -16px;
width: 1px;
background: #e8e3d6;
}
.timeline-item:last-child::before {
display: none;
}
.timeline-icon {
flex-shrink: 0;
width: 24px;
height: 24px;
border-radius: 50%;
background: #e1eeea;
display: flex;
align-items: center;
justify-content: center;
color: #0f6e56;
z-index: 1;
}
.timeline-body {
flex: 1;
min-width: 0;
}
.timeline-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.timeline-type {
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 11px;
}
.timeline-time {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 11px;
}
.timeline-detail {
color: #081319;
margin-top: 2px;
line-height: 1.4;
}
.timeline-actor {
display: flex;
align-items: center;
margin-top: 4px;
font-size: 11px;
}
</style>