VEU.WathBody/3-app/3-web/admin-web/lib/main.dart

174 lines
5.5 KiB
Dart

import 'package:flutter/material.dart';
import 'api.dart';
import 'screens.dart';
/// Админ-консоль «Феникс 6X» (m08): Flutter Web, Material 3,
/// тёмная тема (UI/UX-стандарт). Токен — sessionStorage; 401 → логин.
void main() => runApp(const AdminApp());
class AdminApp extends StatelessWidget {
const AdminApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Феникс 6X — консоль владельца',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
colorSchemeSeed: const Color(0xFF4FC3F7),
useMaterial3: true,
),
home: const RootPage(),
);
}
class RootPage extends StatefulWidget {
const RootPage({super.key});
@override
State<RootPage> createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
int _index = 0;
@override
Widget build(BuildContext context) {
if (!Api.authorized) return LoginPage(onDone: _reload);
const pages = [
AthletesPage(),
WorkoutsPage(),
ProgramsPage(),
MetricsPage(),
PlanPage(),
];
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: _index,
onDestinationSelected: (i) => setState(() => _index = i),
labelType: NavigationRailLabelType.all,
destinations: const [
NavigationRailDestination(
icon: Icon(Icons.people_outline),
selectedIcon: Icon(Icons.people),
label: Text('Атлеты')),
NavigationRailDestination(
icon: Icon(Icons.fitness_center_outlined),
selectedIcon: Icon(Icons.fitness_center),
label: Text('Тренировки')),
NavigationRailDestination(
icon: Icon(Icons.list_alt_outlined),
selectedIcon: Icon(Icons.list_alt),
label: Text('Программы')),
NavigationRailDestination(
icon: Icon(Icons.insights_outlined),
selectedIcon: Icon(Icons.insights),
label: Text('Метрики')),
NavigationRailDestination(
icon: Icon(Icons.event_available_outlined),
selectedIcon: Icon(Icons.event_available),
label: Text('План/факт')),
],
),
const VerticalDivider(width: 1),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8),
child: OutlinedButton(
onPressed: () {
Api.logout();
setState(() {});
},
child: const Text('Выйти'),
),
),
),
Expanded(child: pages[_index]),
],
),
),
],
),
);
}
void _reload() => setState(() {});
}
class LoginPage extends StatefulWidget {
const LoginPage({super.key, required this.onDone});
final VoidCallback onDone;
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _login = TextEditingController();
final _password = TextEditingController();
String? _error;
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: SizedBox(
width: 360,
child: Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Феникс 6X',
style: Theme.of(context).textTheme.headlineMedium),
const Text('консоль владельца'),
const SizedBox(height: 24),
TextField(
controller: _login,
decoration: const InputDecoration(labelText: 'Логин')),
TextField(
controller: _password,
obscureText: true,
onSubmitted: (_) => _submit(),
decoration:
const InputDecoration(labelText: 'Пароль')),
const SizedBox(height: 16),
FilledButton(
onPressed: _submit,
child: const Text('Войти'),
),
if (_error != null)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(_error!,
style: const TextStyle(color: Colors.redAccent)),
),
],
),
),
),
),
),
);
Future<void> _submit() async {
try {
await Api.login(_login.text, _password.text);
widget.onDone();
} on UnauthorizedException {
setState(() => _error = 'Неверный логин или пароль');
} catch (e) {
setState(() => _error = 'Ошибка: $e');
}
}
}