From 996e885ef03028b6d75ae0009535442f4e9f1353 Mon Sep 17 00:00:00 2001 From: privatejoel Date: Wed, 3 Jun 2026 20:11:16 +0530 Subject: [PATCH 1/2] feat: upgrade to segment-targeted net-benefit model + Retail/DMS view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the dealer-lot prototype with the national-portfolio analysis: - App.tsx: rescale to national OEM pipeline ($2.87B deployed); add segment-specific DSI targets, a net-benefit decision engine (act only when carrying cost avoided > clearance loss), strict typing, and a new Retail & DMS view drilling the thesis to dealer floorplan economics. - README.md: answer-first rewrite with defensible-metrics framing. - METHODOLOGY.md: new — full formulas, assumptions, sensitivity, limits. Verified: tsc --noEmit clean, vite build succeeds. Co-Authored-By: Claude Opus 4.8 --- METHODOLOGY.md | 149 +++++ README.md | 126 +++-- src/App.tsx | 1419 +++++++++++++++++++++++++++++++++++------------- 3 files changed, 1252 insertions(+), 442 deletions(-) create mode 100644 METHODOLOGY.md diff --git a/METHODOLOGY.md b/METHODOLOGY.md new file mode 100644 index 0000000..afa7307 --- /dev/null +++ b/METHODOLOGY.md @@ -0,0 +1,149 @@ +# Methodology & Assumptions + +This document makes the model defensible. Every figure in the app derives from the +inputs and formulas below. Numbers are **illustrative** — calibrated to OEM scale so +the narrative is coherent, not pulled from Hyundai's actuals. + +## 1. Scope & unit of analysis + +- **Market:** single market (U.S.), national aggregate. Not modeled per-region or per-dealer. +- **Cadence:** monthly demand, expressed as steady-state run-rate. +- **Pipeline:** units in the national pipeline (in-transit + on-lot), valued at OEM wholesale cost. +- **Currency:** USD, OEM wholesale (not retail/MSRP), to keep capital-at-risk on a cost basis. + +## 2. Inputs (per model) + +| Field | Meaning | +|---|---| +| `stock` | Units in the national pipeline | +| `avgMonthlySales` | Steady-state monthly demand (units) | +| `unitCost` | OEM wholesale cost per unit (USD) | +| `category` | Luxury / EV / SUV / Volume — drives the target DSI | + +## 3. Core formulas + +``` +dailySales = avgMonthlySales × (1 + demandShift) / 30 +DSI = stock / dailySales # days of inventory on hand +targetStock = targetDSI[category] × dailySales +excessUnits = max(0, stock − targetStock) +excessCapital = excessUnits × unitCost +carryingOnExcess= excessCapital × carryingRate # annual $ bled on the excess +liquidationLoss = excessCapital × liquidationDiscount # one-time clearance haircut +recaptured = excessCapital × (1 − liquidationDiscount) +netFirstYear = carryingOnExcess − liquidationLoss +portfolioDSI = Σ stock / Σ dailySales # volume-weighted, not a simple average +``` + +**Status rule (segment-specific, not one universal cutoff):** + +- `At Risk` — `DSI > targetDSI` (over-stocked relative to the segment's velocity) +- `Stockout Risk` — `DSI < 0.5 × targetDSI` (too lean; turning away serviceable demand) +- `Healthy` — otherwise + +## 4. Segment DSI targets (rationale) + +High-margin, low-velocity segments tolerate more days on hand; fast-turning volume +segments do not. A uniform threshold would contradict the thesis. + +| Segment | Target DSI | Rationale | +|---|---|---| +| Luxury | 75 | Low velocity, high margin; longer hold is economic | +| EV | 70 | Elevated by residual-value risk during the BEV transition | +| SUV | 60 | Mainstream mid-velocity | +| Volume | 45 | High turnover; lean target protects ROIC | + +## 5. Decision rule + +For each above-target pool, act **only when `netFirstYear > 0`** — i.e. the annual +carrying cost avoided exceeds the one-time clearance loss. Recaptured capital is then +redeployed against `Stockout Risk` pools first (relieving lost-sale demand), with the +remainder funding strategic EV / growth priorities. This resolves the +redirect-vs-liquidate distinction: transfer where a constrained pool can absorb units, +clear only where none can. + +## 6. Scenario parameters (defaults & ranges) + +| Parameter | Default | Range | Source logic | +|---|---|---|---| +| `carryingRate` | 18%/yr | 8–28% | Floorplan interest (~8%) + depreciation (~7%) + storage/insurance (~3%) | +| `liquidationDiscount` | 6% | 2–15% | Wholesale/auction haircut to clear excess units | +| `demandShift` | 0% | −20% to +20% | Demand stress test | + +## 7. Baseline results (default assumptions) + +| Metric | Value | +|---|---| +| Capital deployed | $2.87B | +| Capital above target | $412M (14% of deployed) | +| Annualized carrying cost on excess | $74M/yr | +| Capital recaptured (net of discount) | $387M | +| One-time clearance loss | $25M | +| First-year net benefit | $49M | +| Stockout-risk funding gap | $216M (fully covered by recaptured capital) | +| Portfolio DSI (volume-weighted) | 50 days | + +## 8. Sensitivity (illustrative) + +- **Carrying rate is the swing factor.** At 8% the annual bleed falls to ~$33M and several + pools no longer clear the net-benefit hurdle; at 28% it rises past ~$115M and clearance is + obviously worth it. This is why the rate assumption must be owned by Finance, not assumed. +- **Clearance discount** mostly affects *how much* is recaptured, not *whether* to act, + until it approaches the carrying rate. +- **Demand shift** moves pools across thresholds — a −10% shock pushes additional volume/SUV + pools above target; +10% relieves them. The status logic recomputes live. + +## 9. Known limitations (and the honest next steps) + +- **No regional granularity.** A real reallocation engine needs per-region inventory and demand + to choose transfer-vs-clear; this models the portfolio level. *Next step: a region × model matrix.* +- **Contribution margin not modeled**, so redeployment upside is stated as the funded demand gap, + not an incremental-profit figure. *Next step: per-segment margin to value recaptured capital.* +- **Steady-state demand** — no seasonality, model-year transitions, or incentive elasticity. +- **Single-period view** — `netFirstYear` mixes a recurring annual saving with a one-time loss; + a full NPV would discount multi-year carrying savings. *Next step: multi-period NPV.* +- Figures are illustrative and not Hyundai actuals. + +## 10. Retail & DMS layer + +The retail view drills the portfolio thesis down to the dealer P&L, where above-target +capital becomes **aged inventory carrying floorplan interest**. + +**Data lineage.** The Dealer Management System (CDK, Reynolds & Reynolds, Tekion, etc.) is +the system of record. VIN-level in-stock dates, F&I and sales transactions, and RDR (retail +delivery reporting) to the OEM are the source for days-on-lot, floorplan balance, and turn. +DMS vendors are named for context only — no affiliation or endorsement. + +**Inputs (per dealer):** `avgUnitCost`, `monthlySales`, and an aging vector +(`0–30 / 31–60 / 61–90 / 90+` day buckets, in units). + +``` +onLot = Σ aging buckets +agedUnits = units 61–90 + units 90+ # > 60 days +criticalUnits = units 90+ # floorplan-curtailment territory +agedShare = agedUnits / onLot +daysSupply = onLot / (monthlySales / 30) +turn (annual) = monthlySales × 12 / onLot +floorplanMonth = onLot × avgUnitCost × (floorplanAPR / 12) +agedFloorplanMo = agedUnits × avgUnitCost × (floorplanAPR / 12) # recoverable waste +``` + +**Floorplan APR:** 9.5% (constant). At 90+ days most floorplan lenders require principal +curtailment, so 90+ is treated as the critical bucket. + +**Retail lever rule (heuristic):** + +- `agedShare > 25%` → **Targeted incentive** if turn > 9x (the store can retail its way out), + else **Dealer trade + incentive** (move units to a higher-velocity rooftop). +- `agedShare 15–25%` → **Promote pre-90d** (act before curtailment). +- otherwise → **Hold**. + +**Baseline network result (illustrative, 5 rooftops):** 540 units on lot, 31% aged > 60 days, +$17.8M floorplan balance costing ~$141K/mo, of which ~$48K/mo is burned on aged units. The +EV-heavy and luxury rooftops carry ~43% aged inventory — the same IONIQ 5 / Genesis pressure +from the OEM layer, now visible as floorplan cost. + +**Retail-layer limitations / next steps:** front + back-end gross erosion by age bucket is not +yet modeled (would quantify the *margin* cost of aging, not just floorplan); dealer-trade +logistics cost is assumed negligible; and a live DMS integration (vs. illustrative aging +vectors) is the obvious productionization step. diff --git a/README.md b/README.md index 89a5f43..4f4f8f0 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,95 @@ -# OEM Inventory Health Dashboard +# OEM Inventory Health -A strategic **Days Sales of Inventory (DSI)** optimization platform for automotive OEM supply chain management. This interactive dashboard enables executives to monitor global fleet health, identify capital-locked inventory, and execute real-time reallocation strategies across regional channels. +**A decision-support model for automotive inventory strategy — from OEM portfolio down to the dealer lot. Answer-first brief, a live net-benefit engine, a governed rollout plan, and a DMS-sourced retail view.** -## Features +🔗 **Live:** https://privatejoel.github.io/OEM-Inventory-Health/ +📄 **Methodology & assumptions:** [METHODOLOGY.md](./METHODOLOGY.md) -- 📊 **Real-Time DSI Monitoring** – Calculates Days Sales of Inventory metrics across vehicle segments -- 🎯 **Capital Leakage Detection** – Identifies at-risk inventory (DSI > 60 days) prone to depreciation -- 🔄 **Inventory Redirection Simulation** – Model 30% capital reallocation with 6% liquidation loss vs. 15% holding-cost savings -- 📈 **Multi-Segment Analytics** – Track Luxury, Volume, EV, and SUV categories simultaneously -- 💰 **Liquidity Tracking** – Real-time recaptured capital display for reinvestment decisions -- 📋 **Executive Case Study** – Comprehensive narrative on OEM inventory challenges and solutions +--- -## Built With +## The problem -- **React** 18.2 – UI framework -- **TypeScript** – Type-safe development -- **Tailwind CSS** 3.3 – Utility-first styling -- **Vite** 5.0 – Lightning-fast build tool -- **Lucide React** – Premium icon library +A multi-billion-dollar national vehicle portfolio is, at any moment, simultaneously +*over-stocked in slow-turning segments* and *under-stocked in fast ones*. Judging every +segment against one inventory benchmark hides this — luxury and high-volume models have +fundamentally different velocity economics. The result is capital bleeding carrying cost in +one place while demand goes unserved in another. -## Quick Start +## The approach -```bash -# Install dependencies -npm install - -# Start development server -npm run dev - -# Build for production -npm run build -``` +Measure each model's **Days Sales of Inventory (DSI)** against a **segment-specific target**, +then apply a **net-benefit decision rule**: clear above-target inventory *only when the annual +carrying cost avoided exceeds the one-time clearance loss*, and redeploy the recaptured capital +to relieve stockout-risk segments first. -The app will open at `http://localhost:5173` with: -- **Case Study** tab – Educational narrative on inventory optimization frameworks -- **Live Dashboard** tab – Interactive inventory health simulator with Hyundai/Genesis product lineup +## Key results (illustrative baseline) -## Deployment +| Metric | Value | +|---|---| +| Capital deployed | **$2.87B** | +| Capital above segment target | **$412M** (14%) | +| Annualized carrying cost bled | **$74M/yr** | +| Capital recaptured | **$387M** | +| First-year net benefit | **$49M** | +| Demand-constrained gap funded | **$216M**, fully covered | -### GitHub Pages (Automatic) +The recaptured capital more than covers the volume shortfall — the EV-glut and slow-luxury pools +fund the lean volume line, with ~$171M left for strategic priorities. Full derivation in +[METHODOLOGY.md](./METHODOLOGY.md). -This project is configured for automatic deployment to GitHub Pages. Every push to `main` branch triggers the deployment workflow. +## How it's structured -**To set up:** +The app is organized as four role-targeted views: -1. Ensure the repository is public or has GitHub Pages enabled -2. Go to **Settings** → **Pages** → set source to "Deploy from a branch" (gh-pages) -3. The site will be live at: `https://privatejoel.github.io/OEM-Inventory-Health/` +1. **Strategy Brief** — answer-first recommendation with quantified impact, then a + Situation / Complication / Resolution case. *(Consulting framing.)* +2. **Decision Dashboard** — segment-targeted DSI table, corrected and accurately-labeled KPIs, + a ranked net-benefit recommendation engine, and **live scenario sliders** (carrying rate, + clearance discount, demand shift) so every figure can be stress-tested. *(Planning / analytics.)* +3. **Execution Plan** — phased rollout, RACI decision rights, governance KPIs, and a + risk/mitigation register. *(Program management.)* +4. **Retail & DMS** — drills the OEM thesis down to the dealer P&L: DMS-sourced aging buckets, + floorplan cost, inventory turn, and dealer-level retail levers (incentive / dealer trade / + wholesale). Shows the OEM → dealer → DMS data flow. *(Retail execution.)* -### Manual Deployment +## Why the metrics are defensible -```bash -# Deploy to GitHub Pages -npm run deploy -``` +This was deliberately built to survive a probing interview: -This runs the build and deploys the `dist` folder to the `gh-pages` branch. +- **Scale matches the narrative** — national-aggregate units and OEM wholesale costs put the + portfolio in the billions, not the low millions. +- **Metrics are labeled for what they are** — "Annualized Carrying Cost," "Capital Above Target," + and a volume-weighted "Portfolio DSI," not a count ratio mislabeled as a financial return. +- **The headline tradeoff is actually computed** — carrying cost avoided vs. clearance loss, per + pool, with an explicit act/hold rule — not asserted in prose. +- **Targets are segment-specific**, consistent with the core thesis. +- **Assumptions are owned and sourced**, with sensitivity and limitations documented openly. -## Use Case +## Built with -**Scenario:** A global OEM (Hyundai Motor Group) manages a multi-billion-dollar vehicle pipeline across luxury (Genesis G80), volume (Venue, Elantra), and EV segments (IONIQ 5). High-DSI luxury models accumulate holding costs while volume segments suffer from stockouts. This dashboard provides the decision-support layer to: +React 18 · TypeScript (strict) · Tailwind CSS 3 · Vite 5 · lucide-react -1. **Detect** – Identify which regional channels have stagnant capital -2. **Model** – Simulate inventory redirection and capital recapture scenarios -3. **Execute** – Approve strategic liquidation with transparent ROI calculations +## Run locally -## Data Model +```bash +npm install +npm run dev # http://localhost:5173 +npm run build # production build to /dist +npm run deploy # publish /dist to GitHub Pages +``` -The dashboard tracks 6 vehicle models with realistic metrics: +## Limitations -| Model | Segment | Stock | Monthly Sales | Unit Cost | Category | -|-------|---------|-------|---|---|---| -| Genesis G80 | Luxury Sedan | 14 | 3.2 | $56,000 | Luxury | -| Hyundai Venue | Compact SUV | 58 | 62 | $19,800 | Volume | -| Hyundai IONIQ 5 | EV SUV | 32 | 38 | $45,000 | EV | -| Hyundai Santa Fe | Mid-size SUV | 24 | 28 | $36,000 | SUV | -| Hyundai Elantra | Sedan | 42 | 55 | $21,500 | Volume | -| Hyundai Palisade | Full-size SUV | 18 | 5.5 | $48,000 | Luxury | +Portfolio-level (no per-region matrix), steady-state demand, single-period net benefit, and +contribution margin not yet modeled. Each is called out with its next step in +[METHODOLOGY.md](./METHODOLOGY.md) §9. ## Disclaimer -This project is **for educational purposes only**. It is not affiliated with, endorsed by, or representative of Hyundai Motor Group or its subsidiaries. All data, scenarios, and optimization models are simulated to demonstrate product planning and analytical frameworks. +For portfolio and educational purposes only. Not affiliated with, endorsed by, or representative +of Hyundai Motor Group or its subsidiaries. All data and scenarios are simulated to demonstrate +the analytical framework. ## Author -Joel Johnson – OEM Product Strategy Portfolio \ No newline at end of file +**Joel Johnson** — Automotive Strategy & Planning Portfolio diff --git a/src/App.tsx b/src/App.tsx index b120055..1b5b527 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,433 +1,1086 @@ import React, { useState, useMemo } from 'react'; -import { - TrendingUp, - AlertCircle, - CheckCircle2, - ArrowRightLeft, - DollarSign, - Package, - BarChart3, - RefreshCw, - Info, - X, - ArrowRight, - Factory, - FileText, - LayoutDashboard, - Target, - Zap, +import { + AlertTriangle, + DollarSign, + Info, + Factory, + FileText, + LayoutDashboard, + Target, + Zap, ShieldAlert, - RotateCcw + RotateCcw, + Gauge, + Boxes, + TrendingDown, + ArrowDownRight, + GitBranch, + Users, + ClipboardCheck, + Store, + Truck, + Clock, + Building2, } from 'lucide-react'; -// Real-world OEM Data: Hyundai/Genesis Lineup -const HYUNDAI_INVENTORY = [ - { id: 1, model: 'Genesis G80', segment: 'Luxury Sedan', stock: 14, avgMonthlySales: 3.2, unitCost: 56000, category: 'Luxury' }, - { id: 2, model: 'Hyundai Venue', segment: 'Compact SUV', stock: 58, avgMonthlySales: 62, unitCost: 19800, category: 'Volume' }, - { id: 3, model: 'Hyundai IONIQ 5', segment: 'EV SUV', stock: 32, avgMonthlySales: 38, unitCost: 45000, category: 'EV' }, - { id: 4, model: 'Hyundai Santa Fe', segment: 'Mid-size SUV', stock: 24, avgMonthlySales: 28, unitCost: 36000, category: 'SUV' }, - { id: 5, model: 'Hyundai Elantra', segment: 'Sedan', stock: 42, avgMonthlySales: 55, unitCost: 21500, category: 'Volume' }, - { id: 6, model: 'Hyundai Palisade', segment: 'Full-size SUV', stock: 18, avgMonthlySales: 5.5, unitCost: 48000, category: 'Luxury' }, +/* ============================================================================ + MODEL + ---------------------------------------------------------------------------- + Single-market (U.S.) national portfolio, monthly. Figures are illustrative + but scaled to OEM reality so the narrative ("multi-billion-dollar pipeline") + matches the data. Full assumptions and formulas are documented in + METHODOLOGY.md. + ========================================================================== */ + +interface InventoryItem { + id: number; + model: string; + segment: string; + category: 'Luxury' | 'EV' | 'SUV' | 'Volume'; + stock: number; // units in national pipeline + avgMonthlySales: number; // units / month (steady-state demand) + unitCost: number; // OEM wholesale cost / unit (USD) +} + +const HYUNDAI_INVENTORY: InventoryItem[] = [ + { id: 1, model: 'Genesis G80', segment: 'Luxury Sedan', category: 'Luxury', stock: 3400, avgMonthlySales: 1200, unitCost: 48000 }, + { id: 2, model: 'Genesis GV80', segment: 'Luxury SUV', category: 'Luxury', stock: 2600, avgMonthlySales: 950, unitCost: 55000 }, + { id: 3, model: 'Hyundai IONIQ 5', segment: 'EV SUV', category: 'EV', stock: 12000, avgMonthlySales: 3000, unitCost: 40000 }, + { id: 4, model: 'Hyundai Santa Fe', segment: 'Mid-size SUV', category: 'SUV', stock: 21000, avgMonthlySales: 12000, unitCost: 30000 }, + { id: 5, model: 'Hyundai Elantra', segment: 'Compact Sedan', category: 'Volume', stock: 9000, avgMonthlySales: 14000, unitCost: 18000 }, + { id: 6, model: 'Hyundai Palisade', segment: 'Full-size SUV', category: 'SUV', stock: 13500, avgMonthlySales: 9000, unitCost: 42000 }, + { id: 7, model: 'Hyundai Tucson', segment: 'Compact SUV', category: 'Volume', stock: 30000, avgMonthlySales: 15000, unitCost: 24000 }, ]; -const INITIAL_LIQUID_CASH = 2450000; +// Segment-specific DSI targets. High-margin / low-velocity segments tolerate +// more days on hand than fast-turning volume segments. This is the core of the +// thesis: a single uniform threshold would be analytically wrong. +const SEGMENT_TARGET_DSI: Record = { + Luxury: 75, + EV: 70, + SUV: 60, + Volume: 45, +}; + +// A pool running below half its target DSI is treated as stockout-risk +// (too lean — leaving sales on the table). +const STOCKOUT_FACTOR = 0.5; -const CaseStudyPage = () => ( -
-
-

Executive Case Study

-

- Optimizing Multi-Billion Dollar
OEM Inventory Cycles -

-

- A strategic framework for capital reallocation in high-volatility automotive markets. +interface ScenarioParams { + carryingRate: number; // annual carrying cost as % of unit cost (floorplan + depreciation + storage) + liquidationDiscount: number; // one-time haircut to clear excess units + demandShift: number; // +/- shock applied to monthly sales +} + +const DEFAULT_PARAMS: ScenarioParams = { + carryingRate: 0.18, + liquidationDiscount: 0.06, + demandShift: 0, +}; + +type Status = 'At Risk' | 'Healthy' | 'Stockout Risk'; + +interface ProcessedItem extends InventoryItem { + dailySales: number; + dsi: number; + targetDSI: number; + targetStock: number; + status: Status; + excessUnits: number; + excessCapital: number; + shortfallUnits: number; + shortfallCapital: number; + carryingOnExcess: number; // annual $ bleed on the excess + recaptured: number; // capital freed if excess cleared (net of discount) + liquidationLoss: number; // one-time loss to clear excess + netFirstYear: number; // carryingOnExcess - liquidationLoss + totalCapital: number; +} + +function process(items: InventoryItem[], p: ScenarioParams): ProcessedItem[] { + return items.map((item) => { + const adjMonthly = item.avgMonthlySales * (1 + p.demandShift); + const dailySales = adjMonthly / 30; + const dsi = dailySales > 0 ? item.stock / dailySales : 999; + const targetDSI = SEGMENT_TARGET_DSI[item.category]; + const targetStock = Math.round(targetDSI * dailySales); + + let status: Status = 'Healthy'; + if (dsi > targetDSI) status = 'At Risk'; + else if (dsi < targetDSI * STOCKOUT_FACTOR) status = 'Stockout Risk'; + + const excessUnits = Math.max(0, item.stock - targetStock); + const excessCapital = excessUnits * item.unitCost; + const shortfallUnits = Math.max(0, targetStock - item.stock); + const shortfallCapital = status === 'Stockout Risk' ? shortfallUnits * item.unitCost : 0; + + const carryingOnExcess = excessCapital * p.carryingRate; + const liquidationLoss = excessCapital * p.liquidationDiscount; + const recaptured = excessCapital * (1 - p.liquidationDiscount); + const netFirstYear = carryingOnExcess - liquidationLoss; + + return { + ...item, + dailySales, + dsi: Math.round(dsi), + targetDSI, + targetStock, + status, + excessUnits, + excessCapital, + shortfallUnits, + shortfallCapital, + carryingOnExcess, + recaptured, + liquidationLoss, + netFirstYear, + totalCapital: item.stock * item.unitCost, + }; + }); +} + +interface Portfolio { + capitalDeployed: number; + capitalAboveTarget: number; + carryingBleed: number; // annual $ bleed on above-target capital + portfolioDSI: number; // volume-weighted days on hand + recapturable: number; + liquidationLossTotal: number; + netFirstYearTotal: number; + stockoutGapCapital: number; + atRiskCount: number; +} + +function summarize(rows: ProcessedItem[]): Portfolio { + const capitalDeployed = rows.reduce((s, r) => s + r.totalCapital, 0); + const capitalAboveTarget = rows.reduce((s, r) => s + r.excessCapital, 0); + const carryingBleed = rows.reduce((s, r) => s + r.carryingOnExcess, 0); + const totalStock = rows.reduce((s, r) => s + r.stock, 0); + const totalDaily = rows.reduce((s, r) => s + r.dailySales, 0); + const recapturable = rows.reduce((s, r) => s + r.recaptured, 0); + const liquidationLossTotal = rows.reduce((s, r) => s + r.liquidationLoss, 0); + const stockoutGapCapital = rows.reduce((s, r) => s + r.shortfallCapital, 0); + return { + capitalDeployed, + capitalAboveTarget, + carryingBleed, + portfolioDSI: totalDaily > 0 ? Math.round(totalStock / totalDaily) : 0, + recapturable, + liquidationLossTotal, + netFirstYearTotal: carryingBleed - liquidationLossTotal, + stockoutGapCapital, + atRiskCount: rows.filter((r) => r.status === 'At Risk').length, + }; +} + +/* ============================================================================ + FORMATTING + ========================================================================== */ + +const money = (n: number): string => { + const a = Math.abs(n); + const sign = n < 0 ? '-' : ''; + if (a >= 1e9) return `${sign}$${(a / 1e9).toFixed(2)}B`; + if (a >= 1e6) return `${sign}$${(a / 1e6).toFixed(0)}M`; + if (a >= 1e3) return `${sign}$${(a / 1e3).toFixed(0)}K`; + return `${sign}$${a.toFixed(0)}`; +}; + +const num = (n: number): string => n.toLocaleString('en-US'); +const pct = (n: number): string => `${(n * 100).toFixed(0)}%`; + +const STATUS_STYLE: Record = { + 'At Risk': 'text-red-600 bg-red-50 border-red-200', + Healthy: 'text-green-600 bg-green-50 border-green-200', + 'Stockout Risk': 'text-amber-600 bg-amber-50 border-amber-200', +}; + +/* ============================================================================ + STRATEGY BRIEF (consulting-grade, answer-first) + ========================================================================== */ + +const TONE: Record = { + slate: 'bg-slate-100', + red: 'bg-red-50', + green: 'bg-green-50', +}; + +const BriefBlock: React.FC<{ + icon: React.ReactNode; + tone: keyof typeof TONE; + label: string; + lead: string; + children: React.ReactNode; +}> = ({ icon, tone, label, lead, children }) => ( +

+
+
+
{icon}
+

{label}

+
+

{lead}

+
+
+

{children}

+
+
+); + +const StrategyBrief: React.FC<{ rows: ProcessedItem[]; pf: Portfolio }> = ({ rows, pf }) => { + const aboveTargetShare = pf.capitalDeployed > 0 ? pf.capitalAboveTarget / pf.capitalDeployed : 0; + const evRow = rows.find((r) => r.category === 'EV'); + const leanRow = rows.find((r) => r.status === 'Stockout Risk'); + + return ( +
+
+

Strategy Brief

+

+ Reallocating above-target inventory to recover capital and protect ROIC +

+
+ + {/* Answer-first recommendation */} +
+

Recommendation

+

+ Clear {money(pf.capitalAboveTarget)} of above-target + inventory ({pct(aboveTargetShare)} of deployed capital). This halts{' '} + {money(pf.carryingBleed)}/yr of carrying cost, recaptures{' '} + {money(pf.recapturable)}, and fully funds the{' '} + {money(pf.stockoutGapCapital)} shortfall in + demand-constrained volume segments — for a one-time clearance cost of{' '} + {money(pf.liquidationLossTotal)}. +

+
+ {[ + { k: 'First-year net benefit', v: money(pf.netFirstYearTotal) }, + { k: 'Recurring savings', v: `${money(pf.carryingBleed)}/yr` }, + { k: 'Capital recaptured', v: money(pf.recapturable) }, + { k: 'Pools to act on', v: `${pf.atRiskCount}` }, + ].map((m) => ( +
+

{m.k}

+

{m.v}

+
+ ))} +
+
+ +
+ } + tone="slate" + label="Situation" + lead="Capital is locked in a multi-billion-dollar national pipeline; velocity varies sharply by segment." + > + The portfolio holds {money(pf.capitalDeployed)} across luxury, + EV, SUV, and volume segments. Each segment turns at a different rate, so inventory health cannot be judged + against one universal benchmark — it has to be measured against a segment-specific target Days Sales of + Inventory (DSI). + + + } + tone="red" + label="Complication" + lead="The current mix is simultaneously over-stocked in slow segments and under-stocked in fast ones." + > + {evRow && ( + <> + {evRow.model} is running at {evRow.dsi} days against a{' '} + {evRow.targetDSI}-day target — softening BEV demand showing up as on-lot capital.{' '} + + )} + {leanRow && ( + <> + At the same time, {leanRow.model} sits at {leanRow.dsi} days{' '} + — too lean — so the network is turning away volume demand it could profitably serve.{' '} + + )} + The net effect is {money(pf.carryingBleed)}/yr of avoidable + carrying cost while a {money(pf.stockoutGapCapital)} demand + gap goes unfunded. + + + } + tone="green" + label="Resolution" + lead="A net-benefit decision rule that acts only when carrying cost avoided exceeds the clearance cost." + > + For every above-target pool, act only when{' '} + + carrying cost avoided > one-time clearance loss + + . Recaptured capital is redeployed to relieve stockout-risk segments first, with the remainder funding + strategic EV and growth priorities. The Decision Dashboard quantifies this per model and lets you stress the + assumptions; the Execution Plan governs the rollout. + +
+ +

+ Illustrative analysis for portfolio purposes. Not affiliated with, endorsed by, or representative of Hyundai + Motor Group. All figures are simulated to demonstrate the analytical framework; assumptions and formulas are + documented in METHODOLOGY.md.

+ ); +}; + +/* ============================================================================ + DECISION DASHBOARD (planning / analytics-grade) + ========================================================================== */ + +const KpiCard: React.FC<{ label: string; value: string; sub: string; tip: string; accent?: string }> = ({ + label, + value, + sub, + tip, + accent = 'text-slate-800', +}) => ( +
+
+

{label}

+ +
+

{value}

+

{sub}

+
+); + +const Slider: React.FC<{ + label: string; + value: number; + min: number; + max: number; + step: number; + display: string; + onChange: (v: number) => void; +}> = ({ label, value, min, max, step, display, onChange }) => ( +
+
+ {label} + {display} +
+ onChange(parseFloat(e.target.value))} + className="w-full accent-indigo-600 cursor-pointer" + /> +
+); -
- {/* Section 1: Situation */} -
-
-
-
-

The Situation

+const Dashboard: React.FC<{ + rows: ProcessedItem[]; + pf: Portfolio; + params: ScenarioParams; + setParams: React.Dispatch>; + recaptured: number; + onExecute: (id: number) => void; + onExecuteAll: () => void; + onReset: () => void; +}> = ({ rows, pf, params, setParams, recaptured, onExecute, onExecuteAll, onReset }) => { + const recommendations = rows + .filter((r) => r.status === 'At Risk') + .sort((a, b) => b.excessCapital - a.excessCapital); + + return ( +
+ {/* Header */} +
+
+

Decision Dashboard

+

Segment-targeted DSI monitoring with net-benefit reallocation

+
+
+
+ +
+
+

Capital Recaptured

+

{money(recaptured)}

+

Freed for redeployment

-

- Modern OEMs manage massive global vehicle pipelines where capital is locked in transit for 60-90 days before reaching retail delivery. -

-
-

- "In 2024, inventory holding costs surged due to shifting consumer preferences from ICE to EV and high interest rates on floor-planning loans. Hyundai Motor Group required a mechanism to identify 'stagnant' capital at a regional level before it impacted quarterly ROE." -

+
+ + {/* KPIs */} +
+ + + + +
+ + {/* Scenario controls */} +
+
+ +

Scenario Assumptions

+ — drag to test sensitivity; every figure recomputes live
-
- - {/* Section 2: Complication */} -
-
-
-
-

The Complication

+
+ setParams((p) => ({ ...p, carryingRate: v }))} + /> + setParams((p) => ({ ...p, liquidationDiscount: v }))} + /> + 0 ? '+' : ''}${pct(params.demandShift)}`} + onChange={(v) => setParams((p) => ({ ...p, demandShift: v }))} + /> +
+
+ + {/* Channel table */} +
+
+
+ +

Segment Velocity Map

+ +
+
+ Healthy + Stockout Risk + At Risk
-

- Traditional reporting is reactive. By the time a 'slow-mover' is identified, the capital is already depleted by depreciation and regional port storage fees. -

-
-
-
-

Issue A

-

Luxury segments (Genesis) have high margins but extremely low velocity, causing "dead-stock" if misallocated to wrong regions.

-
-
-

Issue B

-

High-turnover segments (Venue/Elantra) suffer from frequent stockouts, leading to missed revenue opportunities.

+
+ + + + + + + + + + + + {rows.map((r) => ( + + + + + + + + ))} + +
ModelDSI / TargetStatusCapital Above TargetAction
+

{r.model}

+

{r.segment} · {num(r.stock)} units

+
+ + {r.dsi} + + / {r.targetDSI}d + + + {r.status} + + + {r.excessCapital > 0 ? money(r.excessCapital) : } + + {r.status === 'At Risk' ? ( + + ) : ( + No action + )} +
+
+
+ + {/* Recommendation engine */} + {recommendations.length > 0 && ( +
+
+
+ +

Recommended Actions

+ — ranked by capital above target, net-benefit tested
+ +
+
+ {recommendations.map((r) => ( +
+
+
+
+

{r.model} — clear {num(r.excessUnits)} units to target

+

+ Avoids {money(r.carryingOnExcess)}/yr carrying cost + at a one-time {money(r.liquidationLoss)} clearance + loss. Recaptures {money(r.recaptured)}. +

+
+
+
+

First-year net benefit

+

= 0 ? 'text-green-600' : 'text-red-600'}`}> + {money(r.netFirstYear)} +

+
+
+ ))}
-
- - {/* Section 3: Resolution */} -
-
-
-
-

The Resolution

+ )} + +
+ +
+
+ ); +}; + +/* ============================================================================ + EXECUTION PLAN (program-management-grade) + ========================================================================== */ + +const PHASES = [ + { window: 'Weeks 0–2', name: 'Mobilize', owner: 'PMO', detail: 'Stand up the data feed, ratify segment DSI targets with Finance, baseline the bleed.' }, + { window: 'Weeks 2–6', name: 'Pilot', owner: 'Regional Ops', detail: 'Apply the net-benefit rule to EV + one region; validate the recaptured-capital model against actuals.' }, + { window: 'Weeks 6–12', name: 'Scale', owner: 'Sales Ops', detail: 'Roll across all segments; embed reallocation triggers in the monthly S&OP cycle.' }, + { window: 'Ongoing', name: 'Govern', owner: 'Strategy / Finance', detail: 'Monthly net-benefit realized vs. plan; recalibrate targets quarterly as residuals and rates move.' }, +]; + +const RACI = { + cols: ['PMO', 'Regional Ops', 'Finance', 'Network'], + rows: [ + { task: 'Set segment DSI targets', vals: ['C', 'C', 'A', 'I'] }, + { task: 'Approve reallocation', vals: ['I', 'R', 'A', 'C'] }, + { task: 'Execute clearance', vals: ['I', 'A', 'I', 'R'] }, + { task: 'Track realized benefit', vals: ['A', 'C', 'R', 'I'] }, + ], +}; + +const RISKS = [ + { risk: 'Luxury brand-equity erosion from discounting', mit: 'Use targeted retail incentives and inter-region transfers before any open clearance.' }, + { risk: 'Channel conflict over reallocated units', mit: 'Route decisions through Network with transparent allocation rules and dealer comms.' }, + { risk: 'Demand-forecast error invalidates targets', mit: 'Recompute against rolling 90-day sales; cap single-cycle reallocation at 30% of a pool.' }, + { risk: 'EV residual-value volatility', mit: 'Tighten the EV carrying-rate assumption; re-test net benefit monthly, not quarterly.' }, +]; + +const RACI_COLOR: Record = { + R: 'bg-indigo-600 text-white', + A: 'bg-slate-900 text-white', + C: 'bg-slate-200 text-slate-700', + I: 'bg-slate-50 text-slate-400', +}; + +const ExecutionPlan: React.FC = () => ( +
+
+

Execution Plan

+

+ From recommendation to governed rollout +

+
+ + {/* Phases */} +
+ +

Phased Rollout

+
+
+ {PHASES.map((p, i) => ( +
+
+ {p.window} + {i + 1}
-

- A Predictive DSI Dashboard that allows OEM Executives to simulate "Inventory Redirection" from low-velocity ports to high-demand hubs. -

+

{p.name}

+

Owner: {p.owner}

+

{p.detail}

+
+ ))} +
+ + {/* RACI */} +
+ +

Decision Rights (RACI)

+
+
+ + + + + {RACI.cols.map((c) => )} + + + + {RACI.rows.map((row) => ( + + + {row.vals.map((v, idx) => ( + + ))} + + ))} + +
Activity{c}
{row.task} + {v} +
+
+ + {/* KPIs */} +
+ +

Governance KPIs

+
+
+ {[ + { k: 'Weekly', v: 'At-risk pool watchlist + units above target' }, + { k: 'Monthly', v: 'Realized net benefit vs. plan; capital recaptured' }, + { k: 'Quarterly', v: 'DSI target recalibration vs. rates & residuals' }, + ].map((m) => ( +
+

{m.k}

+

{m.v}

-
-

The Proposed Solution:

-
    -
  • -
    1
    -

    - Threshold Automation: Set a 60-day DSI hard-cap to trigger immediate management review of luxury segments. -

    -
  • -
  • -
    2
    -

    - Liquidity Reinvestment: 30% reallocation of capital from "At Risk" units back into Volume and EV segments. -

    -
  • -
  • -
    3
    -

    - Decision-Support UI: A confirmation workflow that models the 6% liquidation loss versus the 15% holding-cost saving. -

    -
  • -
+ ))} +
+ + {/* Risks */} +
+ +

Key Risks & Mitigations

+
+
+ {RISKS.map((r) => ( +
+

{r.risk}

+

{r.mit}

-
- - {/* Disclaimer Section for Case Study */} -
-

- Disclaimer: This case study and associated dashboard are created for educational purposes only. This project is not affiliated with, endorsed by, or representative of Hyundai Motor Group or its subsidiaries. All data, scenarios, and optimization models are simulated to demonstrate product planning and analytical capabilities. -

-
+ ))}
); -interface ProcessedItem { +/* ============================================================================ + RETAIL & DMS LAYER (retail-execution-grade) + ---------------------------------------------------------------------------- + Drills the OEM thesis down to where it becomes a dealer P&L reality. The + Dealer Management System (e.g. CDK, Reynolds & Reynolds, Tekion) is the + system of record: VIN-level in-stock dates, F&I / sales transactions, and + RDR (retail delivery reporting) back to the OEM are the source for + days-on-lot, floorplan, and turn used here. See METHODOLOGY.md §10. + ========================================================================== */ + +const FLOORPLAN_APR = 0.095; // dealer floorplan financing rate + +interface Dealer { id: number; - model: string; - segment: string; - stock: number; - avgMonthlySales: number; - unitCost: number; - category: string; - dsi: number; - status: string; - color: string; - icon: React.ReactNode; - totalCapital: number; + name: string; + profile: string; + avgUnitCost: number; + monthlySales: number; + aging: { d0_30: number; d31_60: number; d61_90: number; d90plus: number }; } -const App = () => { - const [view, setView] = useState<'casestudy' | 'dashboard'>('casestudy'); - const [inventory, setInventory] = useState(HYUNDAI_INVENTORY); - const [liquidCash, setLiquidCash] = useState(INITIAL_LIQUID_CASH); - const [lastAction, setLastAction] = useState(null); - const [selectedItem, setSelectedItem] = useState(null); - - const processedInventory = useMemo(() => { - return inventory.map(item => { - const dsi = Math.round(item.stock / (item.avgMonthlySales / 30)); - let status = 'Healthy'; - let color = 'text-green-600 bg-green-50 border-green-200'; - let icon = ; - - if (dsi > 60) { - status = 'At Risk'; - color = 'text-red-600 bg-red-50 border-red-200'; - icon = ; - } else if (dsi > 30) { - status = 'Warning'; - color = 'text-amber-600 bg-amber-50 border-amber-200'; - icon = ; - } - const totalCapital = item.stock * item.unitCost; - return { ...item, dsi, status, color, icon, totalCapital }; - }); - }, [inventory]); - - const metrics = useMemo(() => { - const totalInventoryValue = processedInventory.reduce((sum, item) => sum + item.totalCapital, 0); - const atRiskCapital = processedInventory - .filter(item => item.status === 'At Risk') - .reduce((sum, item) => sum + item.totalCapital, 0); - const healthyInventory = processedInventory.filter(item => item.status === 'Healthy').length; - return { totalInventoryValue, atRiskCapital, healthyInventory }; - }, [processedInventory]); - - const resetDashboard = () => { - setInventory(HYUNDAI_INVENTORY); - setLiquidCash(INITIAL_LIQUID_CASH); - setLastAction(null); - setSelectedItem(null); - }; +const DEALERS: Dealer[] = [ + { id: 1, name: 'Summit Hyundai', profile: 'Balanced metro', avgUnitCost: 28000, monthlySales: 95, aging: { d0_30: 60, d31_60: 35, d61_90: 18, d90plus: 7 } }, + { id: 2, name: 'Metro Genesis', profile: 'Luxury, low velocity', avgUnitCost: 50000, monthlySales: 30, aging: { d0_30: 22, d31_60: 18, d61_90: 15, d90plus: 15 } }, + { id: 3, name: 'Coastal Hyundai', profile: 'EV-heavy', avgUnitCost: 38000, monthlySales: 70, aging: { d0_30: 50, d31_60: 35, d61_90: 30, d90plus: 35 } }, + { id: 4, name: 'Valley Hyundai', profile: 'Volume, fast turn', avgUnitCost: 22000, monthlySales: 110, aging: { d0_30: 55, d31_60: 25, d61_90: 8, d90plus: 2 } }, + { id: 5, name: 'Lakeside Hyundai', profile: 'Mixed', avgUnitCost: 30000, monthlySales: 65, aging: { d0_30: 40, d31_60: 30, d61_90: 22, d90plus: 18 } }, +]; - const openReallocationModal = (id: number) => { - const item = processedInventory.find(i => i.id === id); - if (item) setSelectedItem(item); - }; +interface DealerCalc extends Dealer { + onLot: number; + agedUnits: number; // > 60 days + criticalUnits: number; // 90+ days (curtailment territory) + agedShare: number; + daysSupply: number; + turn: number; // annualized + floorplanMonth: number; + agedFloorplanMonth: number; + lever: string; + note: string; +} - const executeReallocation = () => { - if (!selectedItem) return; - const stockToLiquidate = Math.ceil(selectedItem.stock * 0.30); - const recoveredCapital = stockToLiquidate * selectedItem.unitCost * 0.94; - setInventory(prev => prev.map(i => i.id === selectedItem.id ? { ...i, stock: i.stock - stockToLiquidate } : i)); - setLiquidCash(prev => prev + recoveredCapital); - setLastAction(`OEM Action Executed: Redirected ${stockToLiquidate} units of ${selectedItem.model}. Recovered $${(recoveredCapital / 1000000).toFixed(2)}M.`); - setSelectedItem(null); - }; +function dealerCalc(d: Dealer): DealerCalc { + const onLot = d.aging.d0_30 + d.aging.d31_60 + d.aging.d61_90 + d.aging.d90plus; + const agedUnits = d.aging.d61_90 + d.aging.d90plus; + const criticalUnits = d.aging.d90plus; + const agedShare = onLot > 0 ? agedUnits / onLot : 0; + const daily = d.monthlySales / 30; + const daysSupply = daily > 0 ? Math.round(onLot / daily) : 0; + const turn = onLot > 0 ? (d.monthlySales * 12) / onLot : 0; + const floorplanMonth = onLot * d.avgUnitCost * (FLOORPLAN_APR / 12); + const agedFloorplanMonth = agedUnits * d.avgUnitCost * (FLOORPLAN_APR / 12); + + let lever = 'Hold'; + let note = 'Aging within normal range.'; + if (agedShare > 0.25) { + lever = turn > 9 ? 'Targeted incentive' : 'Dealer trade + incentive'; + note = `${agedUnits} units over 60 days burning ${money(agedFloorplanMonth)}/mo; ${criticalUnits} past 90-day curtailment.`; + } else if (agedShare > 0.15) { + lever = 'Promote pre-90d'; + note = `${agedUnits} units aging; move before 90-day curtailment hits.`; + } + return { ...d, onLot, agedUnits, criticalUnits, agedShare, daysSupply, turn, floorplanMonth, agedFloorplanMonth, lever, note }; +} + +const LEVER_STYLE: Record = { + Hold: 'text-slate-400 bg-slate-50 border-slate-200', + 'Promote pre-90d': 'text-amber-600 bg-amber-50 border-amber-200', + 'Targeted incentive': 'text-indigo-600 bg-indigo-50 border-indigo-200', + 'Dealer trade + incentive': 'text-red-600 bg-red-50 border-red-200', +}; + +const RetailDMS: React.FC = () => { + const dealers = DEALERS.map(dealerCalc); + const onLot = dealers.reduce((s, d) => s + d.onLot, 0); + const agedUnits = dealers.reduce((s, d) => s + d.agedUnits, 0); + const floorplanBalance = dealers.reduce((s, d) => s + d.onLot * d.avgUnitCost, 0); + const floorplanMonth = dealers.reduce((s, d) => s + d.floorplanMonth, 0); + const agedFloorplanMonth = dealers.reduce((s, d) => s + d.agedFloorplanMonth, 0); + const totalMonthlySales = dealers.reduce((s, d) => s + d.monthlySales, 0); + const networkTurn = onLot > 0 ? (totalMonthlySales * 12) / onLot : 0; + + const buckets = [ + { label: '0–30d', n: dealers.reduce((s, d) => s + d.aging.d0_30, 0), color: 'bg-green-500' }, + { label: '31–60d', n: dealers.reduce((s, d) => s + d.aging.d31_60, 0), color: 'bg-slate-400' }, + { label: '61–90d', n: dealers.reduce((s, d) => s + d.aging.d61_90, 0), color: 'bg-amber-500' }, + { label: '90+d', n: dealers.reduce((s, d) => s + d.aging.d90plus, 0), color: 'bg-red-500' }, + ]; + + const actions = dealers.filter((d) => d.lever !== 'Hold').sort((a, b) => b.agedFloorplanMonth - a.agedFloorplanMonth); return ( -
- - {/* Navigation Sidebar-style Toggle */} - +

+ Data lineage: VIN-level in-stock dates, F&I and sales + transactions, and RDR flow from the DMS (CDK · Reynolds · + Tekion) → these are the source for days-on-lot, floorplan balance, and turn. The capital the OEM flags as + above target shows up here as + aged units carrying floorplan interest until they retail, + transfer, or wholesale. +

+
-
- {view === 'casestudy' ? ( - - ) : ( -
- {/* Reallocation Modal */} - {selectedItem && ( -
-
-
-
-

Regional Optimization

-

OEM Strategy Engine

-
- -
- -
-
- -
-

Production Alert: High Inventory

-

- {selectedItem.model} channel DSI is {selectedItem.dsi} days. -

-
-
- -
-
-

Current Stock

-

{selectedItem.stock} Units

-
-
-

Redirect Target

-

{Math.ceil(selectedItem.stock * 0.30)} Units

-
-
- -
-

Capital Recapture

-

- +${((Math.ceil(selectedItem.stock * 0.30) * selectedItem.unitCost * 0.94) / 1000000).toFixed(2)}M -

-
-
+ {/* Network KPIs */} +
+ + + 0 ? agedUnits / onLot : 0)} + sub={`${num(agedUnits)} units past 60 days on lot.`} + tip="Units over 60 days as a share of on-lot inventory. The retail mirror of the OEM above-target metric." + accent="text-amber-600" + /> + +
-
- - -
-
+ {/* Aging distribution */} +
+
+ +

Network Aging Distribution

+ — 90+ days triggers floorplan curtailment +
+
+ {buckets.map((b) => ( +
+ ))} +
+
+ {buckets.map((b) => ( +
+ +
+

{num(b.n)} units

+

{b.label} · {pct(b.n / onLot)}

- )} +
+ ))} +
+
- {/* Header */} -
-
-

Inventory Health

-

Monitoring Days Sales of Inventory (DSI) for National Fleet Management

-
-
-
- -
+ {/* Dealer table */} +
+
+ +

Dealer Network

+
+
+ + + + + + + + + + + + {dealers.map((d) => ( + + + + + + + + ))} + +
RooftopDays Supply / TurnAged >60dFloorplan / moRecommended Lever
+

{d.name}

+

{d.profile} · {num(d.onLot)} units

+
+ {d.daysSupply}d + · {d.turn.toFixed(1)}x + + 0.25 ? 'text-red-600' : d.agedShare > 0.15 ? 'text-amber-600' : 'text-slate-800'}`}> + {pct(d.agedShare)} + + · {d.criticalUnits} @ 90+ + {money(d.floorplanMonth)} + + {d.lever} + +
+
+
+ + {/* Retail actions */} + {actions.length > 0 && ( +
+
+ +

Retail Actions

+ — ranked by monthly floorplan burn on aged units +
+
+ {actions.map((d) => ( +
+
+
-
-

Recaptured Capital

- -
-

${(liquidCash / 1000000).toFixed(2)}M

-

Recovered for Reinvestment

+

{d.name} — {d.lever.toLowerCase()}

+

{d.note}

-
- - {/* Summary Metrics */} -
-
-
-

Global Fleet Value

- -
-

${(metrics.totalInventoryValue / 1000000).toFixed(2)}M

-

Total market valuation of vehicles in channel.

-
-
-
-

Capital Leakage Risk

- +
+

Floorplan recoverable

+

{money(d.agedFloorplanMonth)}/mo

-

${(metrics.atRiskCapital / 1000000).toFixed(2)}M

-

Assets in low-velocity segments prone to depreciation.

-
-
-

ROE Efficiency

- -
-

{((metrics.healthyInventory / inventory.length) * 100).toFixed(0)}%

-

Ratio of high-turnover assets vs. total fleet.

-
-
+ ))} +
+
+ )} - {/* Table */} -
-
-
-

Channel Velocity Map

- -
-
-
-
Healthy
-
At Risk
-
-

Real-time segment performance tracking

-
-
-
- - - - - - - - - - - {processedInventory.map((item) => ( - - - - - - - ))} - -
Vehicle ModelSegmentAvg. DSIStrategic Action
-

{item.model}

-

{item.category}

-
- {item.segment} - - {item.dsi} DAYS - - {item.status === 'At Risk' ? ( - - ) : ( - Efficient - )} -
-
-
+

+ Representative dealer-group figures, illustrative only. DMS vendors named for context; no affiliation or + endorsement implied. Floorplan and aging logic documented in METHODOLOGY.md §10. +

+
+ ); +}; - {/* Execution Log */} - {lastAction && ( -
-
-
-

Execution Log

-

{lastAction}

-
-
- )} - - {/* Dashboard Disclaimer */} -
-

- Note: This dashboard is for educational purposes only and is not affiliated with, endorsed by, or representative of Hyundai Motor Group. All data and scenarios are simulated to demonstrate automotive product planning frameworks. -

-
+/* ============================================================================ + APP SHELL + ========================================================================== */ + +type View = 'brief' | 'dashboard' | 'execution' | 'retail'; + +const App: React.FC = () => { + const [view, setView] = useState('brief'); + const [inventory, setInventory] = useState(HYUNDAI_INVENTORY); + const [params, setParams] = useState(DEFAULT_PARAMS); + const [recaptured, setRecaptured] = useState(0); + + const rows = useMemo(() => process(inventory, params), [inventory, params]); + const pf = useMemo(() => summarize(rows), [rows]); + + // Strategy brief always reflects the baseline data at default assumptions so + // the headline numbers stay stable and consistent with the documented case. + const baselineRows = useMemo(() => process(HYUNDAI_INVENTORY, DEFAULT_PARAMS), []); + const baselinePf = useMemo(() => summarize(baselineRows), [baselineRows]); + + const executeOne = (id: number) => { + const row = rows.find((r) => r.id === id); + if (!row || row.excessUnits <= 0) return; + setRecaptured((c) => c + row.recaptured); + setInventory((prev) => prev.map((i) => (i.id === id ? { ...i, stock: row.targetStock } : i))); + }; + + const executeAll = () => { + const positive = rows.filter((r) => r.status === 'At Risk' && r.netFirstYear > 0); + if (positive.length === 0) return; + const freed = positive.reduce((s, r) => s + r.recaptured, 0); + setRecaptured((c) => c + freed); + setInventory((prev) => + prev.map((i) => { + const hit = positive.find((r) => r.id === i.id); + return hit ? { ...i, stock: hit.targetStock } : i; + }) + ); + }; + + const reset = () => { + setInventory(HYUNDAI_INVENTORY); + setParams(DEFAULT_PARAMS); + setRecaptured(0); + }; + + const tabs: { id: View; label: string; icon: React.ReactNode }[] = [ + { id: 'brief', label: 'Strategy Brief', icon: }, + { id: 'dashboard', label: 'Decision Dashboard', icon: }, + { id: 'execution', label: 'Execution Plan', icon: }, + { id: 'retail', label: 'Retail & DMS', icon: }, + ]; + + return ( +
+ + +
+ {view === 'brief' && } + {view === 'dashboard' && ( +
+
)} -
+ {view === 'execution' && } + {view === 'retail' && } + -
-

- Joel Johnson | OEM Product Strategy Portfolio | Industry Case Study: Hyundai Motor Group +

+

+ Joel Johnson · Automotive Strategy & Planning Portfolio +

+

+ Illustrative case study. Not affiliated with or endorsed by Hyundai Motor Group. Methodology and assumptions in + METHODOLOGY.md.

From 42f1a99a6162db438e45bdc47031cebf0979f09c Mon Sep 17 00:00:00 2001 From: privatejoel Date: Wed, 3 Jun 2026 20:14:44 +0530 Subject: [PATCH 2/2] fix: enforce net-benefit rule per row + harden CI/deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dashboard: the per-row Reallocate action now obeys the same net-benefit gate as "Execute all" — when clearing a pool costs more than the carrying cost it avoids (netFirstYear <= 0), the row shows a "Hold · net-negative" state instead of an actionable button, and executeOne guards against acting. Previously a single row could be cleared against the model's own act-only-when-it-pays rule (e.g. at an 8% carrying rate). Matches METHODOLOGY.md §5/§8. - CI: add a `tsc --noEmit` typecheck gate before build. - Deploy: guard the GitHub Pages publish to push-to-main only, so pull_request builds no longer risk publishing un-merged code. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/deploy.yml | 4 ++++ src/App.tsx | 13 +++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e916246..4148e6e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -27,10 +27,14 @@ jobs: - name: Install dependencies run: npm install + - name: Type check + run: npx tsc --noEmit + - name: Build run: npm run build - name: Deploy to GitHub Pages + if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/src/App.tsx b/src/App.tsx index 1b5b527..a462451 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -524,13 +524,20 @@ const Dashboard: React.FC<{ {r.excessCapital > 0 ? money(r.excessCapital) : } - {r.status === 'At Risk' ? ( + {r.status === 'At Risk' && r.netFirstYear > 0 ? ( + ) : r.status === 'At Risk' ? ( + + Hold · net-negative + ) : ( No action )} @@ -997,7 +1004,9 @@ const App: React.FC = () => { const executeOne = (id: number) => { const row = rows.find((r) => r.id === id); - if (!row || row.excessUnits <= 0) return; + // Honor the net-benefit rule per row, exactly like "Execute all": only act + // when carrying cost avoided exceeds the one-time clearance loss. + if (!row || row.excessUnits <= 0 || row.netFirstYear <= 0) return; setRecaptured((c) => c + row.recaptured); setInventory((prev) => prev.map((i) => (i.id === id ? { ...i, stock: row.targetStock } : i))); };