feat: surface admin load errors
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../app/error_message.dart';
|
||||
import '../app/load_state.dart';
|
||||
import '../providers/providers.dart';
|
||||
import '../widgets/load_error_view.dart';
|
||||
|
||||
/// Экран управления расписаниями.
|
||||
/// Показывает все задачи (one-shot и cron), позволяет создавать и удалять.
|
||||
@@ -12,8 +15,6 @@ class SchedulesScreen extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _SchedulesScreenState extends ConsumerState<SchedulesScreen> {
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -22,45 +23,16 @@ class _SchedulesScreenState extends ConsumerState<SchedulesScreen> {
|
||||
|
||||
Future<void> _load() async {
|
||||
await ref.read(tasksProvider.notifier).load();
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tasks = ref.watch(tasksProvider);
|
||||
final tasksState = ref.watch(tasksProvider);
|
||||
final tasks = tasksState.data;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('РАСПИСАНИЯ')),
|
||||
body: _loading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(color: Colors.deepOrange),
|
||||
)
|
||||
: tasks.isEmpty
|
||||
? const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.schedule, size: 64, color: Colors.white24),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Нет активных расписаний',
|
||||
style: TextStyle(color: Colors.white54, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
color: Colors.deepOrange,
|
||||
onRefresh: () => ref.read(tasksProvider.notifier).load(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: tasks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final task = tasks[index];
|
||||
return _TaskCard(task: task);
|
||||
},
|
||||
),
|
||||
),
|
||||
body: _buildContent(tasksState, tasks),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
backgroundColor: Colors.deepOrange,
|
||||
onPressed: () => _showAddDialog(context),
|
||||
@@ -81,6 +53,74 @@ class _SchedulesScreenState extends ConsumerState<SchedulesScreen> {
|
||||
builder: (ctx) => const _AddScheduleSheet(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(
|
||||
LoadState<List<dynamic>> tasksState,
|
||||
List<dynamic> 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 const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.schedule, size: 64, color: Colors.white24),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Нет активных расписаний',
|
||||
style: TextStyle(color: Colors.white54, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 taskIndex = index - statusHeaderCount;
|
||||
return _TaskCard(task: tasks[taskIndex]);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Карточка одной задачи расписания
|
||||
@@ -151,9 +191,20 @@ class _TaskCard extends ConsumerWidget {
|
||||
child: const Text('Нет'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
Navigator.of(ctx).pop();
|
||||
ref.read(tasksProvider.notifier).cancel(jobId);
|
||||
try {
|
||||
await ref.read(tasksProvider.notifier).cancel(jobId);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Ошибка отмены задачи: ${describeLoadError(e)}',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('Да', style: TextStyle(color: Colors.redAccent)),
|
||||
),
|
||||
@@ -363,9 +414,9 @@ class _AddScheduleSheetState extends ConsumerState<_AddScheduleSheet> {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Ошибка: $e')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Ошибка: ${describeLoadError(e)}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user