Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 | /** * @file MovementLineCard.tsx * @module pages/analytics/blocks/MovementLineCard * * @summary * Line chart version of monthly stock movement (A2) with two series: * Stock In vs Stock Out over months in the selected date range. * * @enterprise * - Reuses the same API as the bar version; purely a presentation swap. * - Supplier-aware via props; hook is unconditional and keyed by filters. */ import * as React from 'react'; import { Card, CardContent, Typography, Skeleton, Box } from '@mui/material'; import { useTheme as useMuiTheme } from '@mui/material/styles'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { ResponsiveContainer, LineChart, CartesianGrid, XAxis, YAxis, Tooltip, Legend, Line } from 'recharts'; import { getMonthlyStockMovement, type MonthlyMovement } from '../../../api/analytics'; import { useSettings } from '../../../hooks/useSettings'; import { formatDate, formatNumber } from '../../../utils/formatters'; export type MovementLineCardProps = { from?: string; to?: string; supplierId?: string | null }; export default function MovementLineCard({ from, to, supplierId }: MovementLineCardProps) { const { t } = useTranslation(['analytics']); const muiTheme = useMuiTheme(); const { userPreferences } = useSettings(); const q = useQuery<MonthlyMovement[]>({ queryKey: ['analytics', 'movementLine', from ?? null, to ?? null, supplierId ?? null], queryFn: () => getMonthlyStockMovement({ from, to, supplierId: supplierId ?? undefined }), staleTime: 60_000, }); const data = React.useMemo( () => [...(q.data ?? [])], [q.data] ); const formatDateLabel = React.useCallback( (value: string | number) => { const str = String(value); const formatted = formatDate(str, userPreferences.dateFormat); return formatted || str; }, [userPreferences.dateFormat] ); return ( <Card> <CardContent> <Typography variant="subtitle1" sx={{ mb: 1 }}> {t('analytics:cards.monthlyMovement')} </Typography> {q.isLoading ? ( <Skeleton variant="rounded" height={220} /> ) : data.length === 0 ? ( <Box sx={{ height: 220, display: 'grid', placeItems: 'center', color: 'text.secondary' }}> {t('analytics:cards.noData')} </Box> ) : ( <Box sx={{ height: 260 }}> <ResponsiveContainer width="100%" height="100%"> <LineChart data={data} margin={{ top: 8, right: 16, left: 8, bottom: 8 }}> <CartesianGrid strokeDasharray="3 3" /> <XAxis dataKey="month" tickFormatter={formatDateLabel} /> <YAxis tickFormatter={(value) => formatNumber(Number(value), userPreferences.numberFormat, 0)} /> <Tooltip labelFormatter={(value) => formatDateLabel(value as string)} formatter={(value: number | string) => typeof value === 'number' ? formatNumber(value, userPreferences.numberFormat, 0) : value } /> <Legend /> <Line type="monotone" dataKey="stockIn" name={t('analytics:cards.monthlyMovement') + ' • In'} stroke={muiTheme.palette.success.main} strokeWidth={2} dot={{ r: 2 }} activeDot={{ r: 4 }} isAnimationActive={false} /> <Line type="monotone" dataKey="stockOut" name={t('analytics:cards.monthlyMovement') + ' • Out'} stroke={muiTheme.palette.error.main} strokeWidth={2} dot={{ r: 2 }} activeDot={{ r: 4 }} isAnimationActive={false} /> </LineChart> </ResponsiveContainer> </Box> )} </CardContent> </Card> ); } |