funnel: outcomes only, SMS separate panel

This commit is contained in:
root 2026-05-25 12:47:57 +00:00
parent 011dd08f08
commit 79e1173dd3
1 changed files with 68 additions and 54 deletions

View File

@ -58,41 +58,6 @@ const CustomTooltip = ({ active, payload, label: tooltipLabel }) => {
);
};
const FunnelStep = ({ label, value, maxValue, color, pct }) => {
if (!maxValue) return null;
const widthPct = Math.max(18, (value / maxValue) * 100);
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px', width: '100%' }}>
<div style={{ fontSize: '0.85rem', color: 'var(--color-text)', fontWeight: 700, textAlign: 'center' }}>
{value}
</div>
<div style={{
width: widthPct + '%',
height: '32px',
background: color,
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'width 0.4s ease',
minWidth: '50px',
maxWidth: '100%',
}}>
<span style={{ fontSize: '0.7rem', fontWeight: 600, color: '#fff', textShadow: '0 1px 2px rgba(0,0,0,0.3)' }}>
{pct !== undefined ? pct : (maxValue > 0 ? Math.round((value / maxValue) * 100) : 0)}%
</span>
</div>
<div style={{ fontSize: '0.72rem', color: 'var(--color-text-muted)', textAlign: 'center', maxWidth: '200px' }}>
{label}
</div>
</div>
);
};
const FunnelConnector = () => (
<div style={{ width: '2px', height: '6px', background: 'var(--color-border)' }} />
);
export const AdminDashboard = () => {
const [period, setPeriod] = useState('7d');
const { stats, statusDist, dailyTrend, driverStats, economics, isLoading, error, refetch } = useAdminStats(period);
@ -132,6 +97,22 @@ export const AdminDashboard = () => {
total: d.total || 0, delivered: d.delivered || 0, problems: d.problems || 0,
}));
// Funnel data only outcomes
const confirmedSms1 = econ.confirmed_after_sms1 || 0;
const confirmedSms2 = econ.confirmed_after_sms2 || 0;
const confirmedManual = econ.confirmed_via_manual || 0;
const paidStorage = econ.paid_storage_count || 0;
const cancelled = econ.cancelled_count || 0;
const funnelTotal = confirmedSms1 + confirmedSms2 + confirmedManual + paidStorage + cancelled;
const funnelSteps = [
{ label: 'Согласовано после SMS 1', value: confirmedSms1, color: '#22c55e' },
{ label: 'Согласовано после SMS 2', value: confirmedSms2, color: '#14b8a6' },
{ label: 'Согласовано вручную', value: confirmedManual, color: '#eab308' },
{ label: 'Платное хранение', value: paidStorage, color: '#06b6d4' },
{ label: 'Отмена', value: cancelled, color: '#ef4444' },
].filter(s => s.value > 0);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
@ -245,25 +226,39 @@ export const AdminDashboard = () => {
<div style={{ color: 'var(--color-text-muted)', textAlign: 'center', padding: '1rem' }}>Нет данных</div>
) : (
<div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0', padding: '0.5rem 0' }}>
<FunnelStep label="SMS 1 отправлено" value={econ.sms1_sent_count || 0} maxValue={totalGroups} color="#06b6d4" />
<FunnelConnector />
<FunnelStep label="Согласовано после SMS 1" value={econ.confirmed_after_sms1 || 0} maxValue={totalGroups} color="#22c55e" pct={econ.sms1_conversion_pct ?? 0} />
<FunnelConnector />
<FunnelStep label="SMS 2 отправлено" value={econ.sms2_sent_count || 0} maxValue={totalGroups} color="#0284c7" />
<FunnelConnector />
<FunnelStep label="Согласовано после SMS 2" value={econ.confirmed_after_sms2 || 0} maxValue={totalGroups} color="#14b8a6" pct={econ.sms2_conversion_pct ?? 0} />
<FunnelConnector />
<FunnelStep label="Ручное подтверждение" value={econ.confirmed_via_manual || 0} maxValue={totalGroups} color="#eab308" />
<FunnelConnector />
<FunnelStep label="Застряло в ручном" value={econ.stuck_in_manual || 0} maxValue={totalGroups} color="#ef4444" />
{econ.paid_storage_count > 0 && (
<>
<FunnelConnector />
<FunnelStep label="Платное хранение" value={econ.paid_storage_count || 0} maxValue={totalGroups} color="#06b6d4" />
</>
)}
</div>
{/* Funnel bars — outcomes only */}
{funnelSteps.length === 0 ? (
<div style={{ color: 'var(--color-text-muted)', textAlign: 'center', padding: '1rem' }}>Нет завершённых согласований</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0', padding: '0.5rem 0' }}>
{funnelSteps.map((step, i) => {
const pct = totalGroups > 0 ? Math.round((step.value / totalGroups) * 100) : 0;
const widthPct = Math.max(12, (step.value / totalGroups) * 100);
return (
<div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px', width: '100%' }}>
<div style={{ fontSize: '0.85rem', fontWeight: 700, color: 'var(--color-text)', textAlign: 'center' }}>
{step.value}
</div>
<div style={{
width: widthPct + '%', height: '32px', background: step.color,
borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'width 0.4s ease', minWidth: '50px', maxWidth: '100%',
}}>
<span style={{ fontSize: '0.7rem', fontWeight: 600, color: '#fff', textShadow: '0 1px 2px rgba(0,0,0,0.3)' }}>
{pct}%
</span>
</div>
<div style={{ fontSize: '0.72rem', color: 'var(--color-text-muted)', textAlign: 'center', maxWidth: '220px' }}>
{step.label}
</div>
{i < funnelSteps.length - 1 && (
<div style={{ width: '2px', height: '6px', background: 'var(--color-border)' }} />
)}
</div>
);
})}
</div>
)}
{/* Summary */}
<div style={{
@ -287,6 +282,25 @@ export const AdminDashboard = () => {
)}
</Panel>
{/* SMS — отдельно */}
<Panel style={{ padding: '1rem' }}>
<h3 style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '0.5rem', color: 'var(--color-text)' }}>SMS</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '0.75rem' }}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '0.68rem', color: 'var(--color-text-muted)', marginBottom: '2px' }}>SMS 1 отправлено</div>
<div style={{ fontSize: '1.1rem', fontWeight: 700, color: 'var(--color-text)' }}>{econ.sms1_sent_count ?? 0}</div>
</div>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '0.68rem', color: 'var(--color-text-muted)', marginBottom: '2px' }}>SMS 2 отправлено</div>
<div style={{ fontSize: '1.1rem', fontWeight: 700, color: 'var(--color-text)' }}>{econ.sms2_sent_count ?? 0}</div>
</div>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '0.68rem', color: 'var(--color-text-muted)', marginBottom: '2px' }}>Всего SMS</div>
<div style={{ fontSize: '1.1rem', fontWeight: 700, color: 'var(--color-text)' }}>{econ.sms1_sent_count + econ.sms2_sent_count ?? 0}</div>
</div>
</div>
</Panel>
{/* Drivers */}
<Panel style={{ padding: '1rem' }}>
<h3 style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '0.5rem', color: 'var(--color-text)' }}>По водителям</h3>