From c21a43190f63e3d7dcee5444f764b14098caa8f1 Mon Sep 17 00:00:00 2001 From: max Date: Thu, 17 Jul 2025 10:09:19 +0300 Subject: [PATCH] Added authorization page and page with a list of organizations --- lib/data/interface/IRepository.dart | 6 ++ lib/data/organization_repository.dart | 50 ++++++++++ lib/main.dart | 11 ++- lib/models/organization.dart | 30 ++++++ lib/providers/organization_ptovider.dart | 45 +++++++++ lib/screens/auth/login_page.dart | 88 +++++++++++++++++ .../detail_organization_page.dart | 88 +++++++++++++++++ .../edit_organization_page.dart | 97 +++++++++++++++++++ .../list_organization_page.dart | 87 +++++++++++++++++ 9 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 lib/data/interface/IRepository.dart create mode 100644 lib/data/organization_repository.dart create mode 100644 lib/models/organization.dart create mode 100644 lib/providers/organization_ptovider.dart create mode 100644 lib/screens/auth/login_page.dart create mode 100644 lib/screens/organization_page/detail_organization_page.dart create mode 100644 lib/screens/organization_page/edit_organization_page.dart create mode 100644 lib/screens/organization_page/list_organization_page.dart diff --git a/lib/data/interface/IRepository.dart b/lib/data/interface/IRepository.dart new file mode 100644 index 0000000..0474077 --- /dev/null +++ b/lib/data/interface/IRepository.dart @@ -0,0 +1,6 @@ +abstract class Repository { + Future> load(); + Future add(T organization); + Future update(T organization); + Future delete(String? id); +} \ No newline at end of file diff --git a/lib/data/organization_repository.dart b/lib/data/organization_repository.dart new file mode 100644 index 0000000..1221a66 --- /dev/null +++ b/lib/data/organization_repository.dart @@ -0,0 +1,50 @@ +import 'package:instruction_app/data/interface/IRepository.dart'; +import 'package:instruction_app/models/organization.dart'; + +class LocalOrganizationRepository implements Repository { + final List _organizations = [ + Organization( + id: '1', + title: 'Про-Эксперт' + ), + Organization( + id: '2', + title:'Лидер' + ), + Organization( + id: '3', + title: 'Авангард' + ), + Organization( + id: '4', + title: 'Галактика' + ) + ]; + + @override + Future> load() async { + return List.of(_organizations); + } + + @override + Future add(Organization organization) async { + // изменить id + _organizations.add(organization); + return organization; + } + + @override + Future update(Organization organization) async { + var index = _organizations.indexWhere((item) => item.id == organization.id); + if (index != -1) { + _organizations[index] = organization; + } + } + + @override + Future delete(String? id) async { + if (id == null) + return; + _organizations.removeWhere((item) => item.id == id); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 29ce80f..9a49c71 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,12 @@ // lib/main.dart import 'package:flutter/material.dart'; +import 'package:instruction_app/data/interface/IRepository.dart'; +import 'package:instruction_app/data/organization_repository.dart'; +import 'package:instruction_app/models/organization.dart'; +import 'package:instruction_app/providers/organization_ptovider.dart'; +import 'package:instruction_app/screens/auth/login_page.dart'; +import 'package:instruction_app/screens/organization_page/list_organization_page.dart'; import 'package:provider/provider.dart'; import 'data/instruction_repository.dart'; import 'providers/instruction_provider.dart'; @@ -11,10 +17,13 @@ void main() { final InstructionRepository repository = LocalInstructionRepository(); //LocalInstructionRepository(); + // final Repository organizationRepository = LocalOrganizationRepository(); + runApp( // "Предоставляем" наш провайдер всему дереву виджетов ChangeNotifierProvider( create: (context) => InstructionProvider(repository), + // create: (context) => OrganizationProvider(organizationRepository), child: const MyApp(), ), ); @@ -31,7 +40,7 @@ class MyApp extends StatelessWidget { colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), - home: const InstructionListPage(), + home: const AuthorizationPage(), ); } } \ No newline at end of file diff --git a/lib/models/organization.dart b/lib/models/organization.dart new file mode 100644 index 0000000..849893c --- /dev/null +++ b/lib/models/organization.dart @@ -0,0 +1,30 @@ +class Organization { + final String? id; + final String title; + + Organization({ + this.id, + required this.title + }); + + Organization copyWith(String? id, String? title) { + return Organization( + id: id ?? this.id, + title: title ?? this.title + ); + } + + factory Organization.fromJson(Map json) { + return Organization( + id: json['id'], + title: json['title'], + ); + } + + Map toJson() { + return { + 'id': id, + 'title': title, + }; + } +} \ No newline at end of file diff --git a/lib/providers/organization_ptovider.dart b/lib/providers/organization_ptovider.dart new file mode 100644 index 0000000..4d353c9 --- /dev/null +++ b/lib/providers/organization_ptovider.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:instruction_app/data/interface/IRepository.dart'; +import 'package:instruction_app/models/organization.dart'; + +class OrganizationProvider extends ChangeNotifier { + final Repository _repository; + + OrganizationProvider(this._repository); + + List _organizations = []; + List get organizations => _organizations; + + bool _isLoading = false; + bool get isLoading => _isLoading; + + Future loadOrganizations() async { + setLoading(false); + _organizations = await _repository.load(); + setLoading(true); + } + + Future addOrganization(String title) async { + final newOrganization = Organization( + title: title + ); + await _repository.add(newOrganization); + await loadOrganizations(); + } + + Future updateOrganization(Organization organization) async { + await _repository.update(organization); + await loadOrganizations(); + } + + Future deleteOrganization(String id) async { + await _repository.delete(id); + _organizations.removeWhere((item) => item.id == id); + notifyListeners(); + } + + void setLoading(bool value){ + _isLoading = value; + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/screens/auth/login_page.dart b/lib/screens/auth/login_page.dart new file mode 100644 index 0000000..07eda3c --- /dev/null +++ b/lib/screens/auth/login_page.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +class AuthorizationPage extends StatefulWidget { + const AuthorizationPage({super.key}); + + @override + State createState() => _AuthorizationPage(); +} + +class _AuthorizationPage extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Авторизация"), + ), + body: Padding( + padding: EdgeInsets.all(80), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FormWidget(), + SizedBox(height: 25,), + TextButton( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(Colors.blue), + foregroundColor: WidgetStateProperty.all(Colors.white), + ), + onPressed: () {}, + child: Text("Войти") + ) + ], + ) + ) + ); + } +} + +class FormWidget extends StatefulWidget { + const FormWidget({super.key}); + + @override + State createState() => _FormWidget(); +} + +class _FormWidget extends State { + final borederRadius = BorderRadius.all(Radius.circular(10)); + bool passwordVisible = false; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + TextField( + decoration: InputDecoration( + prefixIcon: Icon(Icons.account_circle), + border: OutlineInputBorder( + borderRadius: borederRadius + ), + labelText: 'Логин' + ) + ), + SizedBox(height: 25,), + TextField( + obscureText: !passwordVisible, + decoration: InputDecoration( + prefixIcon: Icon(Icons.lock), + border: OutlineInputBorder( + borderRadius: borederRadius + ), + labelText: 'Пароль', + suffixIcon: IconButton( + icon: Icon( + passwordVisible ? Icons.visibility : Icons.visibility_off + ), + onPressed: () { + setState(() { + passwordVisible = !passwordVisible; + }); + } + ) + ) + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/organization_page/detail_organization_page.dart b/lib/screens/organization_page/detail_organization_page.dart new file mode 100644 index 0000000..cc1035d --- /dev/null +++ b/lib/screens/organization_page/detail_organization_page.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:instruction_app/models/organization.dart'; +import 'package:instruction_app/providers/organization_ptovider.dart'; +import 'package:instruction_app/screens/organization_page/edit_organization_page.dart'; +import 'package:provider/provider.dart'; + + +class OrganizationDetailPage extends StatefulWidget { + final Organization initialOrganization; + const OrganizationDetailPage({super.key, required this.initialOrganization}); + + @override + State createState() => _OrganizationDetailPage(); +} + +class _OrganizationDetailPage extends State{ + late Organization editOrganization = widget.initialOrganization; + + void _deleteOrganization(BuildContext context) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Подтверждение операции'), + content: Text("Вы уверены, что хотите удалить организацию \"${editOrganization.title}\"?"), + actions: [ + TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Отмена')), + TextButton( + onPressed: () { + context.read().deleteOrganization(editOrganization.id!); + Navigator.of(ctx).pop(); + Navigator.of(context).pop(); + }, + child: const Text('Удалить', style: TextStyle(color: Colors.red)), + ), + ], + ) + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(editOrganization.title, overflow: TextOverflow.ellipsis), + actions: [ + IconButton( + icon: const Icon(Icons.edit), + tooltip: 'Редактировать', + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (ctx) => OrganizationAddPage(initialOrganization: editOrganization))) + .then((value) => { + setState(() { + if (value != null && value is Organization) + editOrganization = value; + }) + }), + ), + IconButton( + icon: const Icon(Icons.delete), + tooltip: 'Удалить', + onPressed: () => _deleteOrganization(context), + ), + ] + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: ListView( + children: [ + Text('Содержание:', + style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(12), + height: 200, + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(8), + ), + child: const Text('Описание ...', + style: const TextStyle(fontSize: 16, height: 1.5) + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/organization_page/edit_organization_page.dart b/lib/screens/organization_page/edit_organization_page.dart new file mode 100644 index 0000000..d61cc86 --- /dev/null +++ b/lib/screens/organization_page/edit_organization_page.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:instruction_app/models/organization.dart'; +import 'package:instruction_app/providers/organization_ptovider.dart'; +import 'package:provider/provider.dart'; + + +class OrganizationAddPage extends StatefulWidget { + final Organization? initialOrganization; + const OrganizationAddPage({super.key, this.initialOrganization}); + + @override + State createState() => _OrganizationAddPage(); +} + +class _OrganizationAddPage extends State { + final _formKey = GlobalKey(); + late TextEditingController _titleController; + + bool get _isEditing => widget.initialOrganization != null; + + @override + void initState() { + super.initState(); + _titleController = TextEditingController(text: widget.initialOrganization?.title ?? ''); + } + + @override + void dispose() { + _titleController.dispose(); + super.dispose(); + } + + Future _saveForm() async { + if(_formKey.currentState!.validate()) { + final provider = context.read(); + final title = _titleController.text; + final updateOrganization = new Organization( + id: widget.initialOrganization?.id, + title: title + ); + + // showDialog( + // context: context, + // barrierDismissible: false, + // builder: (context) => Center(child: CircularProgressIndicator()) + // ); + + try { + if(_isEditing) { + await provider.updateOrganization(updateOrganization); + } else { + await provider.addOrganization(title); + } + if (mounted) { + Navigator.of(context).pop(updateOrganization); + } + } catch(exeption) { + if(mounted) { + // Navigator.of(context).pop(updateOrganization); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Ошибка сохранения: $exeption')), + ); + } + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_isEditing ? 'Редактировать' : 'Добавление организации'), + actions: [ + IconButton( + tooltip: 'Сохранить', + icon: const Icon(Icons.save), + onPressed: _saveForm + )], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: ListView( + children: [ + TextFormField( + controller: _titleController, + decoration: const InputDecoration(labelText: 'Название', border: OutlineInputBorder()), + validator: (v) => v == null || v.isEmpty ? 'Название не может быть пустым' : null, + ), + ], + ) + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/screens/organization_page/list_organization_page.dart b/lib/screens/organization_page/list_organization_page.dart new file mode 100644 index 0000000..723927a --- /dev/null +++ b/lib/screens/organization_page/list_organization_page.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:instruction_app/providers/organization_ptovider.dart'; +import 'package:instruction_app/screens/organization_page/edit_organization_page.dart'; +import 'package:instruction_app/screens/organization_page/detail_organization_page.dart'; +import 'package:provider/provider.dart'; + +class OrganizationListPage extends StatefulWidget { + const OrganizationListPage({super.key}); + + @override + State createState() => _OrganizationListPage(); +} + +class _OrganizationListPage extends State{ + @override + void initState() { + super.initState(); + Future.microtask(() => + Provider.of(context, listen: false) + .loadOrganizations()); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Список организаций"), + ), + body: Consumer( + builder: (context, provider, child) { + return Column( + children: [ + provider.isLoading ? + _widgetOrganizationList(provider) : + const Expanded(child: Center(child: CircularProgressIndicator())) + ], + ); + } + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + // возможно поменять + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const OrganizationAddPage()), + ); + }, + tooltip: "Добавить организацию", + child: const Icon(Icons.add), + ), + ); + } + + Widget _widgetOrganizationList(OrganizationProvider provider){ + if (!provider.isLoading) { + return const Expanded( + child: Center( + child: Text( + 'Организации не найдены', + textAlign: TextAlign.center, + ), + ), + ); + } + return Expanded( + child: ListView.builder( + itemCount: provider.organizations.length, + itemBuilder: (context, index) { + final organization = provider.organizations[index]; + return Card( + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8 + ), + child: ListTile( + title: Text(organization.title), + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (ctx) => OrganizationDetailPage(initialOrganization: organization), + )); + }, + ), + ); + } + ), + ); + } +} \ No newline at end of file