Files
ss-tools/frontend/src/lib/components/reports/SummaryPanel.svelte
busya f3d92e55a0 refactor(frontend): replace raw buttons with /ui/Button in SummaryPanel
- Replace raw <button> with <Button variant="ghost">
- Dim zero-count badges with opacity-50
- Add transition-colors duration-300 to count numbers
- Replace inline reconnect link with themed <Button>
2026-07-05 15:50:41 +03:00

112 lines
6.0 KiB
Svelte
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.

<!-- #region SummaryPanel [C:4] [TYPE Component] [SEMANTICS component, summary, dashboard, websocket-reactive, task-status-center] -->
<!-- @BRIEF Summary dashboard panel — grid of clickable cards showing task counts by type × status. -->
<!-- @LAYER UI -->
<!-- @UX_STATE idle: cards rendered normally, clickable -->
<!-- @UX_STATE loading: skeleton placeholders (4 cards with pulse animation) -->
<!-- @UX_STATE empty: all cards show zero counts -->
<!-- @UX_STATE reconnecting: last known counts visible, yellow pulsing indicator -->
<!-- @UX_STATE disconnected: last known counts visible, opacity reduced, "disconnected" badge -->
<!-- @UX_FEEDBACK Count change: number animates (CSS transition). Card click: emits filter event. Zero-count badges remain dimmed but visible. -->
<!-- @UX_RECOVERY Disconnected: auto-reconnect indicator. Click → manual reconnect button. -->
<!-- @UX_REACTIVITY Props: summary: TaskSummary | null, screenState: ScreenState. Derived: by_type grid. -->
<!-- @RELATION BINDS_TO -> [TaskCenter.Model] -->
<!-- @PRE summary is non-null when screenState is 'ready' or 'disconnected'. -->
<!-- @POST Clicking a card calls onFilterByTypeAndStatus(taskType, status). -->
<!-- @DATA_CONTRACT Input: TaskSummary + ScreenState → Output: clickable status summary cards. -->
<!-- @SIDE_EFFECT Calls parent callbacks only; no direct network or store mutation. -->
<script lang="ts">
import { Button } from '$lib/ui';
import type { TaskType, ReportStatus, ScreenState, StatusCounts, TaskSummary } from '$types/reports';
const STATUS_CONFIG: { key: keyof StatusCounts; label: string; colorClass: string }[] = [
{ key: 'running', label: 'Выполняется', colorClass: 'bg-primary/10 text-primary' },
{ key: 'pending', label: 'Ожидает', colorClass: 'bg-surface-muted text-text-muted' },
{ key: 'awaiting_input', label: 'Ожидает ввода', colorClass: 'bg-warning-light text-warning' },
{ key: 'success', label: 'Успешно', colorClass: 'bg-success-light text-success' },
{ key: 'failed', label: 'Упало', colorClass: 'bg-destructive-light text-destructive' },
];
interface Props {
summary: TaskSummary | null;
screenState: ScreenState;
onFilterByTypeAndStatus: (_type: TaskType, _status: ReportStatus) => void;
onReconnect?: () => void;
activeFilters?: { task_types: TaskType[]; statuses: ReportStatus[] };
}
let { summary, screenState, onFilterByTypeAndStatus, onReconnect = () => {}, activeFilters = { task_types: [], statuses: [] } }: Props = $props();
function getCount(typeSummary: TaskSummary['by_type'][0], statusKey: keyof StatusCounts): number {
const c = typeSummary.counts;
return c[statusKey] ?? 0;
}
function handleCardClick(typeSummary: TaskSummary['by_type'][0], statusKey: keyof StatusCounts): void {
// Map summary status keys to ReportStatus for filtering
onFilterByTypeAndStatus(typeSummary.task_type, mapStatusKeyToReportStatus(statusKey));
}
function isStatusActive(typeSummary: TaskSummary['by_type'][0], statusKey: keyof StatusCounts): boolean {
const status = mapStatusKeyToReportStatus(statusKey);
const typeMatches = activeFilters.task_types.length === 0 || activeFilters.task_types.includes(typeSummary.task_type);
return typeMatches && activeFilters.statuses.includes(status);
}
function mapStatusKeyToReportStatus(statusKey: keyof StatusCounts): ReportStatus {
const statusMap: Record<string, ReportStatus> = {
running: 'in_progress',
pending: 'in_progress',
awaiting_input: 'in_progress',
success: 'success',
failed: 'failed',
};
return statusMap[statusKey] ?? 'in_progress';
}
</script>
{#if screenState === 'loading'}
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{#each [1, 2, 3, 4] as n (n)}
<div class="bg-surface-muted rounded-lg h-20 animate-pulse"></div>
{/each}
</div>
{:else if summary && summary.by_type.length > 0}
<div class="space-y-3">
{#each summary.by_type as typeSummary (typeSummary.task_type)}
<!-- Type header -->
<div class="flex items-center gap-2 mb-1">
<span class="text-text text-sm font-medium">{typeSummary.display_label}</span>
<span class="text-text-muted text-xs">({typeSummary.total})</span>
</div>
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5">
{#each STATUS_CONFIG as status (status.key)}
{@const count = getCount(typeSummary, status.key)}
{@const isActive = isStatusActive(typeSummary, status.key)}
<Button
variant="ghost"
size="sm"
class="flex min-h-11 w-full items-center justify-start gap-2 rounded-lg border px-2 py-2 text-left cursor-pointer transition-all hover:border-border-strong focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-ring {screenState === 'disconnected' ? 'opacity-60' : ''} {status.colorClass} {count === 0 ? 'opacity-50' : ''} {isActive ? 'border-primary-ring ring-2 ring-primary-ring/50' : 'border-border'}"
onclick={() => handleCardClick(typeSummary, status.key)}
aria-label="{typeSummary.display_label}, {status.label}: {count}"
aria-pressed={isActive}
>
<span class="text-sm font-mono min-w-[1.5rem] text-center font-semibold transition-colors duration-300">{count}</span>
<span class="text-xs truncate">{status.label}</span>
</Button>
{/each}
</div>
{/each}
{#if screenState === 'disconnected'}
<div class="flex items-center gap-2 text-xs text-text-muted mt-2">
<span class="w-2 h-2 rounded-full bg-destructive"></span>
Данные могут быть неактуальны
<Button variant="ghost" size="sm" class="h-auto p-0 underline" onclick={onReconnect}>Подключиться</Button>
</div>
{/if}
</div>
{:else}
<!-- empty state - just placeholder -->
<div class="text-text-muted text-sm py-4">Нет активных задач</div>
{/if}
<!-- #endregion SummaryPanel -->