All files / src/pages/analytics/blocks StockPerSupplierDonut.tsx

96.8% Statements 91/94
100% Branches 9/9
50% Functions 1/2
96.8% Lines 91/94

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 951x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 3x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 1x 1x 1x 1x       1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 5x  
/**
 * @file StockPerSupplierDonut.tsx
 * @module pages/analytics/blocks/StockPerSupplierDonut
 *
 * @summary
 * Donut (pie) view of stock share per supplier (quantity-based).
 * Uses /api/analytics/stock-per-supplier which returns totals by supplier.
 *
 * @enterprise
 * - Purely presentational alternative to the bar version.
 * - Gracefully handles empty datasets and long supplier names (legend).
 */
 
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, PieChart, Pie, Tooltip, Legend, Cell } from 'recharts';
import { getStockPerSupplier, type StockPerSupplierPoint } from '../../../api/analytics';
import { useSettings } from '../../../hooks/useSettings';
import { formatNumber } from '../../../utils/formatters';
 
export default function StockPerSupplierDonut() {
  const { t } = useTranslation(['analytics']);
  const muiTheme = useMuiTheme();
  const { userPreferences } = useSettings();
 
  const q = useQuery<StockPerSupplierPoint[]>({
    queryKey: ['analytics', 'stockPerSupplierDonut'],
    queryFn: getStockPerSupplier,
    staleTime: 60_000,
  });
 
  const data = React.useMemo(
    () => (q.data ?? []).map(d => ({ name: d.supplierName, value: d.totalQuantity })),
    [q.data]
  );
 
  const colors = [
    muiTheme.palette.primary.main,
    muiTheme.palette.success.main,
    muiTheme.palette.info.main,
    muiTheme.palette.warning.main,
    muiTheme.palette.error.main,
    muiTheme.palette.secondary.main,
  ];
 
  return (
    <Card>
      <CardContent>
        <Typography variant="subtitle1" sx={{ mb: 1 }}>
          {t('analytics:stockPerSupplier.title')}
        </Typography>
 
        {q.isLoading ? (
          <Skeleton variant="rounded" height={220} />
        ) : data.length === 0 ? (
          <Box sx={{ height: 220, display: 'grid', placeItems: 'center', color: 'text.secondary' }}>
            {t('analytics:stockPerSupplier.empty', 'No supplier data for the current filters.')}
          </Box>
        ) : (
          <Box sx={{ height: 260 }}>
            <ResponsiveContainer width="100%" height="100%">
              <PieChart>
                <Pie
                  data={data}
                  dataKey="value"
                  nameKey="name"
                  innerRadius="55%"
                  outerRadius="80%"
                  paddingAngle={1}
                  isAnimationActive={false}
                >
                  {data.map((_, i) => (
                    <Cell key={`seg-${i}`} fill={colors[i % colors.length]} />
                  ))}
                </Pie>
                <Tooltip
                  formatter={(value: number | string) =>
                    typeof value === 'number'
                      ? formatNumber(value, userPreferences.numberFormat, 0)
                      : value
                  }
                />
                <Legend />
              </PieChart>
            </ResponsiveContainer>
          </Box>
        )}
      </CardContent>
    </Card>
  );
}