feat: polish phase 7 forms and schedules

This commit is contained in:
Artem Kokos
2026-05-01 09:47:08 +07:00
parent 91a494adf5
commit 2fa89f6be0
9 changed files with 1583 additions and 599 deletions

View File

@@ -0,0 +1,147 @@
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>[
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<int> 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<int> weekdays) {
if (weekdays.length == scheduleWeekdayOptions.length) {
return '*';
}
final sorted = weekdays.toList()..sort();
return sorted.join(',');
}
String describeCronWeekdays(Set<int> 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<int>()
.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());
}