SupplierStats.tsx 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. import { useQuery } from '@tanstack/react-query';
  2. import { useTranslation } from 'react-i18next';
  3. import { AlertTriangle, Loader2 } from 'lucide-react';
  4. import { api } from '../api/client';
  5. // Consumption, cost and stock grouped by the purchase-source supplier
  6. // (#2988) — "how much did we run through supplier X" straight from the
  7. // recorded usage history, archived spools included.
  8. interface SupplierStatsProps {
  9. currency: string;
  10. // The dashboard timeframe, the same one every other widget on that page
  11. // honours. It scopes consumption and cost; stock is point-in-time.
  12. dateFrom?: string;
  13. dateTo?: string;
  14. }
  15. function formatGrams(g: number): string {
  16. if (Math.abs(g) >= 1000) return `${(g / 1000).toFixed(2)} kg`;
  17. return `${Math.round(g)} g`;
  18. }
  19. export function SupplierStats({ currency, dateFrom, dateTo }: SupplierStatsProps) {
  20. const { t } = useTranslation();
  21. const { data, isLoading, isError } = useQuery({
  22. queryKey: ['supplier-stats', dateFrom ?? 'all', dateTo ?? 'all'],
  23. queryFn: () => api.getSupplierStats(dateFrom, dateTo),
  24. });
  25. if (isLoading) {
  26. return (
  27. <div className="flex items-center justify-center py-8">
  28. <Loader2 className="w-6 h-6 text-bambu-green animate-spin" />
  29. </div>
  30. );
  31. }
  32. // A failed request is not an empty inventory: reporting a 403 or a 500 as
  33. // "you have not assigned any" tells the user they have not done something
  34. // they have.
  35. if (isError) {
  36. return (
  37. <p className="flex items-center gap-2 text-sm text-red-400 py-4">
  38. <AlertTriangle className="w-4 h-4" />
  39. {t('stats.suppliers.error')}
  40. </p>
  41. );
  42. }
  43. if (!data || data.length === 0) {
  44. return <p className="text-sm text-bambu-gray py-4">{t('stats.suppliers.empty')}</p>;
  45. }
  46. return (
  47. <div className="overflow-x-auto">
  48. <table className="w-full text-sm">
  49. <thead>
  50. <tr className="text-left text-xs text-bambu-gray border-b border-bambu-dark-tertiary">
  51. <th className="py-2 pr-4 font-medium">{t('stats.suppliers.supplier')}</th>
  52. <th className="py-2 pr-4 font-medium text-right">{t('stats.suppliers.spools')}</th>
  53. <th className="py-2 pr-4 font-medium text-right">{t('stats.suppliers.remaining')}</th>
  54. <th className="py-2 pr-4 font-medium text-right">{t('stats.suppliers.consumed')}</th>
  55. <th className="py-2 font-medium text-right">{t('stats.suppliers.cost')}</th>
  56. </tr>
  57. </thead>
  58. <tbody>
  59. {data.map((row) => (
  60. <tr key={row.supplier_id} className="border-b border-bambu-dark-tertiary/50 last:border-b-0">
  61. <td className="py-2 pr-4 text-white font-medium">{row.supplier_name}</td>
  62. <td className="py-2 pr-4 text-bambu-gray text-right">{row.spool_count}</td>
  63. <td className="py-2 pr-4 text-bambu-gray text-right">{formatGrams(row.remaining_g)}</td>
  64. <td className="py-2 pr-4 text-bambu-gray text-right">{formatGrams(row.consumed_g)}</td>
  65. <td className="py-2 text-bambu-gray text-right">
  66. {currency} {row.cost.toFixed(2)}
  67. </td>
  68. </tr>
  69. ))}
  70. </tbody>
  71. </table>
  72. </div>
  73. );
  74. }