class ScheduleWeekdayOption { final int backendValue; final String shortLabel; final String fullLabel; const ScheduleWeekdayOption({ required this.backendValue, required this.shortLabel, required this.fullLabel, }); } const scheduleWeekdayOptions = [ ScheduleWeekdayOption( backendValue: 1, shortLabel: 'Пн', fullLabel: 'понедельник', ), ScheduleWeekdayOption( backendValue: 2, shortLabel: 'Вт', fullLabel: 'вторник', ), ScheduleWeekdayOption(backendValue: 3, shortLabel: 'Ср', fullLabel: 'среда'), ScheduleWeekdayOption( backendValue: 4, shortLabel: 'Чт', fullLabel: 'четверг', ), ScheduleWeekdayOption( backendValue: 5, shortLabel: 'Пт', fullLabel: 'пятница', ), ScheduleWeekdayOption( backendValue: 6, shortLabel: 'Сб', fullLabel: 'суббота', ), ScheduleWeekdayOption( backendValue: 0, shortLabel: 'Вс', fullLabel: 'воскресенье', ), ]; DateTime defaultOnceScheduleTime({DateTime? now}) { final base = now ?? DateTime.now(); final candidate = base.add(const Duration(hours: 4)); final roundedMinute = ((candidate.minute + 4) ~/ 5) * 5; final normalized = DateTime( candidate.year, candidate.month, candidate.day, candidate.hour, roundedMinute, ); return normalized.isAfter(base) ? normalized : normalized.add(const Duration(minutes: 5)); } String? validateOnceSchedule(DateTime? scheduledAt, {DateTime? now}) { if (scheduledAt == null) { return 'Выберите дату и время'; } final threshold = (now ?? DateTime.now()).add(const Duration(minutes: 1)); if (!scheduledAt.isAfter(threshold)) { return 'Нужна дата хотя бы на минуту вперёд'; } return null; } String? validateCronSchedule({ required int? hour, required int? minute, required Set weekdays, }) { if (hour == null || hour < 0 || hour > 23) { return 'Час должен быть от 0 до 23'; } if (minute == null || minute < 0 || minute > 59) { return 'Минута должна быть от 0 до 59'; } if (weekdays.isEmpty) { return 'Выберите хотя бы один день недели'; } return null; } String serializeCronWeekdays(Set weekdays) { if (weekdays.length == scheduleWeekdayOptions.length) { return '*'; } final sorted = weekdays.toList()..sort(); return sorted.join(','); } String describeCronWeekdays(Set weekdays) { if (weekdays.isEmpty) { return 'дни не выбраны'; } if (weekdays.length == scheduleWeekdayOptions.length) { return 'каждый день'; } final weekdaySet = weekdays.toSet(); final selected = scheduleWeekdayOptions .where((option) => weekdaySet.contains(option.backendValue)) .map((option) => option.shortLabel) .toList(); return selected.join(', '); } String describeCronWeekdaysExpression(String? value) { if (value == null || value.isEmpty || value == '*') { return 'каждый день'; } final weekdays = value .split(',') .map((item) => int.tryParse(item.trim())) .whereType() .toSet(); if (weekdays.isEmpty) { return value; } return describeCronWeekdays(weekdays); } String formatLocalScheduleDateTime(DateTime value) { String pad(int number) => number.toString().padLeft(2, '0'); return '${pad(value.day)}.${pad(value.month)}.${value.year} ${pad(value.hour)}:${pad(value.minute)}'; } String formatRunAtLabel(String? value) { if (value == null || value.isEmpty) { return 'время не указано'; } final parsed = DateTime.tryParse(value); if (parsed == null) { return value; } return formatLocalScheduleDateTime(parsed.toLocal()); }