AddToQueueModal.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import { useState, useEffect } from 'react';
  2. import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
  3. import { Calendar, Clock, X, AlertCircle, Power } from 'lucide-react';
  4. import { api } from '../api/client';
  5. import type { PrintQueueItemCreate } from '../api/client';
  6. import { Card, CardContent } from './Card';
  7. import { Button } from './Button';
  8. import { useToast } from '../contexts/ToastContext';
  9. interface AddToQueueModalProps {
  10. archiveId: number;
  11. archiveName: string;
  12. onClose: () => void;
  13. }
  14. export function AddToQueueModal({ archiveId, archiveName, onClose }: AddToQueueModalProps) {
  15. const queryClient = useQueryClient();
  16. const { showToast } = useToast();
  17. const [printerId, setPrinterId] = useState<number | null>(null);
  18. const [scheduleType, setScheduleType] = useState<'asap' | 'scheduled'>('asap');
  19. const [scheduledTime, setScheduledTime] = useState('');
  20. const [requirePreviousSuccess, setRequirePreviousSuccess] = useState(false);
  21. const [autoOffAfter, setAutoOffAfter] = useState(false);
  22. const { data: printers } = useQuery({
  23. queryKey: ['printers'],
  24. queryFn: () => api.getPrinters(),
  25. });
  26. // Set default printer if only one available
  27. useEffect(() => {
  28. if (printers?.length === 1 && !printerId) {
  29. setPrinterId(printers[0].id);
  30. }
  31. }, [printers, printerId]);
  32. // Close on Escape key
  33. useEffect(() => {
  34. const handleKeyDown = (e: KeyboardEvent) => {
  35. if (e.key === 'Escape') onClose();
  36. };
  37. window.addEventListener('keydown', handleKeyDown);
  38. return () => window.removeEventListener('keydown', handleKeyDown);
  39. }, [onClose]);
  40. const addMutation = useMutation({
  41. mutationFn: (data: PrintQueueItemCreate) => api.addToQueue(data),
  42. onSuccess: () => {
  43. queryClient.invalidateQueries({ queryKey: ['queue'] });
  44. showToast('Added to print queue');
  45. onClose();
  46. },
  47. onError: (error: Error) => {
  48. showToast(error.message || 'Failed to add to queue', 'error');
  49. },
  50. });
  51. const handleSubmit = (e: React.FormEvent) => {
  52. e.preventDefault();
  53. if (!printerId) {
  54. showToast('Please select a printer', 'error');
  55. return;
  56. }
  57. const data: PrintQueueItemCreate = {
  58. printer_id: printerId,
  59. archive_id: archiveId,
  60. require_previous_success: requirePreviousSuccess,
  61. auto_off_after: autoOffAfter,
  62. };
  63. if (scheduleType === 'scheduled' && scheduledTime) {
  64. data.scheduled_time = new Date(scheduledTime).toISOString();
  65. }
  66. addMutation.mutate(data);
  67. };
  68. // Get minimum datetime (now + 1 minute)
  69. const getMinDateTime = () => {
  70. const now = new Date();
  71. now.setMinutes(now.getMinutes() + 1);
  72. return now.toISOString().slice(0, 16);
  73. };
  74. return (
  75. <div
  76. className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
  77. onClick={onClose}
  78. >
  79. <Card className="w-full max-w-md" onClick={(e) => e.stopPropagation()}>
  80. <CardContent className="p-0">
  81. {/* Header */}
  82. <div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
  83. <div className="flex items-center gap-2">
  84. <Calendar className="w-5 h-5 text-bambu-green" />
  85. <h2 className="text-xl font-semibold text-white">Schedule Print</h2>
  86. </div>
  87. <button
  88. onClick={onClose}
  89. className="text-bambu-gray hover:text-white transition-colors"
  90. >
  91. <X className="w-5 h-5" />
  92. </button>
  93. </div>
  94. {/* Form */}
  95. <form onSubmit={handleSubmit} className="p-4 space-y-4">
  96. {/* Archive name */}
  97. <div>
  98. <label className="block text-sm text-bambu-gray mb-1">Print Job</label>
  99. <p className="text-white font-medium truncate">{archiveName}</p>
  100. </div>
  101. {/* Printer selection */}
  102. <div>
  103. <label className="block text-sm text-bambu-gray mb-1">Printer</label>
  104. {printers?.length === 0 ? (
  105. <div className="flex items-center gap-2 text-red-400 text-sm">
  106. <AlertCircle className="w-4 h-4" />
  107. No printers configured
  108. </div>
  109. ) : (
  110. <select
  111. className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
  112. value={printerId || ''}
  113. onChange={(e) => setPrinterId(e.target.value ? Number(e.target.value) : null)}
  114. required
  115. >
  116. <option value="">Select printer...</option>
  117. {printers?.map((p) => (
  118. <option key={p.id} value={p.id}>{p.name}</option>
  119. ))}
  120. </select>
  121. )}
  122. </div>
  123. {/* Schedule type */}
  124. <div>
  125. <label className="block text-sm text-bambu-gray mb-2">When to print</label>
  126. <div className="flex gap-2">
  127. <button
  128. type="button"
  129. className={`flex-1 px-3 py-2 rounded-lg border text-sm flex items-center justify-center gap-2 transition-colors ${
  130. scheduleType === 'asap'
  131. ? 'bg-bambu-green border-bambu-green text-white'
  132. : 'bg-bambu-dark border-bambu-dark-tertiary text-bambu-gray hover:text-white'
  133. }`}
  134. onClick={() => setScheduleType('asap')}
  135. >
  136. <Clock className="w-4 h-4" />
  137. ASAP (when idle)
  138. </button>
  139. <button
  140. type="button"
  141. className={`flex-1 px-3 py-2 rounded-lg border text-sm flex items-center justify-center gap-2 transition-colors ${
  142. scheduleType === 'scheduled'
  143. ? 'bg-bambu-green border-bambu-green text-white'
  144. : 'bg-bambu-dark border-bambu-dark-tertiary text-bambu-gray hover:text-white'
  145. }`}
  146. onClick={() => setScheduleType('scheduled')}
  147. >
  148. <Calendar className="w-4 h-4" />
  149. Scheduled
  150. </button>
  151. </div>
  152. </div>
  153. {/* Scheduled time input */}
  154. {scheduleType === 'scheduled' && (
  155. <div>
  156. <label className="block text-sm text-bambu-gray mb-1">Date & Time</label>
  157. <input
  158. type="datetime-local"
  159. className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
  160. value={scheduledTime}
  161. onChange={(e) => setScheduledTime(e.target.value)}
  162. min={getMinDateTime()}
  163. required
  164. />
  165. </div>
  166. )}
  167. {/* Require previous success */}
  168. <div className="flex items-center gap-2">
  169. <input
  170. type="checkbox"
  171. id="requirePrevious"
  172. checked={requirePreviousSuccess}
  173. onChange={(e) => setRequirePreviousSuccess(e.target.checked)}
  174. className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
  175. />
  176. <label htmlFor="requirePrevious" className="text-sm text-bambu-gray">
  177. Only start if previous print succeeded
  178. </label>
  179. </div>
  180. {/* Auto power off */}
  181. <div className="flex items-center gap-2">
  182. <input
  183. type="checkbox"
  184. id="autoOffAfter"
  185. checked={autoOffAfter}
  186. onChange={(e) => setAutoOffAfter(e.target.checked)}
  187. className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
  188. />
  189. <label htmlFor="autoOffAfter" className="text-sm text-bambu-gray flex items-center gap-1">
  190. <Power className="w-3.5 h-3.5" />
  191. Power off printer when done
  192. </label>
  193. </div>
  194. {/* Help text */}
  195. <p className="text-xs text-bambu-gray">
  196. {scheduleType === 'asap'
  197. ? 'Print will start as soon as the printer is idle.'
  198. : 'Print will start at the scheduled time if the printer is idle. If busy, it will wait until the printer becomes available.'}
  199. </p>
  200. {/* Actions */}
  201. <div className="flex gap-3 pt-2">
  202. <Button type="button" variant="secondary" onClick={onClose} className="flex-1">
  203. Cancel
  204. </Button>
  205. <Button
  206. type="submit"
  207. className="flex-1"
  208. disabled={addMutation.isPending || !printerId || printers?.length === 0}
  209. >
  210. {addMutation.isPending ? 'Adding...' : 'Add to Queue'}
  211. </Button>
  212. </div>
  213. </form>
  214. </CardContent>
  215. </Card>
  216. </div>
  217. );
  218. }