ProfileDefaultsSettings.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import { useState } from 'react';
  2. import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
  3. import { useTranslation } from 'react-i18next';
  4. import { Loader2, Sparkles, Trash2, Wand2 } from 'lucide-react';
  5. import { api } from '../api/client';
  6. import type { ProfileDefault } from '../api/client';
  7. import { useToast } from '../contexts/ToastContext';
  8. import { Button } from './Button';
  9. import { Card, CardContent, CardHeader } from './Card';
  10. import { ConfirmModal } from './ConfirmModal';
  11. /**
  12. * Settings > Filament > Profile defaults (#1326): what new spools of each
  13. * product start with. A default is saved from a spool's Printers tab ("Save as
  14. * default"), which is also how one is changed: this card lists them, applies
  15. * one to the spools already in stock, and deletes one.
  16. */
  17. export function ProfileDefaultsSettings() {
  18. const { t } = useTranslation();
  19. const { showToast } = useToast();
  20. const queryClient = useQueryClient();
  21. const [deleting, setDeleting] = useState<ProfileDefault | null>(null);
  22. const { data: defaults = [], isLoading } = useQuery({
  23. queryKey: ['profile-defaults'],
  24. queryFn: api.getProfileDefaults,
  25. });
  26. const { data: printers = [] } = useQuery({ queryKey: ['printers'], queryFn: api.getPrinters });
  27. // Applying writes to the spools of the inventory in use.
  28. const { data: spoolmanSettings } = useQuery({
  29. queryKey: ['spoolman-settings'],
  30. queryFn: api.getSpoolmanSettings,
  31. staleTime: 5 * 60 * 1000,
  32. });
  33. const spoolmanMode = spoolmanSettings?.spoolman_enabled === 'true' && !!spoolmanSettings?.spoolman_url;
  34. const printerName = (id: number) => printers.find((p) => p.id === id)?.name ?? `#${id}`;
  35. const applyMutation = useMutation({
  36. mutationFn: (row: ProfileDefault) => api.applyProfileDefault(row.id, spoolmanMode),
  37. onSuccess: async ({ updated }) => {
  38. showToast(t('inventory.profileDefaults.applied', { count: updated }), 'success');
  39. await queryClient.invalidateQueries({ queryKey: ['inventory-spools'] });
  40. await queryClient.invalidateQueries({ queryKey: ['spoolman-inventory-spools'] });
  41. },
  42. onError: () => showToast(t('inventory.profileDefaults.applyFailed'), 'error'),
  43. });
  44. const deleteMutation = useMutation({
  45. mutationFn: (row: ProfileDefault) => api.deleteProfileDefault(row.id),
  46. onSuccess: async () => {
  47. setDeleting(null);
  48. await queryClient.invalidateQueries({ queryKey: ['profile-defaults'] });
  49. },
  50. onError: () => {
  51. setDeleting(null);
  52. showToast(t('inventory.profileDefaults.deleteFailed'), 'error');
  53. },
  54. });
  55. const productOf = (row: ProfileDefault) =>
  56. [row.brand, row.material, row.subtype].filter((part) => part.trim()).join(' ');
  57. return (
  58. <Card id="card-profiledefaults">
  59. <CardHeader>
  60. <h2 className="text-lg font-semibold text-white flex items-center gap-2">
  61. <Sparkles className="w-5 h-5 text-bambu-green" />
  62. {t('inventory.profileDefaults.title')}
  63. </h2>
  64. </CardHeader>
  65. <CardContent className="space-y-3">
  66. <p className="text-sm text-bambu-gray">{t('inventory.profileDefaults.description')}</p>
  67. {isLoading ? (
  68. <div className="flex justify-center py-4">
  69. <Loader2 className="w-5 h-5 animate-spin text-bambu-gray" />
  70. </div>
  71. ) : defaults.length === 0 ? (
  72. <p className="text-sm text-bambu-gray italic">{t('inventory.profileDefaults.empty')}</p>
  73. ) : (
  74. <ul className="space-y-2">
  75. {defaults.map((row) => (
  76. <li key={row.id} className="p-3 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg">
  77. <div className="flex flex-wrap items-start gap-2">
  78. <div className="min-w-0 flex-1">
  79. <p className="text-sm text-white">
  80. {productOf(row)}
  81. <span className="ml-2 text-xs text-bambu-gray">
  82. {row.color_name || t('inventory.profileDefaults.allColours')}
  83. </span>
  84. </p>
  85. <ul className="mt-1 space-y-0.5 text-xs text-bambu-gray">
  86. {row.slicer_filament && (
  87. <li>
  88. {t('inventory.filamentPreset')}: {row.slicer_filament_name || row.slicer_filament}
  89. </li>
  90. )}
  91. {row.presets.map((p) => (
  92. <li key={`${p.printer_model}:${p.nozzle_diameter}`}>
  93. {p.printer_model}
  94. {p.nozzle_diameter ? ` ${p.nozzle_diameter} mm` : ''}: {p.slicer_filament_name || p.slicer_filament}
  95. </li>
  96. ))}
  97. {row.k_profiles.map((k) => (
  98. <li key={`${k.printer_id}:${k.extruder}:${k.nozzle_diameter}`}>
  99. {`${printerName(k.printer_id)} ${k.nozzle_diameter ?? '0.4'} mm: ${k.name || '-'} K=${k.k_value.toFixed(3)}`}
  100. {(k.extruder ?? 0) > 0 ? ` (${t('inventory.leftNozzle')})` : ''}
  101. </li>
  102. ))}
  103. </ul>
  104. </div>
  105. <div className="flex gap-2 shrink-0">
  106. <Button
  107. size="sm"
  108. variant="secondary"
  109. onClick={() => applyMutation.mutate(row)}
  110. // Not before the inventory mode is known: it picks the spools written to.
  111. disabled={applyMutation.isPending || spoolmanSettings === undefined}
  112. title={t('inventory.profileDefaults.applyHelp')}
  113. >
  114. {applyMutation.isPending && applyMutation.variables?.id === row.id ? (
  115. <Loader2 className="w-4 h-4 animate-spin" />
  116. ) : (
  117. <Wand2 className="w-4 h-4" />
  118. )}
  119. {t('inventory.profileDefaults.apply')}
  120. </Button>
  121. <Button
  122. size="sm"
  123. variant="secondary"
  124. onClick={() => setDeleting(row)}
  125. aria-label={t('common.delete')}
  126. >
  127. <Trash2 className="w-4 h-4" />
  128. </Button>
  129. </div>
  130. </div>
  131. </li>
  132. ))}
  133. </ul>
  134. )}
  135. </CardContent>
  136. {deleting && (
  137. <ConfirmModal
  138. title={t('inventory.profileDefaults.deleteTitle')}
  139. message={t('inventory.profileDefaults.deleteMessage', {
  140. product: `${productOf(deleting)} ${deleting.color_name || t('inventory.profileDefaults.allColours')}`,
  141. })}
  142. confirmText={t('common.delete')}
  143. variant="danger"
  144. isLoading={deleteMutation.isPending}
  145. onConfirm={() => deleteMutation.mutate(deleting)}
  146. onCancel={() => setDeleting(null)}
  147. />
  148. )}
  149. </Card>
  150. );
  151. }