Spaces:
Sleeping
Sleeping
File size: 7,710 Bytes
14356bb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | import React, { useState } from 'react'
import { useData } from '../DataContext.jsx'
export default function PriceGradient() {
const { pgi } = useData()
const [ch, setCh] = useState('TT')
if (!pgi) return <div className="panel"><div className="no-data">Loading…</div></div>
const d = pgi[ch]
if (!d) return <div className="panel"><div className="no-data">No data for {ch}</div></div>
const { rows, gi_c, gi_n, gi_chg } = d
const giColor = gi_chg < 0 ? 'var(--gn)' : gi_chg > 0 ? 'var(--rd)' : 'var(--mt)'
const dirColor = gi_n < 1 ? 'var(--gn)' : gi_n > 1 ? 'var(--rd)' : 'var(--mt)'
const giSubLabel = gi_chg === 0 ? 'Unchanged'
: (gi_chg < 0 ? 'Compressed ' : 'Stretched ') + (gi_chg < 0 ? '' : '+') + gi_chg.toFixed(2) + '%'
return (
<div className="panel">
<div className="stitle">Price Gradient Index — Current vs Recommended</div>
<div className="sdesc">
Reward gradient = % change in price/ml (PML) moving to the next pack size.
Negative = discount for buying bigger (good). Positive premium = consumer penalised for upsizing (bad).
GI = 400+ml PML ÷ 20-30ml PML.
</div>
{/* Channel filter */}
<div className="fb" style={{ marginBottom: 20 }}>
<div className="fg">
<div className="fl">Channel</div>
<select value={ch} onChange={e => setCh(e.target.value)} style={{ fontSize: 12, padding: '6px 10px', border: '1px solid var(--bd2)', borderRadius: 7, background: '#fff', color: 'var(--tx)' }}>
<option value="TT">TT — Traditional Trade</option>
<option value="MT">MT — Modern Trade</option>
</select>
</div>
</div>
{/* KPI cards */}
<div className="gi-kpi-row">
<div className="gi-kpi">
<div className="gi-kpi-lbl">GI Current</div>
<div className="gi-kpi-val" style={{ color: 'var(--mt)' }}>{gi_c.toFixed(4)}</div>
<div className="gi-kpi-sub">400+ml PML ÷ 20-30ml PML</div>
</div>
<div className="gi-kpi">
<div className="gi-kpi-lbl">GI Recommended</div>
<div className="gi-kpi-val" style={{ color: giColor }}>{gi_n.toFixed(4)}</div>
<div className="gi-kpi-sub">{giSubLabel}</div>
</div>
<div className="gi-kpi">
<div className="gi-kpi-lbl">Gradient Direction</div>
<div className="gi-kpi-val" style={{ color: dirColor }}>{gi_n < 1 ? 'Inverted ✓' : 'Positive'}</div>
<div className="gi-kpi-sub">{gi_n < 1 ? 'Large packs cheaper/ml' : 'Large packs costlier/ml'}</div>
</div>
</div>
{/* Pack table */}
<div style={{ background: '#fff', border: '1px solid var(--bd)', borderRadius: 12, overflow: 'hidden', boxShadow: '0 1px 6px rgba(0,0,0,.06)' }}>
<div style={{ padding: '12px 16px', borderBottom: '1px solid var(--bd)', background: '#f5f0e8' }}>
<span style={{ fontSize: 13, fontWeight: 700 }}>Pack-level Detail</span>
<span style={{ fontSize: 10, color: 'var(--mt)', marginLeft: 8 }}>Shaded rows = packs with recommended price change</span>
</div>
<div style={{ overflowX: 'auto', padding: 16 }}>
<table className="gi-tbl">
<thead>
<tr>
<th>Pack</th><th>SKU (ml)</th><th>MRP current</th><th>MRP recommended</th>
<th style={{ textAlign: 'center' }}>Change</th>
<th>PML current</th><th>PML rec</th><th>Reward gradient (current → rec)</th>
</tr>
</thead>
<tbody>
{rows.map((r, i) => {
const changed = r.delta !== 0
const flipWarn = r.rg_c !== null && r.rg_n !== null &&
((r.rg_c <= 0 && r.rg_n > 0) || (r.rg_c > 0 && r.rg_n < 0))
const rgStr = r.rg_c !== null && r.rg_n !== null
? `${r.rg_c >= 0 ? '+' : ''}${r.rg_c.toFixed(1)}% → ${r.rg_n >= 0 ? '+' : ''}${r.rg_n.toFixed(1)}%${flipWarn ? ' ⚠' : ''}`
: '—'
const rgClass = r.rg_n === null ? 'gi-rg-neu' : r.rg_n > 5 ? 'gi-rg-bad' : r.rg_n <= 0 ? 'gi-rg-good' : 'gi-rg-neu'
return (
<tr key={i} className={changed ? 'gi-row-changed' : ''}>
<td style={{ fontWeight: 700 }}>{r.pack}ml</td>
<td style={{ fontFamily: 'var(--mono)' }}>{r.sku}</td>
<td style={{ fontFamily: 'var(--mono)' }}>₹{r.mrp_c.toFixed(2)}</td>
<td style={{ fontFamily: 'var(--mono)' }}>
<span className={r.delta > 0 ? 'gi-mrp-up' : r.delta < 0 ? 'gi-mrp-dn' : ''}>
₹{r.mrp_n.toFixed(2)}
</span>
</td>
<td style={{ textAlign: 'center' }}>
{r.delta === 0
? <span style={{ color: 'var(--mt)' }}>—</span>
: <span className={`gi-delta ${r.delta > 0 ? 'gi-delta-up' : 'gi-delta-dn'}`}>
{r.delta > 0 ? '+' : ''}{r.delta.toFixed(0)}%
</span>
}
</td>
<td style={{ fontFamily: 'var(--mono)' }}>₹{r.pml_c.toFixed(4)}</td>
<td style={{ fontFamily: 'var(--mono)' }}>
<span className={r.delta !== 0 ? (r.pml_n > r.pml_c ? 'gi-mrp-up' : 'gi-mrp-dn') : ''}>
₹{r.pml_n.toFixed(4)}
</span>
</td>
<td><span className={`gi-rg ${rgClass}`}>{rgStr}</span></td>
</tr>
)
})}
</tbody>
</table>
</div>
{/* Flags */}
<GIFlags rows={rows} gi_n={gi_n} gi_chg={gi_chg} />
</div>
</div>
)
}
function GIFlags({ rows, gi_n, gi_chg }) {
const flags = []
rows.forEach((r, i) => {
if (r.rg_c !== null && r.rg_n !== null) {
const flip = (r.rg_c <= 0 && r.rg_n > 0) || (r.rg_c > 0 && r.rg_n < 0)
const fix = r.rg_c > 5 && r.rg_n <= 0
const prevPack = rows[i - 1]
if (flip && r.rg_n > 0) {
flags.push(
<div key={`flip-${i}`} className="gi-flag gi-flag-warn">
<span className="gi-flag-icon">!</span>
<div>
<b>{prevPack ? prevPack.pack + 'ml' : '—'}→{r.pack}ml step flips to premium (+{r.rg_n.toFixed(1)}%)</b>
{' '}— after the {r.delta > 0 ? '+' : '−'}{Math.abs(r.delta)}% change on {r.pack}ml, it becomes MORE expensive per ml than the previous pack.
</div>
</div>
)
}
if (fix) {
flags.push(
<div key={`fix-${i}`} className="gi-flag gi-flag-ok">
<span className="gi-flag-icon">✓</span>
<div>
<b>{prevPack ? prevPack.pack + 'ml' : '—'}→{r.pack}ml penalty removed</b>
{' '}— current premium of +{r.rg_c.toFixed(1)}% becomes a reward of {r.rg_n.toFixed(1)}% after the price cut.
</div>
</div>
)
}
}
})
if (gi_chg <= -10) {
flags.push(
<div key="gi-compress" className="gi-flag gi-flag-warn">
<span className="gi-flag-icon">!</span>
<div>
<b>GI compressed by {Math.abs(gi_chg).toFixed(1)}%</b>
{' '}— large packs are now significantly cheaper per ml than small packs (GI={gi_n.toFixed(4)}).
</div>
</div>
)
}
if (!flags.length) return null
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, margin: '0 16px 16px' }}>
{flags}
</div>
)
}
|