Spaces:
Running
Running
Route economy drilldown pages
Browse files- web-app-v2/src/App.tsx +6 -0
- web-app-v2/src/pages/Market.tsx +158 -130
web-app-v2/src/App.tsx
CHANGED
|
@@ -46,6 +46,12 @@ function AppRoutes() {
|
|
| 46 |
<Market initialTab="economy" />
|
| 47 |
</ProtectedRoute>
|
| 48 |
} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
<Route path="/portfolio" element={
|
| 51 |
<ProtectedRoute>
|
|
|
|
| 46 |
<Market initialTab="economy" />
|
| 47 |
</ProtectedRoute>
|
| 48 |
} />
|
| 49 |
+
|
| 50 |
+
<Route path="/economy/:economyPage" element={
|
| 51 |
+
<ProtectedRoute>
|
| 52 |
+
<Market initialTab="economy" />
|
| 53 |
+
</ProtectedRoute>
|
| 54 |
+
} />
|
| 55 |
|
| 56 |
<Route path="/portfolio" element={
|
| 57 |
<ProtectedRoute>
|
web-app-v2/src/pages/Market.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
| 10 |
useGetSecondaryBondDailyDetailQuery,
|
| 11 |
useGetSecondaryBondsQuery,
|
| 12 |
} from '../store/api/economyApi';
|
| 13 |
-
import { useNavigate, useSearchParams } from 'react-router-dom';
|
| 14 |
import {
|
| 15 |
Search,
|
| 16 |
TrendingUp,
|
|
@@ -39,6 +39,7 @@ type AssetTab = 'stocks' | 'funds' | 'bonds' | 'economy';
|
|
| 39 |
|
| 40 |
export default function Market({ initialTab = 'stocks' }: { initialTab?: AssetTab }) {
|
| 41 |
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
| 42 |
const initialSearch = searchParams.get('search') || '';
|
| 43 |
const [activeTab, setActiveTab] = useState<AssetTab>(initialTab);
|
| 44 |
const [searchQuery, setSearchQuery] = useState(initialSearch);
|
|
@@ -206,6 +207,7 @@ export default function Market({ initialTab = 'stocks' }: { initialTab?: AssetTa
|
|
| 206 |
|
| 207 |
{activeTab === 'economy' ? (
|
| 208 |
<EconomyPanel
|
|
|
|
| 209 |
intelligence={intelligence}
|
| 210 |
alerts={filteredAlerts}
|
| 211 |
secondaryBonds={filteredSecondaryBonds}
|
|
@@ -308,6 +310,7 @@ type AssetRowProps = {
|
|
| 308 |
};
|
| 309 |
|
| 310 |
type EconomyPanelProps = {
|
|
|
|
| 311 |
intelligence: any | null;
|
| 312 |
alerts: any[];
|
| 313 |
secondaryBonds: any[];
|
|
@@ -353,6 +356,7 @@ const buildInflationChartRows = (series: any[], selectedBaskets: string[]) => {
|
|
| 353 |
const formatDuration = (value: any) => String(value || 'UNKNOWN');
|
| 354 |
|
| 355 |
const EconomyPanel: React.FC<EconomyPanelProps> = ({
|
|
|
|
| 356 |
intelligence,
|
| 357 |
alerts,
|
| 358 |
secondaryBonds,
|
|
@@ -362,7 +366,7 @@ const EconomyPanel: React.FC<EconomyPanelProps> = ({
|
|
| 362 |
isLoading,
|
| 363 |
onRefresh,
|
| 364 |
}) => {
|
| 365 |
-
const
|
| 366 |
const [selectedBondDay, setSelectedBondDay] = React.useState<string | null>(null);
|
| 367 |
const topOpportunities = alerts.filter((alert) => alert.severity === 'opportunity').slice(0, 4);
|
| 368 |
const cautions = alerts.filter((alert) => alert.severity !== 'opportunity').slice(0, 4);
|
|
@@ -388,6 +392,17 @@ const EconomyPanel: React.FC<EconomyPanelProps> = ({
|
|
| 388 |
{ key: 'bonds', label: 'BONDS', icon: <Landmark className="h-3.5 w-3.5" /> },
|
| 389 |
{ key: 'nbs', label: 'NBS_DATA', icon: <FileText className="h-3.5 w-3.5" /> },
|
| 390 |
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
|
| 392 |
return (
|
| 393 |
<div className="space-y-4">
|
|
@@ -414,7 +429,7 @@ const EconomyPanel: React.FC<EconomyPanelProps> = ({
|
|
| 414 |
<button
|
| 415 |
key={item.key}
|
| 416 |
type="button"
|
| 417 |
-
onClick={() =>
|
| 418 |
className={`flex items-center justify-center gap-2 px-4 py-3 text-[10px] mono font-bold transition-all ${subPage === item.key ? 'bg-accent text-bg' : 'bg-bg-card text-text-dim hover:text-white'}`}
|
| 419 |
>
|
| 420 |
{item.icon}
|
|
@@ -458,136 +473,149 @@ const EconomyPanel: React.FC<EconomyPanelProps> = ({
|
|
| 458 |
</div>
|
| 459 |
</section>
|
| 460 |
|
| 461 |
-
|
| 462 |
-
<>
|
| 463 |
-
<
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
<LineChart data={inflationChartRows}>
|
| 486 |
-
<CartesianGrid stroke="#24303a" strokeDasharray="3 3" />
|
| 487 |
-
<XAxis dataKey="date" stroke="#8b95a1" tick={{ fontSize: 10 }} />
|
| 488 |
-
<YAxis stroke="#8b95a1" tick={{ fontSize: 10 }} tickFormatter={(value) => `${value}%`} />
|
| 489 |
-
<Tooltip contentStyle={{ background: '#090b0f', border: '1px solid #26313a', color: '#fff' }} />
|
| 490 |
-
{selectedBaskets.map((basket, index) => (
|
| 491 |
-
<Line
|
| 492 |
-
key={basket}
|
| 493 |
-
type="monotone"
|
| 494 |
-
dataKey={basket}
|
| 495 |
-
dot={false}
|
| 496 |
-
strokeWidth={2}
|
| 497 |
-
stroke={chartColors[index % chartColors.length]}
|
| 498 |
-
/>
|
| 499 |
-
))}
|
| 500 |
-
</LineChart>
|
| 501 |
-
</ResponsiveContainer>
|
| 502 |
-
</div>
|
| 503 |
-
<div className="grid gap-px bg-line md:grid-cols-2">
|
| 504 |
-
<InflationRank title="FASTEST_RISERS" rows={inflationDetail?.top_risers || []} />
|
| 505 |
-
<InflationRank title="LOWEST_INFLATION" rows={inflationDetail?.top_disinflation || []} />
|
| 506 |
-
</div>
|
| 507 |
-
</section>
|
| 508 |
-
</div>
|
| 509 |
-
)}
|
| 510 |
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
<
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
<Tooltip contentStyle={{ background: '#090b0f', border: '1px solid #26313a', color: '#fff' }} />
|
| 546 |
-
<Line type="monotone" dataKey="average_yield_percent" stroke="#00f09a" strokeWidth={2} dot />
|
| 547 |
-
</LineChart>
|
| 548 |
-
</ResponsiveContainer>
|
| 549 |
-
</div>
|
| 550 |
-
</section>
|
| 551 |
-
<DurationTable rows={activeDayDurationRows} />
|
| 552 |
-
</div>
|
| 553 |
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
|
|
|
|
|
|
|
|
|
| 591 |
</div>
|
| 592 |
);
|
| 593 |
};
|
|
|
|
| 10 |
useGetSecondaryBondDailyDetailQuery,
|
| 11 |
useGetSecondaryBondsQuery,
|
| 12 |
} from '../store/api/economyApi';
|
| 13 |
+
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
| 14 |
import {
|
| 15 |
Search,
|
| 16 |
TrendingUp,
|
|
|
|
| 39 |
|
| 40 |
export default function Market({ initialTab = 'stocks' }: { initialTab?: AssetTab }) {
|
| 41 |
const [searchParams, setSearchParams] = useSearchParams();
|
| 42 |
+
const { economyPage } = useParams();
|
| 43 |
const initialSearch = searchParams.get('search') || '';
|
| 44 |
const [activeTab, setActiveTab] = useState<AssetTab>(initialTab);
|
| 45 |
const [searchQuery, setSearchQuery] = useState(initialSearch);
|
|
|
|
| 207 |
|
| 208 |
{activeTab === 'economy' ? (
|
| 209 |
<EconomyPanel
|
| 210 |
+
routePage={economyPage}
|
| 211 |
intelligence={intelligence}
|
| 212 |
alerts={filteredAlerts}
|
| 213 |
secondaryBonds={filteredSecondaryBonds}
|
|
|
|
| 310 |
};
|
| 311 |
|
| 312 |
type EconomyPanelProps = {
|
| 313 |
+
routePage?: string;
|
| 314 |
intelligence: any | null;
|
| 315 |
alerts: any[];
|
| 316 |
secondaryBonds: any[];
|
|
|
|
| 356 |
const formatDuration = (value: any) => String(value || 'UNKNOWN');
|
| 357 |
|
| 358 |
const EconomyPanel: React.FC<EconomyPanelProps> = ({
|
| 359 |
+
routePage,
|
| 360 |
intelligence,
|
| 361 |
alerts,
|
| 362 |
secondaryBonds,
|
|
|
|
| 366 |
isLoading,
|
| 367 |
onRefresh,
|
| 368 |
}) => {
|
| 369 |
+
const navigate = useNavigate();
|
| 370 |
const [selectedBondDay, setSelectedBondDay] = React.useState<string | null>(null);
|
| 371 |
const topOpportunities = alerts.filter((alert) => alert.severity === 'opportunity').slice(0, 4);
|
| 372 |
const cautions = alerts.filter((alert) => alert.severity !== 'opportunity').slice(0, 4);
|
|
|
|
| 392 |
{ key: 'bonds', label: 'BONDS', icon: <Landmark className="h-3.5 w-3.5" /> },
|
| 393 |
{ key: 'nbs', label: 'NBS_DATA', icon: <FileText className="h-3.5 w-3.5" /> },
|
| 394 |
];
|
| 395 |
+
const validRoutePages = new Set<EconomySubPage>(['overview', 'inflation', 'bonds', 'nbs']);
|
| 396 |
+
const subPage = validRoutePages.has(routePage as EconomySubPage) ? routePage as EconomySubPage : 'overview';
|
| 397 |
+
const openSubPage = (page: EconomySubPage) => {
|
| 398 |
+
void navigate(page === 'overview' ? '/economy' : `/economy/${page}`);
|
| 399 |
+
};
|
| 400 |
+
const pageIndex = subPages.findIndex((item) => item.key === subPage);
|
| 401 |
+
const pageVariants = {
|
| 402 |
+
enter: { opacity: 0, x: 36 },
|
| 403 |
+
center: { opacity: 1, x: 0 },
|
| 404 |
+
exit: { opacity: 0, x: -28 },
|
| 405 |
+
};
|
| 406 |
|
| 407 |
return (
|
| 408 |
<div className="space-y-4">
|
|
|
|
| 429 |
<button
|
| 430 |
key={item.key}
|
| 431 |
type="button"
|
| 432 |
+
onClick={() => openSubPage(item.key)}
|
| 433 |
className={`flex items-center justify-center gap-2 px-4 py-3 text-[10px] mono font-bold transition-all ${subPage === item.key ? 'bg-accent text-bg' : 'bg-bg-card text-text-dim hover:text-white'}`}
|
| 434 |
>
|
| 435 |
{item.icon}
|
|
|
|
| 473 |
</div>
|
| 474 |
</section>
|
| 475 |
|
| 476 |
+
<div className="relative overflow-hidden">
|
| 477 |
+
<AnimatePresence mode="wait" custom={pageIndex}>
|
| 478 |
+
<motion.div
|
| 479 |
+
key={subPage}
|
| 480 |
+
custom={pageIndex}
|
| 481 |
+
variants={pageVariants}
|
| 482 |
+
initial="enter"
|
| 483 |
+
animate="center"
|
| 484 |
+
exit="exit"
|
| 485 |
+
transition={{ duration: 0.22, ease: 'easeOut' }}
|
| 486 |
+
className="space-y-4"
|
| 487 |
+
>
|
| 488 |
+
{subPage === 'overview' && (
|
| 489 |
+
<>
|
| 490 |
+
<div className="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
|
| 491 |
+
<SecondaryBondSnapshotTable secondaryBonds={secondaryBonds} isLoading={isLoading} />
|
| 492 |
+
<section className="space-y-4">
|
| 493 |
+
<AlertList title="GOOD_DEALS" alerts={topOpportunities} emptyLabel="NO_GOOD_DEAL_ALERTS" />
|
| 494 |
+
<AlertList title="STRANGE_OR_EXPENSIVE" alerts={cautions} emptyLabel="NO_CAUTION_ALERTS" />
|
| 495 |
+
</section>
|
| 496 |
+
</div>
|
| 497 |
+
<SourceDocuments documents={documents} />
|
| 498 |
+
</>
|
| 499 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
|
| 501 |
+
{subPage === 'inflation' && (
|
| 502 |
+
<section className="border border-line bg-bg-card">
|
| 503 |
+
<PanelHeader icon={<TrendingUp className="h-4 w-4" />} title="INFLATION_BY_BASKET" />
|
| 504 |
+
<div className="grid gap-px bg-line md:grid-cols-3">
|
| 505 |
+
<MetricCell label="LATEST_MONTH" value={inflationDetail?.latest_date || '--'} />
|
| 506 |
+
<MetricCell label="BASKETS_TRACKED" value={(inflationDetail?.baskets?.length || 0).toString()} />
|
| 507 |
+
<MetricCell label="SERIES_POINTS" value={(inflationSeries.length || 0).toString()} />
|
| 508 |
+
</div>
|
| 509 |
+
<div className="h-[360px] px-5 py-4">
|
| 510 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 511 |
+
<LineChart data={inflationChartRows}>
|
| 512 |
+
<CartesianGrid stroke="#24303a" strokeDasharray="3 3" />
|
| 513 |
+
<XAxis dataKey="date" stroke="#8b95a1" tick={{ fontSize: 10 }} />
|
| 514 |
+
<YAxis stroke="#8b95a1" tick={{ fontSize: 10 }} tickFormatter={(value) => `${value}%`} />
|
| 515 |
+
<Tooltip contentStyle={{ background: '#090b0f', border: '1px solid #26313a', color: '#fff' }} />
|
| 516 |
+
{selectedBaskets.map((basket, index) => (
|
| 517 |
+
<Line
|
| 518 |
+
key={basket}
|
| 519 |
+
type="monotone"
|
| 520 |
+
dataKey={basket}
|
| 521 |
+
dot={false}
|
| 522 |
+
strokeWidth={2}
|
| 523 |
+
stroke={chartColors[index % chartColors.length]}
|
| 524 |
+
/>
|
| 525 |
+
))}
|
| 526 |
+
</LineChart>
|
| 527 |
+
</ResponsiveContainer>
|
| 528 |
+
</div>
|
| 529 |
+
<div className="grid gap-px bg-line md:grid-cols-2">
|
| 530 |
+
<InflationRank title="FASTEST_RISERS" rows={inflationDetail?.top_risers || []} />
|
| 531 |
+
<InflationRank title="LOWEST_INFLATION" rows={inflationDetail?.top_disinflation || []} />
|
| 532 |
+
</div>
|
| 533 |
+
</section>
|
| 534 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
|
| 536 |
+
{subPage === 'bonds' && (
|
| 537 |
+
<>
|
| 538 |
+
<section className="border border-line bg-bg-card">
|
| 539 |
+
<PanelHeader icon={<Landmark className="h-4 w-4" />} title="ECONOMIC_BONDS_DAILY" />
|
| 540 |
+
<div className="flex gap-px overflow-x-auto bg-line border-b border-line">
|
| 541 |
+
{bondDays.map((day: any) => (
|
| 542 |
+
<button
|
| 543 |
+
key={day.trade_date}
|
| 544 |
+
type="button"
|
| 545 |
+
onClick={() => setSelectedBondDay(day.trade_date)}
|
| 546 |
+
className={`min-w-[150px] px-4 py-3 text-left ${activeBondDay === day.trade_date ? 'bg-accent text-bg' : 'bg-bg-card text-text-dim hover:text-white'}`}
|
| 547 |
+
>
|
| 548 |
+
<p className="text-[10px] mono font-bold">{day.trade_date}</p>
|
| 549 |
+
<p className="mt-1 text-[9px] mono">{day.trade_count} trades</p>
|
| 550 |
+
</button>
|
| 551 |
+
))}
|
| 552 |
+
</div>
|
| 553 |
+
<div className="grid gap-px bg-line md:grid-cols-4">
|
| 554 |
+
<MetricCell label="ACTIVE_DAY" value={activeBondDay || '--'} />
|
| 555 |
+
<MetricCell label="DAY_TRADES" value={activeTrades.length.toString()} />
|
| 556 |
+
<MetricCell label="AVG_PRICE" value={formatPercent(bondDays.find((d: any) => d.trade_date === activeBondDay)?.average_price_percent)} />
|
| 557 |
+
<MetricCell label="AVG_YIELD" value={formatPercent(bondDays.find((d: any) => d.trade_date === activeBondDay)?.average_yield_percent)} />
|
| 558 |
+
</div>
|
| 559 |
+
</section>
|
| 560 |
+
|
| 561 |
+
<div className="grid gap-4 xl:grid-cols-2">
|
| 562 |
+
<section className="border border-line bg-bg-card">
|
| 563 |
+
<PanelHeader icon={<Activity className="h-4 w-4" />} title="YIELD_CURVE_BY_DURATION" />
|
| 564 |
+
<div className="h-[320px] px-5 py-4">
|
| 565 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 566 |
+
<LineChart data={yieldCurveRows}>
|
| 567 |
+
<CartesianGrid stroke="#24303a" strokeDasharray="3 3" />
|
| 568 |
+
<XAxis dataKey="duration_bucket" stroke="#8b95a1" tick={{ fontSize: 10 }} />
|
| 569 |
+
<YAxis stroke="#8b95a1" tick={{ fontSize: 10 }} tickFormatter={(value) => `${value}%`} />
|
| 570 |
+
<Tooltip contentStyle={{ background: '#090b0f', border: '1px solid #26313a', color: '#fff' }} />
|
| 571 |
+
<Line type="monotone" dataKey="average_yield_percent" stroke="#00f09a" strokeWidth={2} dot />
|
| 572 |
+
</LineChart>
|
| 573 |
+
</ResponsiveContainer>
|
| 574 |
+
</div>
|
| 575 |
+
</section>
|
| 576 |
+
<DurationTable rows={activeDayDurationRows} />
|
| 577 |
+
</div>
|
| 578 |
+
|
| 579 |
+
<div className="grid gap-4 xl:grid-cols-[1fr_0.9fr]">
|
| 580 |
+
<BondTradesTable rows={activeTrades} />
|
| 581 |
+
<AnomalyList rows={activeAnomalies} />
|
| 582 |
+
</div>
|
| 583 |
+
</>
|
| 584 |
+
)}
|
| 585 |
|
| 586 |
+
{subPage === 'nbs' && (
|
| 587 |
+
<>
|
| 588 |
+
<SourceDocuments documents={documents.filter((doc) => String(doc.source || '').toLowerCase() === 'nbs')} />
|
| 589 |
+
<section className="border border-line bg-bg-card">
|
| 590 |
+
<PanelHeader icon={<FileText className="h-4 w-4" />} title="NBS_BASKET_LATEST_VALUES" />
|
| 591 |
+
<div className="overflow-x-auto">
|
| 592 |
+
<table className="w-full min-w-[760px] text-left">
|
| 593 |
+
<thead className="border-b border-line bg-black/30">
|
| 594 |
+
<tr>
|
| 595 |
+
<HeaderCell>BASKET</HeaderCell>
|
| 596 |
+
<HeaderCell align="right">INFLATION</HeaderCell>
|
| 597 |
+
<HeaderCell align="right">INDEX</HeaderCell>
|
| 598 |
+
<HeaderCell align="right">WEIGHT</HeaderCell>
|
| 599 |
+
</tr>
|
| 600 |
+
</thead>
|
| 601 |
+
<tbody className="divide-y divide-line">
|
| 602 |
+
{(inflationDetail?.latest || []).slice(0, 24).map((row: any) => (
|
| 603 |
+
<tr key={`${row.basket}-${row.date}`}>
|
| 604 |
+
<BodyCell strong>{row.basket}</BodyCell>
|
| 605 |
+
<BodyCell align="right">{formatPercent(row.inflation_rate)}</BodyCell>
|
| 606 |
+
<BodyCell align="right">{row.index_value != null ? Number(row.index_value).toFixed(2) : '--'}</BodyCell>
|
| 607 |
+
<BodyCell align="right">{row.weight != null ? Number(row.weight).toFixed(2) : '--'}</BodyCell>
|
| 608 |
+
</tr>
|
| 609 |
+
))}
|
| 610 |
+
</tbody>
|
| 611 |
+
</table>
|
| 612 |
+
</div>
|
| 613 |
+
</section>
|
| 614 |
+
</>
|
| 615 |
+
)}
|
| 616 |
+
</motion.div>
|
| 617 |
+
</AnimatePresence>
|
| 618 |
+
</div>
|
| 619 |
</div>
|
| 620 |
);
|
| 621 |
};
|