funnel: outcomes only, SMS separate panel
This commit is contained in:
parent
011dd08f08
commit
79e1173dd3
|
|
@ -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 = () => {
|
export const AdminDashboard = () => {
|
||||||
const [period, setPeriod] = useState('7d');
|
const [period, setPeriod] = useState('7d');
|
||||||
const { stats, statusDist, dailyTrend, driverStats, economics, isLoading, error, refetch } = useAdminStats(period);
|
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,
|
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 (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
<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 style={{ color: 'var(--color-text-muted)', textAlign: 'center', padding: '1rem' }}>Нет данных</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0', padding: '0.5rem 0' }}>
|
{/* Funnel bars — outcomes only */}
|
||||||
<FunnelStep label="SMS 1 отправлено" value={econ.sms1_sent_count || 0} maxValue={totalGroups} color="#06b6d4" />
|
{funnelSteps.length === 0 ? (
|
||||||
<FunnelConnector />
|
<div style={{ color: 'var(--color-text-muted)', textAlign: 'center', padding: '1rem' }}>Нет завершённых согласований</div>
|
||||||
<FunnelStep label="Согласовано после SMS 1" value={econ.confirmed_after_sms1 || 0} maxValue={totalGroups} color="#22c55e" pct={econ.sms1_conversion_pct ?? 0} />
|
) : (
|
||||||
<FunnelConnector />
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0', padding: '0.5rem 0' }}>
|
||||||
<FunnelStep label="SMS 2 отправлено" value={econ.sms2_sent_count || 0} maxValue={totalGroups} color="#0284c7" />
|
{funnelSteps.map((step, i) => {
|
||||||
<FunnelConnector />
|
const pct = totalGroups > 0 ? Math.round((step.value / totalGroups) * 100) : 0;
|
||||||
<FunnelStep label="Согласовано после SMS 2" value={econ.confirmed_after_sms2 || 0} maxValue={totalGroups} color="#14b8a6" pct={econ.sms2_conversion_pct ?? 0} />
|
const widthPct = Math.max(12, (step.value / totalGroups) * 100);
|
||||||
<FunnelConnector />
|
return (
|
||||||
<FunnelStep label="Ручное подтверждение" value={econ.confirmed_via_manual || 0} maxValue={totalGroups} color="#eab308" />
|
<div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px', width: '100%' }}>
|
||||||
<FunnelConnector />
|
<div style={{ fontSize: '0.85rem', fontWeight: 700, color: 'var(--color-text)', textAlign: 'center' }}>
|
||||||
<FunnelStep label="Застряло в ручном" value={econ.stuck_in_manual || 0} maxValue={totalGroups} color="#ef4444" />
|
{step.value}
|
||||||
{econ.paid_storage_count > 0 && (
|
</div>
|
||||||
<>
|
<div style={{
|
||||||
<FunnelConnector />
|
width: widthPct + '%', height: '32px', background: step.color,
|
||||||
<FunnelStep label="Платное хранение" value={econ.paid_storage_count || 0} maxValue={totalGroups} color="#06b6d4" />
|
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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Summary */}
|
{/* Summary */}
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|
@ -287,6 +282,25 @@ export const AdminDashboard = () => {
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</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 */}
|
{/* Drivers */}
|
||||||
<Panel style={{ padding: '1rem' }}>
|
<Panel style={{ padding: '1rem' }}>
|
||||||
<h3 style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '0.5rem', color: 'var(--color-text)' }}>По водителям</h3>
|
<h3 style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '0.5rem', color: 'var(--color-text)' }}>По водителям</h3>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue