| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- import { useState, useEffect, useRef } from 'react';
- import { useTranslation } from 'react-i18next';
- import { Calendar, Clock, Hand, Power, Layers, Code, ListOrdered } from 'lucide-react';
- import type { ScheduleOptionsProps, ScheduleType } from './types';
- import {
- formatDateInput,
- formatTimeInput,
- parseDateInput,
- parseTimeInput,
- getDatePlaceholder,
- getTimePlaceholder,
- toDateTimeLocalValue,
- type DateFormat,
- type TimeFormat,
- } from '../../utils/date';
- /**
- * Schedule options component for queue items.
- * Includes schedule type (ASAP/Queue/Schedule), datetime picker,
- * and options for require previous success and auto power off.
- */
- export function ScheduleOptionsPanel({
- options,
- onChange,
- dateFormat = 'system',
- timeFormat = 'system',
- canControlPrinter = true,
- showStagger = false,
- printerCount = 0,
- hasGcodeSnippets = false,
- }: ScheduleOptionsProps) {
- const { t } = useTranslation();
- const [dateValue, setDateValue] = useState('');
- const [timeValue, setTimeValue] = useState('');
- const [isDateValid, setIsDateValid] = useState(true);
- const [isTimeValid, setIsTimeValid] = useState(true);
- const hiddenInputRef = useRef<HTMLInputElement>(null);
- const isInitializedRef = useRef(false);
- // Initialize or sync from options.scheduledTime
- useEffect(() => {
- if (options.scheduleType !== 'scheduled') {
- isInitializedRef.current = false;
- return;
- }
- // Initialize with default time (now + 1 hour) or from existing value
- if (!isInitializedRef.current) {
- isInitializedRef.current = true;
- let date: Date;
- if (options.scheduledTime) {
- date = new Date(options.scheduledTime);
- if (isNaN(date.getTime())) {
- date = new Date();
- date.setHours(date.getHours() + 1, 0, 0, 0);
- }
- } else {
- date = new Date();
- date.setHours(date.getHours() + 1, 0, 0, 0);
- // Set initial value
- onChange({ ...options, scheduledTime: toDateTimeLocalValue(date) });
- }
- setDateValue(formatDateInput(date, dateFormat as DateFormat));
- setTimeValue(formatTimeInput(date, timeFormat as TimeFormat));
- setIsDateValid(true);
- setIsTimeValid(true);
- }
- }, [options.scheduleType, options.scheduledTime, dateFormat, timeFormat, onChange, options]);
- const handleScheduleTypeChange = (scheduleType: ScheduleType) => {
- onChange({
- ...options,
- scheduleType,
- requireManualStart: scheduleType === 'queue' ? options.requireManualStart : false,
- });
- };
- const updateScheduledTime = (newDateValue: string, newTimeValue: string) => {
- const parsedDate = parseDateInput(newDateValue, dateFormat as DateFormat);
- const parsedTime = parseTimeInput(newTimeValue);
- setIsDateValid(!!parsedDate);
- setIsTimeValid(!!parsedTime);
- if (parsedDate && parsedTime) {
- parsedDate.setHours(parsedTime.hours, parsedTime.minutes, 0, 0);
- const now = new Date();
- if (parsedDate > now) {
- onChange({ ...options, scheduledTime: toDateTimeLocalValue(parsedDate) });
- }
- }
- };
- const handleDateChange = (value: string) => {
- setDateValue(value);
- updateScheduledTime(value, timeValue);
- };
- const handleTimeChange = (value: string) => {
- setTimeValue(value);
- updateScheduledTime(dateValue, value);
- };
- // Handle calendar picker selection
- const handleCalendarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- const value = e.target.value;
- if (value) {
- const date = new Date(value);
- if (!isNaN(date.getTime())) {
- setDateValue(formatDateInput(date, dateFormat as DateFormat));
- setTimeValue(formatTimeInput(date, timeFormat as TimeFormat));
- setIsDateValid(true);
- setIsTimeValid(true);
- onChange({ ...options, scheduledTime: value });
- }
- }
- };
- const openCalendar = () => {
- hiddenInputRef.current?.showPicker();
- };
- return (
- <div className="space-y-4">
- {/* Schedule type */}
- <div>
- <label className="block text-sm text-bambu-gray mb-2">{t('printModal.whenToPrint')}</label>
- <div className="flex gap-2">
- <button
- type="button"
- className={`flex-1 px-2 py-2 rounded-lg border text-sm flex items-center justify-center gap-1.5 transition-colors ${
- options.scheduleType === 'asap'
- ? 'bg-bambu-green border-bambu-green text-white'
- : 'bg-bambu-dark border-bambu-dark-tertiary text-bambu-gray hover:text-white'
- }`}
- onClick={() => handleScheduleTypeChange('asap')}
- >
- <Clock className="w-4 h-4" />
- {t('printModal.asap')}
- </button>
- <button
- type="button"
- className={`flex-1 px-2 py-2 rounded-lg border text-sm flex items-center justify-center gap-1.5 transition-colors ${
- options.scheduleType === 'queue'
- ? 'bg-bambu-green border-bambu-green text-white'
- : 'bg-bambu-dark border-bambu-dark-tertiary text-bambu-gray hover:text-white'
- }`}
- onClick={() => handleScheduleTypeChange('queue')}
- >
- <ListOrdered className="w-4 h-4" />
- {t('printModal.queue')}
- </button>
- <button
- type="button"
- className={`flex-1 px-2 py-2 rounded-lg border text-sm flex items-center justify-center gap-1.5 transition-colors ${
- options.scheduleType === 'scheduled'
- ? 'bg-bambu-green border-bambu-green text-white'
- : 'bg-bambu-dark border-bambu-dark-tertiary text-bambu-gray hover:text-white'
- }`}
- onClick={() => handleScheduleTypeChange('scheduled')}
- >
- <Calendar className="w-4 h-4" />
- {t('printModal.schedule')}
- </button>
- </div>
- </div>
- {/* Scheduled time input */}
- {options.scheduleType === 'scheduled' && (
- <div>
- <label className="block text-sm text-bambu-gray mb-1">{t('printModal.dateTime')}</label>
- <div className="flex gap-2">
- {/* Date input */}
- <div className="flex-1 relative">
- <input
- type="text"
- className={`w-full px-3 py-2 pr-10 bg-bambu-dark border rounded-lg text-white focus:outline-none ${
- isDateValid
- ? 'border-bambu-dark-tertiary focus:border-bambu-green'
- : 'border-red-500'
- }`}
- value={dateValue}
- onChange={(e) => handleDateChange(e.target.value)}
- placeholder={getDatePlaceholder(dateFormat as DateFormat)}
- />
- <button
- type="button"
- onClick={openCalendar}
- className="absolute right-2 top-1/2 -translate-y-1/2 text-bambu-gray hover:text-white"
- title={t('printModal.openCalendar')}
- >
- <Calendar className="w-4 h-4" />
- </button>
- {/* Hidden datetime-local anchored here so the native picker opens near the date field */}
- <input
- ref={hiddenInputRef}
- type="datetime-local"
- className="absolute top-0 left-0 w-0 h-0 opacity-0 pointer-events-none"
- value={options.scheduledTime}
- onChange={handleCalendarChange}
- tabIndex={-1}
- />
- </div>
- {/* Time input */}
- <div className="w-32">
- <input
- type="text"
- className={`w-full px-3 py-2 bg-bambu-dark border rounded-lg text-white focus:outline-none ${
- isTimeValid
- ? 'border-bambu-dark-tertiary focus:border-bambu-green'
- : 'border-red-500'
- }`}
- value={timeValue}
- onChange={(e) => handleTimeChange(e.target.value)}
- placeholder={getTimePlaceholder(timeFormat as TimeFormat)}
- />
- </div>
- </div>
- {(!isDateValid || !isTimeValid) && (
- <p className="mt-1 text-xs text-red-700 dark:text-red-400">
- {t('printModal.invalidDateTime')}
- </p>
- )}
- </div>
- )}
- {/* Manual start */}
- {options.scheduleType === 'queue' && (
- <div className="flex items-center gap-2">
- <input
- type="checkbox"
- id="requireManualStart"
- checked={options.requireManualStart}
- onChange={(e) => onChange({ ...options, requireManualStart: e.target.checked })}
- className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
- />
- <label htmlFor="requireManualStart" className="text-sm flex items-center gap-1 text-bambu-gray">
- <Hand className="w-3.5 h-3.5" />
- {t('printModal.requireManualStart')}
- </label>
- </div>
- )}
- {/* Require previous success */}
- <div className="flex items-center gap-2">
- <input
- type="checkbox"
- id="requirePrevious"
- checked={options.requirePreviousSuccess}
- onChange={(e) => onChange({ ...options, requirePreviousSuccess: e.target.checked })}
- className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
- />
- <label htmlFor="requirePrevious" className="text-sm text-bambu-gray">
- {t('printModal.requirePreviousSuccess')}
- </label>
- </div>
- {/* Auto power off */}
- <div className="flex items-center gap-2">
- <input
- type="checkbox"
- id="autoOffAfter"
- checked={options.autoOffAfter}
- onChange={(e) => onChange({ ...options, autoOffAfter: e.target.checked })}
- disabled={!canControlPrinter}
- className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green disabled:opacity-50"
- />
- <label htmlFor="autoOffAfter" className={`text-sm flex items-center gap-1 ${canControlPrinter ? 'text-bambu-gray' : 'text-bambu-gray/50'}`}>
- <Power className="w-3.5 h-3.5" />
- {t('printModal.autoOffAfter')}
- </label>
- </div>
- {/* G-code injection */}
- {hasGcodeSnippets && (
- <div className="flex items-center gap-2">
- <input
- type="checkbox"
- id="gcodeInjection"
- checked={options.gcodeInjection}
- onChange={(e) => onChange({ ...options, gcodeInjection: e.target.checked })}
- className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
- />
- <label htmlFor="gcodeInjection" className="text-sm flex items-center gap-1 text-bambu-gray">
- <Code className="w-3.5 h-3.5" />
- {t('printModal.gcodeInjection', 'Inject auto-print G-code')}
- </label>
- </div>
- )}
- {/* Stagger start */}
- {showStagger && options.scheduleType !== 'queue' && (
- <div className="space-y-3">
- <div className="flex items-center gap-2">
- <input
- type="checkbox"
- id="staggerEnabled"
- checked={options.staggerEnabled}
- onChange={(e) => onChange({ ...options, staggerEnabled: e.target.checked })}
- className="rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
- />
- <label htmlFor="staggerEnabled" className="text-sm flex items-center gap-1 text-bambu-gray">
- <Layers className="w-3.5 h-3.5" />
- {t('printModal.staggerPrinterStarts', 'Stagger printer starts')}
- </label>
- </div>
- {options.staggerEnabled && (
- <div className="ml-6 space-y-3">
- <div className="flex gap-3">
- <div className="flex-1">
- <label className="block text-xs text-bambu-gray mb-1">{t('printModal.staggerGroupSize', 'Group size')}</label>
- <input
- type="number"
- min={1}
- max={printerCount}
- value={options.staggerGroupSize}
- onChange={(e) => onChange({ ...options, staggerGroupSize: Math.max(1, parseInt(e.target.value) || 1) })}
- className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:outline-none focus:border-bambu-green"
- />
- </div>
- <div className="flex-1">
- <label className="block text-xs text-bambu-gray mb-1">{t('printModal.staggerInterval', 'Interval (min)')}</label>
- <input
- type="number"
- min={1}
- max={60}
- value={options.staggerIntervalMinutes}
- onChange={(e) => onChange({ ...options, staggerIntervalMinutes: Math.max(1, parseInt(e.target.value) || 1) })}
- className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:outline-none focus:border-bambu-green"
- />
- </div>
- </div>
- {printerCount > 0 && (() => {
- const groupCount = Math.ceil(printerCount / options.staggerGroupSize);
- const lastGroupSize = printerCount % options.staggerGroupSize;
- const totalMinutes = (groupCount - 1) * options.staggerIntervalMinutes;
- return (
- <p className="text-xs text-bambu-gray">
- {t('printModal.staggerPreview', '{{printers}} printers → {{groups}} groups of {{size}}, starting every {{interval}} min', {
- printers: printerCount,
- groups: groupCount,
- size: options.staggerGroupSize,
- interval: options.staggerIntervalMinutes,
- })}
- {lastGroupSize !== 0 && options.staggerGroupSize < printerCount
- ? ` (${t('printModal.staggerLastGroup', 'last group: {{count}}', { count: lastGroupSize })})`
- : ''}
- {groupCount > 1
- ? ` (${t('printModal.staggerTotal', 'total: {{minutes}} min', { minutes: totalMinutes })})`
- : ''}
- </p>
- );
- })()}
- </div>
- )}
- </div>
- )}
- {/* Help text */}
- <p className="text-xs text-bambu-gray">
- {options.scheduleType === 'asap'
- ? t('printModal.helpAsap')
- : options.scheduleType === 'scheduled'
- ? t('printModal.helpSchedule')
- : t('printModal.helpQueue')}
- </p>
- </div>
- );
- }
|