/** * PriceTable - Responsive APMC price table with sorting and highlights. * * Features: * - Desktop: Full table with sortable columns * - Mobile: Card-based layout * - Best price row highlighted * - Price range bar (min/max) * - Formatted dates and prices * - Empty state */ import { useMemo } from "react"; import PropTypes from "prop-types"; import { motion } from "framer-motion"; import { ArrowTrendingUpIcon, ArrowTrendingDownIcon, MinusIcon, MapPinIcon, StarIcon, } from "@heroicons/react/24/outline"; import { Card, Badge } from "@components/common"; import { formatPricePerQuintal, formatDate } from "@utils/helpers"; /** * Determine a price trend indicator based on modal vs min/max spread. */ function getTrendIndicator(price) { if (!price.modal_price || !price.min_price || !price.max_price) { return { icon: MinusIcon, color: "text-neutral-400", label: "Stable" }; } const mid = (price.min_price + price.max_price) / 2; const diff = ((price.modal_price - mid) / mid) * 100; if (diff > 2) { return { icon: ArrowTrendingUpIcon, color: "text-primary-600", label: "Rising" }; } if (diff < -2) { return { icon: ArrowTrendingDownIcon, color: "text-danger-600", label: "Falling" }; } return { icon: MinusIcon, color: "text-neutral-400", label: "Stable" }; } function PriceTable({ prices = [], bestAPMCName = "", className = "" }) { // Find the best price row const bestPrice = useMemo(() => { if (prices.length === 0) return null; return prices.reduce((best, p) => (p.price_per_quintal || 0) > (best.price_per_quintal || 0) ? p : best, ); }, [prices]); // Price range for bar visualization const priceRange = useMemo(() => { if (prices.length === 0) return { min: 0, max: 1 }; const allPrices = prices.map((p) => p.price_per_quintal).filter(Boolean); return { min: Math.min(...allPrices), max: Math.max(...allPrices), }; }, [prices]); if (prices.length === 0) { return (

No price data available. Select a commodity to see prices.

); } const range = priceRange.max - priceRange.min || 1; return (
{/* Result count */}

Showing {prices.length} APMCs

{bestPrice && ( Best: {formatPricePerQuintal(bestPrice.price_per_quintal)} )}
{/* Desktop table */}
{prices.map((price, index) => { const isBest = bestAPMCName ? price.mandi_name === bestAPMCName : price === bestPrice; const trend = getTrendIndicator(price); const TrendIcon = trend.icon; const barWidth = ((price.price_per_quintal - priceRange.min) / range) * 100; return ( {/* APMC name */} {/* Location */} {/* Price */} {/* Range bar */} {/* Trend */} {/* Date */} ); })}
APMC Location Price/qtl Range Trend Date
{isBest && ( )} {price.mandi_name}
{[price.district, price.state].filter(Boolean).join(", ")} {formatPricePerQuintal(price.price_per_quintal)}
{price.min_price ? `${Math.round(price.min_price)}` : "--"}
{price.max_price ? `${Math.round(price.max_price)}` : "--"}
{formatDate(price.arrival_date)}
{/* Mobile card layout */}
{prices.map((price, index) => { const isBest = bestAPMCName ? price.mandi_name === bestAPMCName : price === bestPrice; const trend = getTrendIndicator(price); const TrendIcon = trend.icon; return ( {/* Header */}
{isBest && ( )}

{price.mandi_name}

{[price.district, price.state].filter(Boolean).join(", ")}

{isBest && ( Best Price )}
{/* Price row */}
{formatPricePerQuintal(price.price_per_quintal)}
{/* Meta */}
Range: {price.min_price ? `Rs ${Math.round(price.min_price)}` : "--"} - {price.max_price ? `Rs ${Math.round(price.max_price)}` : "--"} {formatDate(price.arrival_date)}
); })}
); } PriceTable.propTypes = { prices: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.number, mandi_name: PropTypes.string, state: PropTypes.string, district: PropTypes.string, commodity: PropTypes.string, price_per_quintal: PropTypes.number, min_price: PropTypes.number, max_price: PropTypes.number, modal_price: PropTypes.number, arrival_date: PropTypes.string, }), ), bestAPMCName: PropTypes.string, className: PropTypes.string, }; export default PriceTable;