VirtualPrinterAddDialog.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
  4. import { Loader2, ChevronDown, ArrowRightLeft } from 'lucide-react';
  5. import { api, multiVirtualPrinterApi } from '../api/client';
  6. import { Card, CardContent } from './Card';
  7. import { Button } from './Button';
  8. import { useToast } from '../contexts/ToastContext';
  9. type Mode = 'immediate' | 'review' | 'print_queue' | 'proxy';
  10. const MODE_LABELS: Record<string, string> = {
  11. immediate: 'archive',
  12. review: 'review',
  13. print_queue: 'queue',
  14. proxy: 'proxy',
  15. };
  16. interface VirtualPrinterAddDialogProps {
  17. onClose: () => void;
  18. }
  19. export function VirtualPrinterAddDialog({ onClose }: VirtualPrinterAddDialogProps) {
  20. const { t } = useTranslation();
  21. const queryClient = useQueryClient();
  22. const { showToast } = useToast();
  23. const [name, setName] = useState('');
  24. const [mode, setMode] = useState<Mode>('immediate');
  25. const [targetPrinterId, setTargetPrinterId] = useState<number | null>(null);
  26. const { data: printers } = useQuery({
  27. queryKey: ['printers'],
  28. queryFn: api.getPrinters,
  29. });
  30. const createMutation = useMutation({
  31. mutationFn: () =>
  32. multiVirtualPrinterApi.create({
  33. name: name.trim() || 'Bambuddy',
  34. mode,
  35. target_printer_id: mode === 'proxy' ? (targetPrinterId ?? undefined) : undefined,
  36. }),
  37. onSuccess: () => {
  38. queryClient.invalidateQueries({ queryKey: ['virtual-printers'] });
  39. showToast(t('virtualPrinter.toast.created'));
  40. onClose();
  41. },
  42. onError: (error: Error) => {
  43. showToast(error.message || t('virtualPrinter.toast.failedToCreate'), 'error');
  44. },
  45. });
  46. return (
  47. <div
  48. className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
  49. onClick={onClose}
  50. >
  51. <Card
  52. className="w-full max-w-md"
  53. onClick={(e: React.MouseEvent) => e.stopPropagation()}
  54. >
  55. <CardContent className="p-6 space-y-4">
  56. <h3 className="text-lg font-semibold text-white">{t('virtualPrinter.addDialog.title')}</h3>
  57. {/* Name */}
  58. <div>
  59. <label className="text-sm text-white font-medium block mb-1">{t('virtualPrinter.addDialog.name')}</label>
  60. <input
  61. type="text"
  62. value={name}
  63. onChange={(e) => setName(e.target.value)}
  64. placeholder="Bambuddy"
  65. className="w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-md px-3 py-2 text-white text-sm placeholder-bambu-gray"
  66. autoFocus
  67. />
  68. </div>
  69. {/* Mode */}
  70. <div>
  71. <label className="text-sm text-white font-medium block mb-1">{t('virtualPrinter.mode.title')}</label>
  72. <div className="grid grid-cols-2 gap-2">
  73. {(['immediate', 'review', 'print_queue', 'proxy'] as const).map((m) => (
  74. <button
  75. key={m}
  76. onClick={() => setMode(m)}
  77. className={`p-2 rounded-lg border text-left transition-colors ${
  78. mode === m
  79. ? m === 'proxy'
  80. ? 'border-blue-500 bg-blue-500/10'
  81. : 'border-bambu-green bg-bambu-green/10'
  82. : 'border-bambu-dark-tertiary hover:border-bambu-gray'
  83. }`}
  84. >
  85. <div className="flex items-center gap-1.5 text-white text-xs font-medium">
  86. {m === 'proxy' && <ArrowRightLeft className="w-3 h-3" />}
  87. {t(`virtualPrinter.mode.${MODE_LABELS[m]}`)}
  88. </div>
  89. <div className="text-[10px] text-bambu-gray">
  90. {t(`virtualPrinter.mode.${MODE_LABELS[m]}Desc`)}
  91. </div>
  92. </button>
  93. ))}
  94. </div>
  95. </div>
  96. {/* Target Printer - only for proxy mode */}
  97. {mode === 'proxy' && (
  98. <div>
  99. <label className="text-sm text-white font-medium block mb-1">{t('virtualPrinter.targetPrinter.title')}</label>
  100. <div className="relative">
  101. <select
  102. value={targetPrinterId ?? ''}
  103. onChange={(e) => {
  104. const id = parseInt(e.target.value, 10);
  105. setTargetPrinterId(isNaN(id) ? null : id);
  106. }}
  107. className="w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-md px-3 py-2 text-white text-sm appearance-none cursor-pointer pr-10"
  108. >
  109. <option value="">{t('virtualPrinter.targetPrinter.placeholder')}</option>
  110. {printers?.map((p) => (
  111. <option key={p.id} value={p.id}>{p.name} ({p.ip_address})</option>
  112. ))}
  113. </select>
  114. <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
  115. </div>
  116. </div>
  117. )}
  118. <p className="text-xs text-bambu-gray">
  119. {t('virtualPrinter.addDialog.hint')}
  120. </p>
  121. {/* Actions */}
  122. <div className="flex gap-3 pt-2">
  123. <Button variant="secondary" onClick={onClose} className="flex-1" disabled={createMutation.isPending}>
  124. {t('common.cancel')}
  125. </Button>
  126. <Button
  127. variant="primary"
  128. onClick={() => createMutation.mutate()}
  129. className="flex-1"
  130. disabled={createMutation.isPending}
  131. >
  132. {createMutation.isPending ? (
  133. <Loader2 className="w-4 h-4 animate-spin" />
  134. ) : (
  135. t('virtualPrinter.addDialog.create')
  136. )}
  137. </Button>
  138. </div>
  139. </CardContent>
  140. </Card>
  141. </div>
  142. );
  143. }