diff --git a/aurora/desktop/com.example.counters.desktop b/aurora/desktop/su.markow.desktop similarity index 59% rename from aurora/desktop/com.example.counters.desktop rename to aurora/desktop/su.markow.desktop index b602369..d207e97 100644 --- a/aurora/desktop/com.example.counters.desktop +++ b/aurora/desktop/su.markow.desktop @@ -2,11 +2,11 @@ Type=Application Name=counters Comment=A new Flutter project. -Icon=com.example.counters -Exec=/usr/bin/com.example.counters +Icon=su.markow.counters +Exec=/usr/bin/su.markow.counters X-Nemo-Application-Type=silica-qt5 [X-Application] -Permissions= -OrganizationName=com.example +Permissions=UserDirs +OrganizationName=su.markow ApplicationName=counters diff --git a/aurora/rpm/com.example.counters.spec b/aurora/rpm/su.markow.spec similarity index 91% rename from aurora/rpm/com.example.counters.spec rename to aurora/rpm/su.markow.spec index 3347d81..579c360 100644 --- a/aurora/rpm/com.example.counters.spec +++ b/aurora/rpm/su.markow.spec @@ -1,7 +1,7 @@ %global __provides_exclude_from ^%{_datadir}/%{name}/lib/.*$ %global __requires_exclude ^lib(dconf|flutter-embedder|maliit-glib|.+_platform_plugin)\\.so.*$ -Name: com.example.counters +Name: su.markow.counters Summary: A new Flutter project. Version: 0.1.0 Release: 1 @@ -11,6 +11,7 @@ Source0: %{name}-%{version}.tar.zst BuildRequires: cmake BuildRequires: ninja BuildRequires: pkgconfig(flutter-embedder) +BuildRequires: pkgconfig(sqlite3) %description %{summary}. diff --git a/lib/address.dart b/lib/address.dart index b28559c..bf20293 100644 --- a/lib/address.dart +++ b/lib/address.dart @@ -1,28 +1,22 @@ -// To parse this JSON data, do -// -// final address = addressFromJson(jsonString); - -import 'dart:convert'; - -Address addressFromJson(String str) => Address.fromJson(json.decode(str)); - -String addressToJson(Address data) => json.encode(data.toJson()); - class Address { - String streetName; - String comments; + final int? id; + final String streetName; + final String comments; Address({ + this.id, required this.streetName, required this.comments, }); - factory Address.fromJson(Map json) => Address( + factory Address.fromMap(Map json) => Address( + id: json["id"], streetName: json["street_name"], comments: json["comments"], ); - Map toJson() => { + Map toMap() => { + "id": id, "street_name": streetName, "comments": comments, }; diff --git a/lib/counters.dart b/lib/counters.dart new file mode 100644 index 0000000..f3940c1 --- /dev/null +++ b/lib/counters.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +enum CounterType { + coldWater(_coldWaterId, Colors.blue, Icons.water_drop), + hotWater(_hotWaterId, Colors.red, Icons.water_drop), + electrisity1rates(_electrisity1rateId, Colors.lightBlue, Icons.tungsten), + electrisity2rates(_electrisity2rateId, Colors.lightBlue, Icons.tungsten), + electrisity3rates(_electrisity3rateId, Colors.lightBlue, Icons.tungsten), + gas(_gasId, Colors.blueAccent, Icons.local_fire_department); + + const CounterType(this.id, this.color, this.icon); + + static const int _coldWaterId = 0; + static const int _hotWaterId = 1; + static const int _gasId = 2; + static const int _electrisity1rateId = 3; + static const int _electrisity2rateId = 4; + static const int _electrisity3rateId = 5; + final int id; + final Color color; + final IconData icon; + + String getLabel(BuildContext context) { + switch (id) { + case _coldWaterId: + return AppLocalizations.of(context)!.cold_water_type_str; + case _hotWaterId: + return AppLocalizations.of(context)!.hot_water_type_str; + case _electrisity1rateId: + return AppLocalizations.of(context)!.electrisity1_type_str; + case _electrisity2rateId: + return AppLocalizations.of(context)!.electrisity2_type_str; + case _electrisity3rateId: + return AppLocalizations.of(context)!.electrisity3_type_str; + case _gasId: + return AppLocalizations.of(context)!.gas_type_str; + default: + } + + return AppLocalizations.of(context)!.unknown_type_str; + } + + CounterUnits getUnits() { + if (id < _electrisity1rateId) { + return CounterUnits.m3; + } + + return CounterUnits.kVt; + } +} + +enum CounterUnits { + m3(0), + kVt(1); + + const CounterUnits(this.id); + final int id; + + String getLabel(BuildContext context) { + switch (id) { + case 0: + return AppLocalizations.of(context)!.m3; + case 1: + return AppLocalizations.of(context)!.kVt; + } + + return ''; + } +} + +class Counter { + final int? id; + final int addressId; + final CounterType counterType; + final String name; + final double? value; + + Counter( + {this.id, + required this.addressId, + required this.counterType, + required this.name, + this.value}); + + factory Counter.fromMap(Map json) { + return Counter( + id: json["id"], + addressId: json["address_id"], + counterType: CounterType.values + .firstWhere((element) => element.id == json["counter_type"]), + name: json["name"], + value: json["value"].toDouble()); + } + + Map toMap() => { + "id": id, + "address_id": addressId, + "counter_type": counterType.id, + "name": name + }; +} diff --git a/lib/counters_page.dart b/lib/counters_page.dart index 448fffe..3adb323 100644 --- a/lib/counters_page.dart +++ b/lib/counters_page.dart @@ -1,9 +1,25 @@ +import 'dart:ffi'; + +import 'package:counters/address.dart'; +import 'package:counters/datbase.dart'; +import 'package:counters/new_counter_page.dart'; +import 'package:counters/record_action_pane.dart'; +import 'package:counters/update_counter_page.dart'; +import 'package:counters/values_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; + +import 'counters.dart'; class CountersPage extends StatefulWidget { - const CountersPage({super.key, required this.title, required this.comments}); + const CountersPage( + {super.key, + required this.title, + required this.comments, + required this.address}); + final Address address; final String title; final String comments; @@ -14,12 +30,13 @@ class CountersPage extends StatefulWidget { class _CountersPageState extends State { @override Widget build(BuildContext context) { + final counters = DBProvider.db.getCountersOfAddress(widget.address); return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( widget.title, @@ -32,14 +49,29 @@ class _CountersPageState extends State { ], ), ), - body: const SizedBox(), + body: Center( + child: FutureBuilder( + future: counters, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!.isNotEmpty) { + return CountersListView( + counters: snapshot.data!, + onUpdated: () { + setState(() {}); + }); + } + + return Text(AppLocalizations.of(context)!.empty_counters_list); + }, + )), floatingActionButton: FloatingActionButton( onPressed: () { Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const NewCounterPage())) - .then((_) => setState(() {})); + context, + MaterialPageRoute( + builder: (context) => NewCounterPage( + addressId: widget.address.id!, + ))).then((_) => setState(() {})); }, tooltip: AppLocalizations.of(context)!.add_new_counter_tooltip, child: const Icon(Icons.add), @@ -47,16 +79,100 @@ class _CountersPageState extends State { } } -class NewCounterPage extends StatelessWidget { - const NewCounterPage({super.key}); +class CountersListView extends StatefulWidget { + const CountersListView({ + super.key, + required this.counters, + this.onUpdated, + }); + + final List counters; + final VoidCallback? onUpdated; + @override + State createState() => _CountersListViewState(); +} + +class _CountersListViewState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text(AppLocalizations.of(context)!.new_counter_title)), - body: const SizedBox(), + return ListView.builder( + itemCount: widget.counters.length, + itemBuilder: (context, index) { + return Slidable( + key: const ValueKey(0), + endActionPane: RecordActionPane(onEdit: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => UpdateCounterPage( + counter: widget.counters[index], + ))).then((_) => widget.onUpdated?.call()); + }, onDelete: () { + DBProvider.db.deleteCounter(widget.counters[index]).then((value) { + widget.onUpdated?.call(); + }); + }).build(context), + child: SizedBox( + height: 60, + child: TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ValuesPage( + counter: widget.counters[index], + ))).then((value) => widget.onUpdated?.call()); + }, + style: + TextButton.styleFrom(shape: const BeveledRectangleBorder()), + child: Row( + children: [ + Icon(widget.counters[index].counterType.icon, + color: widget.counters[index].counterType.color), + Padding( + padding: const EdgeInsets.all(8.0), + child: Align( + alignment: Alignment.centerLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.counters[index].counterType + .getLabel(context), + style: Theme.of(context).textTheme.bodyLarge, + ), + Row( + children: [ + Text( + widget.counters[index].name.isNotEmpty + ? "${widget.counters[index].name} " + : "", + style: Theme.of(context).textTheme.bodySmall, + ), + Text( + '${widget.counters[index].value} ', + //style: Theme.of(context).textTheme.bodySmall, + ), + Text( + widget.counters[index].counterType + .getUnits() + .getLabel(context), + style: Theme.of(context).textTheme.bodySmall, + ) + ], + ) + ], + ), + ), + ), + ], + ), + ), + ), + ); + }, ); } } diff --git a/lib/datbase.dart b/lib/datbase.dart index ba930ea..a165df5 100644 --- a/lib/datbase.dart +++ b/lib/datbase.dart @@ -2,6 +2,8 @@ import 'dart:ffi'; import 'dart:io'; import 'package:counters/address.dart'; +import 'package:counters/counters.dart'; +import 'package:counters/value.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite/sqflite.dart'; @@ -9,6 +11,9 @@ import 'package:sqflite/sqflite.dart'; class DBProvider { static const int dbVersion = 1; static const String addressTableName = 'address$dbVersion'; + static const String countersTableName = 'counters$dbVersion'; + static const String ratesTableName = 'rates$dbVersion'; + static const String valuesTableName = 'values$dbVersion'; DBProvider._(); static final DBProvider db = DBProvider._(); @@ -20,23 +25,59 @@ class DBProvider { return _database!; } + createTables(Database db) async { + await db.execute("CREATE TABLE IF NOT EXISTS $addressTableName (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "street_name TEXT," + "comments TEXT" + ")"); + + await db.execute("CREATE TABLE IF NOT EXISTS $countersTableName (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "address_id INTEGER," + "counter_type INTEGER," + "name TEXT," + "FOREIGN KEY (address_id) REFERENCES $addressTableName (id)" + ")"); + + await db.execute("CREATE TABLE IF NOT EXISTS $ratesTableName (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "counter_id INTEGER," + "rate INTEGER," + "name TEXT," + "FOREIGN KEY (counter_id) REFERENCES $countersTableName (id)" + ")"); + + await db.execute("CREATE TABLE IF NOT EXISTS $valuesTableName (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "counter_id INTEGER," + "date INTEGER," + "rate1_id INTEGER," + "value1 REAL default 0.0," + "rate2_id INTEGER," + "value2 REAL default 0.0," + "rate3_id INTEGER," + "value3 REAL default 0.0," + "FOREIGN KEY (counter_id) REFERENCES $countersTableName (id)," + "FOREIGN KEY (rate1_id) REFERENCES $ratesTableName (id)," + "FOREIGN KEY (rate2_id) REFERENCES $ratesTableName (id)," + "FOREIGN KEY (rate3_id) REFERENCES $ratesTableName (id)" + ")"); + } + initDB() async { - Directory documentsDirectory = await getApplicationDocumentsDirectory(); + Directory documentsDirectory = await getApplicationSupportDirectory(); String path = join(documentsDirectory.path, "counters.db"); print(path); - return await openDatabase(path, version: dbVersion, onOpen: (db) {}, + return await openDatabase(path, version: dbVersion, onOpen: createTables, onCreate: (Database db, int version) async { - await db.execute("CREATE TABLE $addressTableName (" - "id INTEGER PRIMARY KEY," - "street_name TEXT," - "comments TEXT" - ")"); + await createTables(db); }); } Future newAddress(Address newAddress) async { final db = await database; - var res = await db.insert(addressTableName, newAddress.toJson()); + var res = await db.insert(addressTableName, newAddress.toMap()); return res; } @@ -44,14 +85,103 @@ class DBProvider { final db = await database; var res = await db.query(addressTableName, where: "id = ?", whereArgs: [id]); - return res.isNotEmpty ? Address.fromJson(res.first) : Null; + return res.isNotEmpty ? Address.fromMap(res.first) : Null; } Future> getAllAddress() async { final db = await database; var res = await db.query(addressTableName); List
list = - res.isNotEmpty ? res.map((c) => Address.fromJson(c)).toList() : []; + res.isNotEmpty ? res.map((c) => Address.fromMap(c)).toList() : []; return list; } + + Future deleteAddress(Address address) async { + final db = await database; + await deleteValuesByAddress(address); + await deleteCountersByAddress(address); + await db.delete(addressTableName, where: "id = ?", whereArgs: [address.id]); + } + + Future updateAddress(Address address) async { + final db = await database; + var res = await db.update(addressTableName, address.toMap(), + where: "id = ?", whereArgs: [address.id]); + return res; + } + + Future> getCountersOfAddress(Address address) async { + final db = await database; + var sql = + 'select $countersTableName.id, $countersTableName.address_id, $countersTableName.counter_type, $countersTableName.name, 0 as value ' + ' from $countersTableName ' + ' where address_id=${address.id} and $countersTableName.id not in (select counter_id from $valuesTableName) ' + ' union ' + 'select $countersTableName.id, $countersTableName.address_id, $countersTableName.counter_type, $countersTableName.name, sum($valuesTableName.value1) + sum($valuesTableName.value2) + sum($valuesTableName.value3) as value ' + ' from $countersTableName ' + ' INNER JOIN $valuesTableName ON $valuesTableName.counter_id = $countersTableName.id ' + ' where address_id=${address.id} ' + ' group by counter_type'; + var res = await db.rawQuery(sql, []); + + print(res); + List list = + res.isNotEmpty ? res.map((c) => Counter.fromMap(c)).toList() : []; + return list; + } + + Future newCounter(Counter newCounter) async { + final db = await database; + var res = await db.insert(countersTableName, newCounter.toMap()); + return res; + } + + Future updateCounter(Counter counter) async { + final db = await database; + var res = await db.update(countersTableName, counter.toMap(), + where: "id = ?", whereArgs: [counter.id]); + return res; + } + + Future deleteCounter(Counter counter) async { + final db = await database; + await deleteValuesByCounter(counter); + await db + .delete(countersTableName, where: "id = ?", whereArgs: [counter.id]); + } + + deleteCountersByAddress(Address address) async { + final db = await database; + await db.delete(countersTableName, + where: 'address_id = ?', whereArgs: [address.id]); + } + + Future> getValuesOfCounter(Counter counter) async { + final db = await database; + var res = await db.query(valuesTableName, + where: "counter_id = ?", whereArgs: [counter.id]); + List list = + res.isNotEmpty ? res.map((v) => Value.fromMap(v)).toList() : []; + return list; + } + + Future newValue(Value newValue) async { + final db = await database; + var res = await db.insert(valuesTableName, newValue.toMap()); + return res; + } + + deleteValuesByAddress(Address address) async { + final db = await database; + await db.delete(valuesTableName, + where: + 'counter_id in (select id from $countersTableName where address_id = ?)', + whereArgs: [address.id]); + } + + deleteValuesByCounter(Counter counter) async { + final db = await database; + await db.delete(valuesTableName, + where: 'counter_id = ?', whereArgs: [counter.id]); + } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a8762b7..f32968b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -10,5 +10,22 @@ "enter_your_address_comments": "Enter your comment", "add_new_address_button": "Add", "new_counter_title": "New counter", - "add_new_counter_tooltip": "New counter" + "add_new_counter_tooltip": "New counter", + "empty_counters_list": "List of counters is empty", + "enter_counter_name": "Enter counter name", + "cold_water_type_str": "Cold water", + "hot_water_type_str": "Hot water", + "electrisity1_type_str": "Electrisity (1 rate)", + "electrisity2_type_str": "Electrisity (2 rates)", + "electrisity3_type_str": "Electrisity (3 reates)", + "gas_type_str": "Gas", + "unknown_type_str": "Unknown counter type", + "choose_type_of_counter": "choose type of counter", + "empty_values_list": "List of values is empty", + "new_value_title": "Values", + "delete": "Delete", + "edit": "Edit", + "kVt": "kVt", + "m3": "m3", + "values": "Values" } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 001b924..9fa315b 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -7,5 +7,22 @@ "enter_your_address_comments": "Добавте комментарий (опционально)", "add_new_address_button": "Добавить", "new_counter_title": "Новый счетчик", - "add_new_counter_tooltip": "Новый счетчик" + "add_new_counter_tooltip": "Новый счетчик", + "empty_counters_list": "Список счетчиков пуст", + "enter_counter_name": "Введите название счетчика", + "cold_water_type_str": "Холодная вода", + "hot_water_type_str": "Горячая вода", + "electrisity1_type_str": "Электричество (1 тариф)", + "electrisity2_type_str": "Электричество (2 тариф)", + "electrisity3_type_str": "Электричество (3 тариф)", + "gas_type_str": "Газ", + "unknown_type_str": "Неизвестный тип счетчка", + "choose_type_of_counter": "Выберите тип счетчика", + "empty_values_list": "Список показаний пуст", + "new_value_title": "Показания счетчика", + "delete": "Удалить", + "edit": "Изменить", + "kVt": "кВт", + "m3": "куб.м", + "values": "Показания" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 4184053..b6fe39b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,12 +4,18 @@ import 'package:counters/address.dart'; import 'package:counters/counters_page.dart'; import 'package:counters/datbase.dart'; import 'package:counters/new_address_page.dart'; +import 'package:counters/record_action_pane.dart'; +import 'package:counters/update_address_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:intl/intl_standalone.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await findSystemLocale(); if (Platform.isWindows || Platform.isLinux) { sqfliteFfiInit(); } @@ -60,7 +66,12 @@ class _MyHomePageState extends State { future: addresses, builder: (context, snapshot) { if (snapshot.hasData && snapshot.data!.isNotEmpty) { - return AddressesListView(addresses: snapshot.data!); + return AddressesListView( + addresses: snapshot.data!, + onUpdated: () { + setState(() {}); + }, + ); } return Text(AppLocalizations.of(context)!.empty_addresses_list); @@ -79,46 +90,73 @@ class _MyHomePageState extends State { } } -class AddressesListView extends StatelessWidget { +class AddressesListView extends StatefulWidget { const AddressesListView({ super.key, required this.addresses, + this.onUpdated, }); final List
addresses; + final VoidCallback? onUpdated; + @override + State createState() => _AddressesListViewState(); +} + +class _AddressesListViewState extends State { @override Widget build(BuildContext context) { return ListView.builder( - itemCount: addresses.length, + itemCount: widget.addresses.length, itemBuilder: (context, possition) { - return TextButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CountersPage( - title: addresses[possition].streetName, - comments: addresses[possition].comments))); - }, - child: Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - addresses[possition].streetName, - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - addresses[possition].comments, - style: Theme.of(context).textTheme.bodySmall, - ), - ], - )), + return Slidable( + key: const ValueKey(0), + endActionPane: RecordActionPane( + onEdit: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => UpdateAddressPage( + address: widget.addresses[possition], + ))).then((_) => widget.onUpdated?.call()); + }, + onDelete: () { + DBProvider.db.deleteAddress(widget.addresses[possition]); + widget.onUpdated?.call(); + }, + ).build(context), + child: TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CountersPage( + address: widget.addresses[possition], + title: widget.addresses[possition].streetName, + comments: widget.addresses[possition].comments))); + }, + style: + TextButton.styleFrom(shape: const BeveledRectangleBorder()), + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.addresses[possition].streetName, + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + widget.addresses[possition].comments, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + )), + ), ), ); }); diff --git a/lib/new_counter_page.dart b/lib/new_counter_page.dart new file mode 100644 index 0000000..af71b93 --- /dev/null +++ b/lib/new_counter_page.dart @@ -0,0 +1,69 @@ +import 'package:counters/counters.dart'; +import 'package:counters/datbase.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class NewCounterPage extends StatelessWidget { + final int addressId; + final nameController = TextEditingController(); + CounterType counterType = CounterType.coldWater; + + NewCounterPage({super.key, required this.addressId}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(AppLocalizations.of(context)!.new_counter_title)), + body: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + DropdownMenu( + enableFilter: true, + hintText: + AppLocalizations.of(context)!.choose_type_of_counter, + expandedInsets: EdgeInsets.zero, + dropdownMenuEntries: + CounterType.values.map>( + (CounterType icon) { + return DropdownMenuEntry( + value: icon, + labelWidget: Text(icon.getLabel(context)), + label: icon.getLabel(context) + //leadingIcon: Icon(icon.icon), + ); + }, + ).toList(), + onSelected: (value) => counterType = value!, + ), + const SizedBox(height: 50), + TextField( + controller: nameController, + decoration: InputDecoration( + hintStyle: const TextStyle(color: Colors.blue), + hintText: + AppLocalizations.of(context)!.enter_counter_name), + ), + const SizedBox(height: 50), + TextButton( + onPressed: () { + DBProvider.db + .newCounter(Counter( + addressId: addressId, + counterType: counterType, + name: nameController.text)) + .then((value) => Navigator.pop(context)); + }, + child: Text( + AppLocalizations.of(context)!.add_new_address_button)) + ]), + ), + ), + ); + } +} diff --git a/lib/new_value_page.dart b/lib/new_value_page.dart new file mode 100644 index 0000000..f7beaa0 --- /dev/null +++ b/lib/new_value_page.dart @@ -0,0 +1,208 @@ +import 'package:counters/counters.dart'; +import 'package:counters/datbase.dart'; +import 'package:counters/value.dart'; +import 'package:datetime_picker_formfield/datetime_picker_formfield.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; + +class NewValuePage extends StatelessWidget { + NewValuePage({super.key, required this.counter}); + + factory NewValuePage.forCounter({required Counter counter}) { + switch (counter.counterType) { + case CounterType.coldWater: + return NewValuePageWithOneRate(counter: counter); + case CounterType.hotWater: + return NewValuePageWithOneRate(counter: counter); + case CounterType.gas: + return NewValuePageWithOneRate(counter: counter); + case CounterType.electrisity1rates: + return NewValuePageWithOneRate(counter: counter); + case CounterType.electrisity2rates: + return NewValuePageWithTwoRates(counter: counter); + case CounterType.electrisity3rates: + return NewValuePageWithThreeRates(counter: counter); + } + } + + final Counter counter; + final dataFormat = DateFormat("dd-MM-yyyy"); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(AppLocalizations.of(context)!.new_value_title)), + body: SizedBox.shrink(), + ); + } +} + +class NewValuePageWithOneRate extends NewValuePage { + final valueController = TextEditingController(); + final dateController = TextEditingController(); + NewValuePageWithOneRate({super.key, required super.counter}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(AppLocalizations.of(context)!.new_value_title)), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DateTimeField( + format: dataFormat, + initialValue: DateTime.now(), + controller: dateController, + onShowPicker: (context, currentValue) async { + final date = await showDatePicker( + context: context, + initialDate: currentValue ?? DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime(2100), + ); + return date; + }, + ), + const SizedBox(height: 20), + TextField( + controller: valueController, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'(^-?\d*\.?\d*)')) + ], + decoration: InputDecoration( + label: + Text(AppLocalizations.of(context)!.new_value_title), + //hintStyle: const TextStyle(color: Colors.blue), + hintText: '0.0', + suffixText: + counter.counterType.getUnits().getLabel(context))), + const SizedBox(height: 50), + TextButton( + onPressed: () { + DBProvider.db + .newValue(Value( + counterId: counter.id!, + date: dataFormat.parse(dateController.text).toUtc(), + rate1Id: 0, + value1: + double.tryParse(valueController.text) ?? 0.0, + value2: 0, + value3: 0)) + .then((value) => Navigator.pop(context)); + }, + child: Text( + AppLocalizations.of(context)!.add_new_address_button)) + ], + ), + ), + ), + ); + } +} + +class NewValuePageWithTwoRates extends NewValuePage { + final value1Controller = TextEditingController(); + final value2Controller = TextEditingController(); + final dateController = TextEditingController(); + NewValuePageWithTwoRates({super.key, required super.counter}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(AppLocalizations.of(context)!.new_value_title)), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DateTimeField( + format: DateFormat("dd-MM-yyyy"), + initialValue: DateTime.now(), + controller: dateController, + onShowPicker: (context, currentValue) async { + final date = await showDatePicker( + context: context, + initialDate: currentValue ?? DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime(2100), + ); + return date; + }, + ), + const SizedBox(height: 20), + TextField( + controller: value1Controller, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'(^-?\d*\.?\d*)')) + ], + decoration: InputDecoration( + label: + Text(AppLocalizations.of(context)!.new_value_title), + //hintStyle: const TextStyle(color: Colors.blue), + hintText: '0.0', + suffixText: + counter.counterType.getUnits().getLabel(context))), + TextField( + controller: value2Controller, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'(^-?\d*\.?\d*)')) + ], + decoration: InputDecoration( + label: + Text(AppLocalizations.of(context)!.new_value_title), + //hintStyle: const TextStyle(color: Colors.blue), + hintText: '0.0', + suffixText: + counter.counterType.getUnits().getLabel(context))), + const SizedBox(height: 50), + TextButton( + onPressed: () { + DBProvider.db + .newValue(Value( + counterId: counter.id!, + date: dataFormat.parse(dateController.text).toUtc(), + rate1Id: 0, + value1: + double.tryParse(value1Controller.text) ?? 0.0, + value2: + double.tryParse(value2Controller.text) ?? 0.0, + value3: 0)) + .then((value) => Navigator.pop(context)); + }, + child: Text( + AppLocalizations.of(context)!.add_new_address_button)) + ], + ), + ), + ), + ); + } +} + +class NewValuePageWithThreeRates extends NewValuePage { + NewValuePageWithThreeRates({super.key, required super.counter}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(AppLocalizations.of(context)!.new_value_title)), + body: SizedBox.shrink(), + ); + } +} diff --git a/lib/record_action_pane.dart b/lib/record_action_pane.dart new file mode 100644 index 0000000..5b57b48 --- /dev/null +++ b/lib/record_action_pane.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; + +class RecordActionPane extends StatelessWidget { + const RecordActionPane({ + super.key, + this.onDelete, + this.onEdit, + }); + + final VoidCallback? onDelete; + final VoidCallback? onEdit; + + @override + ActionPane build(BuildContext context) { + return ActionPane( + motion: const BehindMotion(), + //dismissible: DismissiblePane(onDismissed: () {}), + + children: [ + SlidableAction( + onPressed: (context) { + onEdit?.call(); + }, + backgroundColor: Colors.green, + foregroundColor: Colors.white, + icon: Icons.edit, + label: AppLocalizations.of(context)!.edit, + ), + SlidableAction( + onPressed: (context) { + onDelete?.call(); + }, + backgroundColor: Colors.red, + foregroundColor: Colors.white, + icon: Icons.delete, + label: AppLocalizations.of(context)!.delete, + ), + ], + ); + } +} diff --git a/lib/update_address_page.dart b/lib/update_address_page.dart new file mode 100644 index 0000000..d03ad5b --- /dev/null +++ b/lib/update_address_page.dart @@ -0,0 +1,62 @@ +import 'package:counters/address.dart'; +import 'package:counters/datbase.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class UpdateAddressPage extends StatelessWidget { + final streetNameController = TextEditingController(); + final commentsController = TextEditingController(); + + UpdateAddressPage({super.key, required this.address}); + + final Address address; + + @override + Widget build(BuildContext context) { + streetNameController.text = address.streetName; + commentsController.text = address.comments; + + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(AppLocalizations.of(context)!.new_address_title), + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + TextField( + controller: streetNameController, + decoration: InputDecoration( + hintStyle: const TextStyle(color: Colors.blue), + hintText: + AppLocalizations.of(context)!.enter_your_address), + ), + const SizedBox(height: 50), + TextField( + controller: commentsController, + decoration: InputDecoration( + hintStyle: const TextStyle(color: Colors.blue), + hintText: AppLocalizations.of(context)! + .enter_your_address_comments), + ), + const SizedBox(height: 50), + TextButton( + onPressed: () { + DBProvider.db + .updateAddress(Address( + id: address.id, + streetName: streetNameController.text, + comments: commentsController.text)) + .then((value) => Navigator.pop(context)); + }, + child: Text(AppLocalizations.of(context)!.edit)) + ]), + ), + ), + ); + } +} diff --git a/lib/update_counter_page.dart b/lib/update_counter_page.dart new file mode 100644 index 0000000..d68fddd --- /dev/null +++ b/lib/update_counter_page.dart @@ -0,0 +1,76 @@ +import 'package:counters/counters.dart'; +import 'package:counters/datbase.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class UpdateCounterPage extends StatelessWidget { + UpdateCounterPage({super.key, required this.counter}); + + final Counter counter; + final nameController = TextEditingController(); + final dropdownController = TextEditingController(); + CounterType counterType = CounterType.coldWater; + + @override + @override + Widget build(BuildContext context) { + nameController.text = counter.name; + counterType = counter.counterType; + dropdownController.text = counter.counterType.getLabel(context); + + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(AppLocalizations.of(context)!.new_counter_title)), + body: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + DropdownMenu( + controller: dropdownController, + enableFilter: true, + hintText: + AppLocalizations.of(context)!.choose_type_of_counter, + expandedInsets: EdgeInsets.zero, + dropdownMenuEntries: + CounterType.values.map>( + (CounterType icon) { + return DropdownMenuEntry( + value: icon, + labelWidget: Text(icon.getLabel(context)), + label: icon.getLabel(context) + //leadingIcon: Icon(icon.icon), + ); + }, + ).toList(), + onSelected: (value) => counterType = value!, + ), + const SizedBox(height: 50), + TextField( + controller: nameController, + decoration: InputDecoration( + hintStyle: const TextStyle(color: Colors.blue), + hintText: + AppLocalizations.of(context)!.enter_counter_name), + ), + const SizedBox(height: 50), + TextButton( + onPressed: () { + DBProvider.db + .updateCounter(Counter( + id: counter.id, + addressId: counter.addressId, + counterType: counterType, + name: nameController.text)) + .then((value) => Navigator.pop(context)); + }, + child: Text(AppLocalizations.of(context)!.edit)) + ]), + ), + ), + ); + } +} diff --git a/lib/value.dart b/lib/value.dart new file mode 100644 index 0000000..417dd9f --- /dev/null +++ b/lib/value.dart @@ -0,0 +1,45 @@ +class Value { + final int? id; + final int counterId; + final DateTime date; + final int rate1Id; + final double value1; + final int? rate2Id; + final double? value2; + final int? rate3Id; + final double? value3; + + Value( + {this.id, + required this.counterId, + required this.date, + required this.rate1Id, + required this.value1, + this.rate2Id, + this.value2, + this.rate3Id, + this.value3}); + + factory Value.fromMap(Map json) => Value( + id: json["id"], + counterId: json["counter_id"], + date: DateTime.fromMicrosecondsSinceEpoch(json["date"], isUtc: true), + rate1Id: json["rate1_id"], + value1: json["value1"], + rate2Id: json["rate2_id"], + value2: json["value2"], + rate3Id: json["rate3_id"], + value3: json["value3"]); + + Map toMap() => { + "id": id, + "counter_id": counterId, + "date": date.microsecondsSinceEpoch, + "rate1_id": rate1Id, + "value1": value1, + "rate2_id": rate2Id, + "value2": value2, + "rate3_id": rate3Id, + "value3": value3 + }; +} diff --git a/lib/values_page.dart b/lib/values_page.dart new file mode 100644 index 0000000..cbe6f3e --- /dev/null +++ b/lib/values_page.dart @@ -0,0 +1,119 @@ +import 'package:counters/counters.dart'; +import 'package:counters/datbase.dart'; +import 'package:counters/new_value_page.dart'; +import 'package:counters/value.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; + +class ValuesPage extends StatefulWidget { + const ValuesPage({super.key, required this.counter}); + + final Counter counter; + + @override + State createState() => _ValuesPageState(); +} + +class _ValuesPageState extends State { + @override + Widget build(BuildContext context) { + var values = DBProvider.db.getValuesOfCounter(widget.counter); + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(AppLocalizations.of(context)!.app_title)), + body: Center( + child: FutureBuilder( + future: values, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!.isNotEmpty) { + return ValuesListView( + counter: widget.counter, + values: snapshot.data!.reversed.toList()); + } + + return Text(AppLocalizations.of(context)!.empty_values_list); + }, + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + NewValuePage.forCounter(counter: widget.counter))) + .then((_) => setState(() {})); + }, + tooltip: AppLocalizations.of(context)!.add_new_counter_tooltip, + child: const Icon(Icons.add), + )); + } +} + +class ValuesListView extends StatelessWidget { + ValuesListView({ + super.key, + required this.values, + required this.counter, + }); + + final List values; + final Counter counter; + final dataFormat = DateFormat("dd-MM-yyyy"); + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: values.length, + itemBuilder: (context, index) { + var units = counter.counterType.getUnits().getLabel(context); + var valueWidgets = List.empty(growable: true); + valueWidgets.add(Text("T1 - ${values[index].value1} $units")); + var sum = values[index].value1; + var value = values[index]; + if (value.value2 != null && + counter.counterType.id > CounterType.electrisity1rates.id) { + valueWidgets.add(Text("T2 - ${value.value2} $units")); + sum += value.value2!; + } + if (value.value3 != null && + counter.counterType.id > CounterType.electrisity2rates.id) { + valueWidgets.add(Text("T3 - ${value.value3} $units")); + sum += value.value3!; + } + + if (valueWidgets.length > 1) { + valueWidgets.add(Text("Итого - $sum $units")); + } + return Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0), + child: Card( + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text(dataFormat.format(value.date.toLocal())), + Text(' ${AppLocalizations.of(context)!.values}:'), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: valueWidgets, + ) + ], + ), + ), + )), + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index da47d0f..7d81381 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + datetime_picker_formfield: + dependency: "direct main" + description: + name: datetime_picker_formfield + sha256: "6d0412c98cc5da18a5dca1f81f82a834fbacdb5d249fd6d9bed42d912339720e" + url: "https://pub.dev" + source: hosted + version: "2.0.1" fake_async: dependency: transitive description: @@ -83,11 +91,40 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_slidable: + dependency: "direct main" + description: + name: flutter_slidable + sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c" + url: "https://pub.dev" + source: hosted + version: "3.1.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + url: "https://pub.dev" + source: hosted + version: "1.2.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" intl: dependency: "direct main" description: @@ -136,6 +173,31 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + package_info_plus_aurora: + dependency: "direct main" + description: + path: "packages/package_info_plus/package_info_plus_aurora" + ref: "package_info_plus_aurora-0.5.0" + resolved-ref: "529306911d320e4b37391baa233842803173b3a1" + url: "https://gitlab.com/omprussia/flutter/flutter-plugins.git" + source: git + version: "0.5.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" path: dependency: "direct main" description: @@ -160,6 +222,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.4" + path_provider_aurora: + dependency: "direct main" + description: + path: "packages/path_provider/path_provider_aurora" + ref: "path_provider_aurora-0.5.0" + resolved-ref: "529306911d320e4b37391baa233842803173b3a1" + url: "https://gitlab.com/omprussia/flutter/flutter-plugins.git" + source: git + version: "0.5.0" path_provider_foundation: dependency: transitive description: @@ -310,6 +381,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" vector_math: dependency: transitive description: @@ -342,6 +421,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + xdga_directories: + dependency: transitive + description: + path: "packages/xdga_directories" + ref: "xdga_directories-0.5.0" + resolved-ref: "529306911d320e4b37391baa233842803173b3a1" + url: "https://gitlab.com/omprussia/flutter/flutter-plugins.git" + source: git + version: "0.5.0" sdks: dart: ">=3.2.2 <4.0.0" flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index f5bc936..932bbec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: counters -description: "A new Flutter project." +description: "Values of counters" publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 @@ -23,7 +23,21 @@ dependencies: sdk: flutter intl: any path_provider: ^2.1.3 + path_provider_aurora: + git: + url: https://gitlab.com/omprussia/flutter/flutter-plugins.git + ref: path_provider_aurora-0.5.0 + path: packages/path_provider/path_provider_aurora + package_info_plus_aurora: + git: + url: https://gitlab.com/omprussia/flutter/flutter-plugins.git + ref: package_info_plus_aurora-0.5.0 + path: packages/package_info_plus/package_info_plus_aurora + + path: ^1.8.3 + flutter_slidable: ^3.1.0 + datetime_picker_formfield: ^2.0.1 dev_dependencies: flutter_test: @@ -35,4 +49,4 @@ flutter: uses-material-design: true assets: - lib/l10n/app_en.arb - - lib/l10n/app_ru.arb \ No newline at end of file + - lib/l10n/app_ru.arb