dabur-pricing-app / react-dashboard /src /tabs /PriceGradient.jsx
Bera
initial deploy
14356bb
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>
)
}