740 lines
24 KiB
Dart
740 lines
24 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
||
import '../app/error_message.dart';
|
||
import '../app/load_state.dart';
|
||
import '../features/schedules/schedule_form_logic.dart';
|
||
import '../models/schedule_task.dart';
|
||
import '../providers/providers.dart';
|
||
import '../widgets/load_error_view.dart';
|
||
|
||
class SchedulesScreen extends ConsumerStatefulWidget {
|
||
const SchedulesScreen({super.key});
|
||
|
||
@override
|
||
ConsumerState<SchedulesScreen> createState() => _SchedulesScreenState();
|
||
}
|
||
|
||
class _SchedulesScreenState extends ConsumerState<SchedulesScreen> {
|
||
final Set<String> _cancellingJobIds = <String>{};
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_load();
|
||
}
|
||
|
||
Future<void> _load() async {
|
||
await ref.read(tasksProvider.notifier).load();
|
||
}
|
||
|
||
Future<void> _cancelTask(ScheduleTask task) async {
|
||
final confirmed = await showDialog<bool>(
|
||
context: context,
|
||
builder: (ctx) => AlertDialog(
|
||
title: const Text('Отменить расписание?'),
|
||
content: Text(
|
||
task.isCron
|
||
? 'Повтор для "${task.targetId}" будет удалён.'
|
||
: 'Одноразовый запуск для "${task.targetId}" будет удалён.',
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(ctx).pop(false),
|
||
child: const Text('Нет'),
|
||
),
|
||
TextButton(
|
||
onPressed: () => Navigator.of(ctx).pop(true),
|
||
child: const Text(
|
||
'Удалить',
|
||
style: TextStyle(color: Colors.redAccent),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
|
||
if (confirmed != true) {
|
||
return;
|
||
}
|
||
|
||
setState(() => _cancellingJobIds.add(task.jobId));
|
||
try {
|
||
await ref.read(tasksProvider.notifier).cancel(task.jobId);
|
||
if (!mounted) return;
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text('Расписание удалено'),
|
||
duration: Duration(seconds: 1),
|
||
),
|
||
);
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(
|
||
'Не удалось удалить расписание: ${describeLoadError(e)}',
|
||
),
|
||
),
|
||
);
|
||
} finally {
|
||
if (mounted) {
|
||
setState(() => _cancellingJobIds.remove(task.jobId));
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> _showAddDialog() async {
|
||
await showModalBottomSheet<void>(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
backgroundColor: const Color(0xFF1E1E1E),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||
),
|
||
builder: (_) => const _AddScheduleSheet(),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final tasksState = ref.watch(tasksProvider);
|
||
final tasks = tasksState.data;
|
||
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('РАСПИСАНИЯ')),
|
||
body: _buildContent(tasksState, tasks),
|
||
floatingActionButton: FloatingActionButton(
|
||
backgroundColor: Colors.deepOrange,
|
||
onPressed: _showAddDialog,
|
||
child: const Icon(Icons.add),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildContent(
|
||
LoadState<List<ScheduleTask>> tasksState,
|
||
List<ScheduleTask> tasks,
|
||
) {
|
||
if ((tasksState.isIdle || tasksState.isLoading) && tasks.isEmpty) {
|
||
return const Center(
|
||
child: CircularProgressIndicator(color: Colors.deepOrange),
|
||
);
|
||
}
|
||
|
||
if (tasksState.hasError && tasks.isEmpty) {
|
||
return LoadErrorView(
|
||
title: 'Не удалось загрузить расписания',
|
||
message: tasksState.errorMessage,
|
||
icon: Icons.schedule,
|
||
onRetry: _load,
|
||
);
|
||
}
|
||
|
||
if (tasks.isEmpty) {
|
||
return RefreshIndicator(
|
||
color: Colors.deepOrange,
|
||
onRefresh: _load,
|
||
child: ListView(
|
||
physics: const AlwaysScrollableScrollPhysics(),
|
||
padding: const EdgeInsets.all(24),
|
||
children: const [
|
||
SizedBox(height: 120),
|
||
Icon(Icons.schedule, size: 64, color: Colors.white24),
|
||
SizedBox(height: 16),
|
||
Text(
|
||
'Пока нет активных расписаний',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(color: Colors.white54, fontSize: 16),
|
||
),
|
||
SizedBox(height: 8),
|
||
Text(
|
||
'Добавьте таймер или повтор для нужной группы.',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(color: Colors.white30),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
final hasStatusHeader = tasksState.isLoading || tasksState.hasError;
|
||
final statusHeaderCount = hasStatusHeader ? 1 : 0;
|
||
|
||
return RefreshIndicator(
|
||
color: Colors.deepOrange,
|
||
onRefresh: _load,
|
||
child: ListView.builder(
|
||
physics: const AlwaysScrollableScrollPhysics(),
|
||
padding: const EdgeInsets.all(12),
|
||
itemCount: tasks.length + statusHeaderCount,
|
||
itemBuilder: (context, index) {
|
||
if (hasStatusHeader && index == 0) {
|
||
if (tasksState.isLoading) {
|
||
return const Padding(
|
||
padding: EdgeInsets.only(bottom: 12),
|
||
child: LinearProgressIndicator(color: Colors.deepOrange),
|
||
);
|
||
}
|
||
|
||
return LoadErrorBanner(
|
||
title: 'Не удалось обновить расписания',
|
||
message: tasksState.errorMessage,
|
||
onRetry: _load,
|
||
);
|
||
}
|
||
|
||
final task = tasks[index - statusHeaderCount];
|
||
final busy = _cancellingJobIds.contains(task.jobId);
|
||
return _TaskCard(
|
||
task: task,
|
||
busy: busy,
|
||
onDelete: busy ? null : () => _cancelTask(task),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _TaskCard extends StatelessWidget {
|
||
final ScheduleTask task;
|
||
final bool busy;
|
||
final VoidCallback? onDelete;
|
||
|
||
const _TaskCard({
|
||
required this.task,
|
||
required this.busy,
|
||
required this.onDelete,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Card(
|
||
margin: const EdgeInsets.only(bottom: 10),
|
||
child: ListTile(
|
||
leading: Container(
|
||
width: 42,
|
||
height: 42,
|
||
decoration: BoxDecoration(
|
||
color: Colors.deepOrange.withValues(alpha: 0.14),
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Icon(
|
||
task.isCron ? Icons.repeat : Icons.alarm,
|
||
color: Colors.deepOrange,
|
||
),
|
||
),
|
||
title: Text(
|
||
'${task.actionText} группу ${task.targetId}',
|
||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||
),
|
||
subtitle: Padding(
|
||
padding: const EdgeInsets.only(top: 6),
|
||
child: Text(
|
||
_taskDescription(task),
|
||
style: const TextStyle(fontSize: 12, color: Colors.white60),
|
||
),
|
||
),
|
||
trailing: busy
|
||
? const SizedBox(
|
||
width: 20,
|
||
height: 20,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
color: Colors.deepOrange,
|
||
),
|
||
)
|
||
: IconButton(
|
||
icon: const Icon(Icons.delete_outline, color: Colors.redAccent),
|
||
tooltip: 'Удалить',
|
||
onPressed: onDelete,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
String _taskDescription(ScheduleTask task) {
|
||
if (task.isCron) {
|
||
final hour = task.hour?.padLeft(2, '0') ?? '--';
|
||
final minute = task.minute?.padLeft(2, '0') ?? '--';
|
||
final days = describeCronWeekdaysExpression(task.dayOfWeek);
|
||
return 'Повтор: $days в $hour:$minute';
|
||
}
|
||
return 'Один раз: ${formatRunAtLabel(task.runAt)}';
|
||
}
|
||
}
|
||
|
||
class _AddScheduleSheet extends ConsumerStatefulWidget {
|
||
const _AddScheduleSheet();
|
||
|
||
@override
|
||
ConsumerState<_AddScheduleSheet> createState() => _AddScheduleSheetState();
|
||
}
|
||
|
||
class _AddScheduleSheetState extends ConsumerState<_AddScheduleSheet> {
|
||
final _formKey = GlobalKey<FormState>();
|
||
final _onceAtCtrl = TextEditingController();
|
||
final _cronTimeCtrl = TextEditingController();
|
||
|
||
String _type = 'once';
|
||
String? _selectedGroupId;
|
||
bool _targetState = false;
|
||
bool _saving = false;
|
||
DateTime _onceAt = defaultOnceScheduleTime();
|
||
TimeOfDay _cronTime = const TimeOfDay(hour: 22, minute: 0);
|
||
Set<int> _selectedWeekdays = scheduleWeekdayOptions
|
||
.map((option) => option.backendValue)
|
||
.toSet();
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_syncControllers();
|
||
Future<void>.microtask(() async {
|
||
try {
|
||
await ref.read(groupsProvider.notifier).refresh();
|
||
} catch (_) {
|
||
// Ошибка загрузки уже отражается через groupsLoadStateProvider.
|
||
}
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_onceAtCtrl.dispose();
|
||
_cronTimeCtrl.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _syncControllers() {
|
||
_onceAtCtrl.text = formatLocalScheduleDateTime(_onceAt);
|
||
_cronTimeCtrl.text =
|
||
'${_cronTime.hour.toString().padLeft(2, '0')}:${_cronTime.minute.toString().padLeft(2, '0')}';
|
||
}
|
||
|
||
Future<void> _pickOnceDateTime() async {
|
||
final pickedDate = await showDatePicker(
|
||
context: context,
|
||
initialDate: _onceAt,
|
||
firstDate: DateTime.now(),
|
||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||
);
|
||
if (!mounted || pickedDate == null) return;
|
||
|
||
final pickedTime = await showTimePicker(
|
||
context: context,
|
||
initialTime: TimeOfDay.fromDateTime(_onceAt),
|
||
);
|
||
if (!mounted || pickedTime == null) return;
|
||
|
||
setState(() {
|
||
_onceAt = DateTime(
|
||
pickedDate.year,
|
||
pickedDate.month,
|
||
pickedDate.day,
|
||
pickedTime.hour,
|
||
pickedTime.minute,
|
||
);
|
||
_syncControllers();
|
||
});
|
||
}
|
||
|
||
Future<void> _pickCronTime() async {
|
||
final pickedTime = await showTimePicker(
|
||
context: context,
|
||
initialTime: _cronTime,
|
||
);
|
||
if (!mounted || pickedTime == null) return;
|
||
setState(() {
|
||
_cronTime = pickedTime;
|
||
_syncControllers();
|
||
});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final groups = ref.watch(groupsProvider);
|
||
final groupsLoadState = ref.watch(groupsLoadStateProvider);
|
||
final bottomPadding = MediaQuery.of(context).viewInsets.bottom;
|
||
final systemPadding = MediaQuery.of(context).padding.bottom;
|
||
final cronWeekdaysError = _type == 'cron'
|
||
? validateCronSchedule(
|
||
hour: _cronTime.hour,
|
||
minute: _cronTime.minute,
|
||
weekdays: _selectedWeekdays,
|
||
)
|
||
: null;
|
||
|
||
return Padding(
|
||
padding: EdgeInsets.only(
|
||
left: 20,
|
||
right: 20,
|
||
top: 20,
|
||
bottom: bottomPadding + systemPadding + 20,
|
||
),
|
||
child: SingleChildScrollView(
|
||
child: Form(
|
||
key: _formKey,
|
||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const Text(
|
||
'Новое расписание',
|
||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||
),
|
||
const SizedBox(height: 6),
|
||
const Text(
|
||
'Сейчас расписания создаются для групп. Сначала выберите группу, потом задайте таймер или повтор.',
|
||
style: TextStyle(color: Colors.white54),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Row(
|
||
children: [
|
||
ChoiceChip(
|
||
label: const Text('Один раз'),
|
||
selected: _type == 'once',
|
||
selectedColor: Colors.deepOrange,
|
||
onSelected: (_) => setState(() => _type = 'once'),
|
||
),
|
||
const SizedBox(width: 8),
|
||
ChoiceChip(
|
||
label: const Text('Повтор'),
|
||
selected: _type == 'cron',
|
||
selectedColor: Colors.deepOrange,
|
||
onSelected: (_) => setState(() => _type = 'cron'),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
if (groupsLoadState.isLoading && groups.isEmpty)
|
||
const Padding(
|
||
padding: EdgeInsets.only(bottom: 12),
|
||
child: LinearProgressIndicator(color: Colors.deepOrange),
|
||
),
|
||
if (groupsLoadState.hasError && groups.isEmpty)
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 12),
|
||
child: LoadErrorBanner(
|
||
title: 'Не удалось загрузить группы',
|
||
message:
|
||
groupsLoadState.errorMessage ?? 'Неизвестная ошибка',
|
||
onRetry: () => ref.read(groupsProvider.notifier).refresh(),
|
||
),
|
||
),
|
||
DropdownButtonFormField<String>(
|
||
initialValue: _selectedGroupId,
|
||
decoration: const InputDecoration(
|
||
labelText: 'Группа',
|
||
helperText: 'Расписание применяется к одной группе света',
|
||
),
|
||
validator: (value) => value == null ? 'Выберите группу' : null,
|
||
items: groups
|
||
.map(
|
||
(group) => DropdownMenuItem<String>(
|
||
value: group.id,
|
||
child: Text(group.name),
|
||
),
|
||
)
|
||
.toList(),
|
||
onChanged: groups.isEmpty
|
||
? null
|
||
: (value) => setState(() => _selectedGroupId = value),
|
||
),
|
||
if (groups.isEmpty)
|
||
const Padding(
|
||
padding: EdgeInsets.only(top: 8),
|
||
child: Text(
|
||
'Сначала создайте хотя бы одну группу света.',
|
||
style: TextStyle(color: Colors.white38, fontSize: 12),
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
SwitchListTile(
|
||
title: Text(_targetState ? 'Включить свет' : 'Выключить свет'),
|
||
subtitle: const Text('Что должно произойти по расписанию'),
|
||
value: _targetState,
|
||
activeThumbColor: Colors.deepOrange,
|
||
onChanged: (value) => setState(() => _targetState = value),
|
||
contentPadding: EdgeInsets.zero,
|
||
),
|
||
const SizedBox(height: 12),
|
||
if (_type == 'once') ...[
|
||
TextFormField(
|
||
controller: _onceAtCtrl,
|
||
readOnly: true,
|
||
decoration: InputDecoration(
|
||
labelText: 'Дата и время',
|
||
helperText: 'Запустится один раз в выбранный момент',
|
||
suffixIcon: IconButton(
|
||
icon: const Icon(Icons.edit_calendar),
|
||
onPressed: _pickOnceDateTime,
|
||
),
|
||
),
|
||
onTap: _pickOnceDateTime,
|
||
validator: (_) =>
|
||
_type == 'once' ? validateOnceSchedule(_onceAt) : null,
|
||
),
|
||
const SizedBox(height: 8),
|
||
Wrap(
|
||
spacing: 8,
|
||
runSpacing: 8,
|
||
children: [
|
||
_QuickTimeChip(
|
||
label: '+1 ч',
|
||
onTap: () =>
|
||
_applyQuickOnceOffset(const Duration(hours: 1)),
|
||
),
|
||
_QuickTimeChip(
|
||
label: '+4 ч',
|
||
onTap: () =>
|
||
_applyQuickOnceOffset(const Duration(hours: 4)),
|
||
),
|
||
_QuickTimeChip(
|
||
label: 'Завтра 09:00',
|
||
onTap: _applyTomorrowMorningPreset,
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
_ScheduleSummaryCard(
|
||
icon: Icons.alarm,
|
||
text:
|
||
'${_targetState ? 'Включить' : 'Выключить'} группу в ${_onceAtCtrl.text}',
|
||
),
|
||
],
|
||
if (_type == 'cron') ...[
|
||
TextFormField(
|
||
controller: _cronTimeCtrl,
|
||
readOnly: true,
|
||
decoration: InputDecoration(
|
||
labelText: 'Время',
|
||
helperText:
|
||
'Повтор будет выполняться каждый выбранный день',
|
||
suffixIcon: IconButton(
|
||
icon: const Icon(Icons.access_time),
|
||
onPressed: _pickCronTime,
|
||
),
|
||
),
|
||
onTap: _pickCronTime,
|
||
),
|
||
const SizedBox(height: 12),
|
||
const Text(
|
||
'Дни недели',
|
||
style: TextStyle(
|
||
color: Colors.white70,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Wrap(
|
||
spacing: 8,
|
||
runSpacing: 8,
|
||
children: [
|
||
FilterChip(
|
||
label: const Text('Каждый день'),
|
||
selected:
|
||
_selectedWeekdays.length ==
|
||
scheduleWeekdayOptions.length,
|
||
selectedColor: Colors.deepOrange,
|
||
onSelected: (selected) {
|
||
setState(() {
|
||
_selectedWeekdays = selected
|
||
? scheduleWeekdayOptions
|
||
.map((option) => option.backendValue)
|
||
.toSet()
|
||
: <int>{};
|
||
});
|
||
},
|
||
),
|
||
for (final option in scheduleWeekdayOptions)
|
||
FilterChip(
|
||
label: Text(option.shortLabel),
|
||
selected: _selectedWeekdays.contains(
|
||
option.backendValue,
|
||
),
|
||
selectedColor: Colors.deepOrange,
|
||
onSelected: (selected) {
|
||
setState(() {
|
||
if (selected) {
|
||
_selectedWeekdays.add(option.backendValue);
|
||
} else {
|
||
_selectedWeekdays.remove(option.backendValue);
|
||
}
|
||
});
|
||
},
|
||
),
|
||
],
|
||
),
|
||
if (cronWeekdaysError != null && _selectedWeekdays.isEmpty)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 8),
|
||
child: Text(
|
||
cronWeekdaysError,
|
||
style: const TextStyle(
|
||
color: Colors.redAccent,
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
_ScheduleSummaryCard(
|
||
icon: Icons.repeat,
|
||
text:
|
||
'${_targetState ? 'Включать' : 'Выключать'} группу ${describeCronWeekdays(_selectedWeekdays)} в ${_cronTimeCtrl.text}',
|
||
),
|
||
],
|
||
const SizedBox(height: 20),
|
||
SizedBox(
|
||
width: double.infinity,
|
||
height: 48,
|
||
child: ElevatedButton(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.deepOrange,
|
||
foregroundColor: Colors.white,
|
||
),
|
||
onPressed: _saving || groups.isEmpty ? null : _save,
|
||
child: _saving
|
||
? const SizedBox(
|
||
width: 20,
|
||
height: 20,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
color: Colors.white,
|
||
),
|
||
)
|
||
: const Text('СОЗДАТЬ РАСПИСАНИЕ'),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _applyQuickOnceOffset(Duration duration) {
|
||
setState(() {
|
||
_onceAt = DateTime.now().add(duration);
|
||
_syncControllers();
|
||
});
|
||
}
|
||
|
||
void _applyTomorrowMorningPreset() {
|
||
final now = DateTime.now();
|
||
setState(() {
|
||
_onceAt = DateTime(now.year, now.month, now.day + 1, 9);
|
||
_syncControllers();
|
||
});
|
||
}
|
||
|
||
Future<void> _save() async {
|
||
FocusScope.of(context).unfocus();
|
||
if (!_formKey.currentState!.validate()) {
|
||
return;
|
||
}
|
||
|
||
if (_type == 'cron' && _selectedWeekdays.isEmpty) {
|
||
setState(() {});
|
||
return;
|
||
}
|
||
|
||
if (_selectedGroupId == null) {
|
||
return;
|
||
}
|
||
|
||
setState(() => _saving = true);
|
||
try {
|
||
if (_type == 'once') {
|
||
await ref
|
||
.read(tasksProvider.notifier)
|
||
.addOnce(
|
||
targetId: _selectedGroupId!,
|
||
targetState: _targetState,
|
||
runAt: _onceAt.toUtc().toIso8601String(),
|
||
);
|
||
} else {
|
||
await ref
|
||
.read(tasksProvider.notifier)
|
||
.addCron(
|
||
targetId: _selectedGroupId!,
|
||
hour: _cronTime.hour.toString(),
|
||
minute: _cronTime.minute.toString(),
|
||
dayOfWeek: serializeCronWeekdays(_selectedWeekdays),
|
||
targetState: _targetState,
|
||
);
|
||
}
|
||
|
||
if (!mounted) return;
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text('Расписание создано'),
|
||
duration: Duration(seconds: 1),
|
||
),
|
||
);
|
||
Navigator.of(context).pop();
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(
|
||
'Не удалось создать расписание: ${describeLoadError(e)}',
|
||
),
|
||
),
|
||
);
|
||
} finally {
|
||
if (mounted) {
|
||
setState(() => _saving = false);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
class _QuickTimeChip extends StatelessWidget {
|
||
final String label;
|
||
final VoidCallback onTap;
|
||
|
||
const _QuickTimeChip({required this.label, required this.onTap});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return ActionChip(
|
||
label: Text(label),
|
||
onPressed: onTap,
|
||
backgroundColor: Colors.white10,
|
||
side: BorderSide(color: Colors.deepOrange.withValues(alpha: 0.3)),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ScheduleSummaryCard extends StatelessWidget {
|
||
final IconData icon;
|
||
final String text;
|
||
|
||
const _ScheduleSummaryCard({required this.icon, required this.text});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withValues(alpha: 0.04),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: Colors.white10),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Icon(icon, size: 18, color: Colors.deepOrange),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: Text(text, style: const TextStyle(color: Colors.white70)),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|