ft/adds person_detail_page

This commit is contained in:
itsscb 2023-11-07 14:48:55 +01:00
parent e2d3720728
commit 6d48638ef1
15 changed files with 622 additions and 47 deletions

View File

@ -1,11 +1,15 @@
import 'package:app/pages/home_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'
show GlobalMaterialLocalizations;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(
MaterialApp(
localizationsDelegates: const [GlobalMaterialLocalizations.delegate],
supportedLocales: const [Locale('en'), Locale('de')],
theme: ThemeData().copyWith(
colorScheme: const ColorScheme(
brightness: Brightness.dark,

View File

@ -4,6 +4,7 @@ class AppException implements Exception {
AppException([this._message, this._prefix]);
@override
String toString() {
return "$_prefix$_message";
}

View File

@ -14,6 +14,7 @@ import 'package:app/pb/rpc_get_person.pb.dart';
import 'package:app/pb/rpc_list_persons.pb.dart';
import 'package:app/pb/rpc_login.pb.dart';
import 'package:app/pb/rpc_refresh_token.pb.dart';
import 'package:app/pb/rpc_update_person.pb.dart';
import 'package:app/pb/service_df.pbgrpc.dart';
import 'package:fixnum/fixnum.dart';
import 'package:grpc/grpc.dart';
@ -114,6 +115,10 @@ class BackendService {
return true;
}
static Future<Int64?> get accountId async {
return (await Session.session).accountId;
}
static Future<bool> createAccount(
{required String email, required String password}) async {
try {
@ -207,14 +212,15 @@ class BackendService {
}
}
Future<Person> createPerson(
{required String firstname,
required String lastname,
required String street,
required String zip,
required String city,
required String country,
required DateTime birthday}) async {
Future<Person> updatePerson(
{required Int64 id,
String? firstname,
String? lastname,
String? street,
String? zip,
String? city,
String? country,
Timestamp? birthday}) async {
Session session = await Session.session;
if (session.accessTokenExpiresAt == null) {
throw UnauthorizedException('Keine Siztung gefunden');
@ -226,16 +232,74 @@ class BackendService {
}
}
try {
final CreatePersonResponse response = await _client.createPerson(
CreatePersonRequest(
accountId: session.accountId,
lastname: lastname,
firstname: firstname,
street: street,
zip: zip,
country: country,
birthday: Timestamp.fromDateTime(birthday),
),
final UpdatePersonRequest req = UpdatePersonRequest(
id: id,
);
if (lastname != null) {
req.lastname = lastname;
}
if (firstname != null) {
req.firstname = firstname;
}
if (street != null) {
req.street = street;
}
if (city != null) {
req.city = city;
}
if (zip != null) {
req.zip = zip;
}
if (country != null) {
req.country = country;
}
if (birthday != null) {
req.birthday = birthday;
}
final UpdatePersonResponse response = await _client.updatePerson(req,
options: CallOptions(
metadata: {'Authorization': 'Bearer ${session.accessToken}'}));
return response.person;
} on SocketException {
throw FetchDataException('Keine Internet Verbindung');
} on GrpcError catch (err) {
throw FetchDataException(err.message);
} catch (err) {
throw InternalException(err.toString());
}
}
Future<Person> createPerson(
{required String firstname,
required String lastname,
required String street,
required String zip,
required String city,
required String country,
required Timestamp birthday}) async {
Session session = await Session.session;
if (session.accessTokenExpiresAt == null) {
throw UnauthorizedException('Keine Siztung gefunden');
}
if (session.accessTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) {
session = await refreshToken(session);
if (session.accessTokenExpiresAt == null) {
throw UnauthorizedException('Sitzung ist abgelaufen');
}
}
try {
final CreatePersonRequest req = CreatePersonRequest(
accountId: session.accountId,
lastname: lastname,
firstname: firstname,
street: street,
city: city,
zip: zip,
country: country,
birthday: birthday,
);
final CreatePersonResponse response = await _client.createPerson(req,
options: CallOptions(
metadata: {'Authorization': 'Bearer ${session.accessToken}'}));
return response.person;

View File

@ -7,11 +7,12 @@ class AccountViewModel extends BaseViewModel {
AccountViewModel() {
_init();
}
ApiResponse _apiResponse = ApiResponse.initial('Keine Daten');
final ApiResponse _apiResponse = ApiResponse.initial('Keine Daten');
final BackendService _service = BackendService();
Account? _account;
@override
ApiResponse get response {
return _apiResponse;
}

View File

@ -1,6 +1,9 @@
import 'package:app/model/apis/api_response.dart';
import 'package:app/model/services/backend_service.dart';
import 'package:app/pb/google/protobuf/timestamp.pb.dart';
import 'package:app/pb/person.pb.dart';
import 'package:app/util/colors.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart';
class PersonsViewModel with ChangeNotifier {
@ -35,8 +38,9 @@ class PersonsViewModel with ChangeNotifier {
required String zip,
required String city,
required String country,
required DateTime birthday}) async {
required Timestamp birthday}) async {
Person person = Person();
final messenger = ScaffoldMessenger.of(context);
_apiResponse = ApiResponse.loading('Erstelle Person');
try {
person = await _service.createPerson(
@ -47,8 +51,102 @@ class PersonsViewModel with ChangeNotifier {
city: city,
country: country,
birthday: birthday);
messenger.showSnackBar(SnackBar(
backgroundColor: CustomColors.success,
content: const Text(
'Gepeichert',
style: TextStyle(color: Colors.white),
),
));
_apiResponse = ApiResponse.completed(person);
} catch (err) {
messenger.showSnackBar(SnackBar(
backgroundColor: CustomColors.error,
content: const Text(
'Fehler beim Speichern',
style: TextStyle(color: Colors.white),
),
action: SnackBarAction(
label: 'Details',
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.black,
icon: Icon(
Icons.error,
color: CustomColors.error,
),
content: Text(
err.toString(),
textAlign: TextAlign.center,
),
));
},
),
));
_apiResponse = ApiResponse.error(err.toString());
}
notifyListeners();
return person;
}
Future<Person> updatePerson(BuildContext context,
{required Int64 id,
String? firstname,
String? lastname,
String? street,
String? zip,
String? city,
String? country,
Timestamp? birthday}) async {
Person person = Person();
final messenger = ScaffoldMessenger.of(context);
_apiResponse = ApiResponse.loading('Erstelle Person');
try {
person = await _service.updatePerson(
id: id,
firstname: firstname,
lastname: lastname,
street: street,
zip: zip,
city: city,
country: country,
birthday: birthday);
messenger.showSnackBar(SnackBar(
backgroundColor: CustomColors.success,
content: const Text(
'Gepeichert',
style: TextStyle(color: Colors.white),
),
));
_apiResponse = ApiResponse.completed(person);
} catch (err) {
messenger.showSnackBar(SnackBar(
backgroundColor: CustomColors.error,
content: const Text(
'Fehler beim Speichern',
style: TextStyle(color: Colors.white),
),
action: SnackBarAction(
label: 'Details',
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.black,
icon: Icon(
Icons.error,
color: CustomColors.error,
),
content: Text(
err.toString(),
textAlign: TextAlign.center,
),
));
},
),
));
_apiResponse = ApiResponse.error(err.toString());
}
notifyListeners();

View File

@ -0,0 +1,339 @@
import 'package:app/model/view_model/persons_vm.dart';
import 'package:app/pb/google/protobuf/timestamp.pb.dart';
import 'package:app/pb/person.pb.dart';
import 'package:app/util/validation.dart';
import 'package:app/widgets/background.dart';
import 'package:app/widgets/bottom_navigation.dart';
import 'package:app/widgets/bottom_navigation_item.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
Future<Person> showPerson(BuildContext context, {Person? person}) async {
PersonsViewModel vm = PersonsViewModel();
final formKey = GlobalKey<FormState>();
final firstnameController = TextEditingController();
final lastnameController = TextEditingController();
final cityController = TextEditingController();
final zipController = TextEditingController();
final streetController = TextEditingController();
final countryController = TextEditingController();
final birthdayController = TextEditingController();
Future<void> _init() async {
if (person == null) {
person ??= Person();
// person ??= Person(accountId: await BackendService.accountId);
} else {
firstnameController.text = person!.firstname;
lastnameController.text = person!.lastname;
cityController.text = person!.city;
zipController.text = person!.zip;
streetController.text = person!.street;
countryController.text = person!.country;
birthdayController.text =
DateFormat('dd.MM.yyyy').format(person!.birthday.toDateTime());
}
}
await _init();
void _updateData() {
person!.firstname = firstnameController.text;
person!.lastname = lastnameController.text;
person!.city = cityController.text;
person!.street = streetController.text;
person!.zip = zipController.text;
person!.country = countryController.text;
}
Future<void> createPerson(BuildContext context) async {
final navigator = Navigator.of(context);
_updateData();
person!.id = Int64(0);
person = await vm.createPerson(context,
firstname: person!.firstname,
lastname: person!.lastname,
street: person!.street,
zip: person!.zip,
city: person!.city,
country: person!.country,
birthday: person!.birthday);
if (person!.id != 0) {
navigator.pop(person);
}
}
Future<void> updatePerson(BuildContext context) async {
final navigator = Navigator.of(context);
_updateData();
final personUpdate = await vm.updatePerson(context,
id: person!.id,
firstname: person!.firstname != firstnameController.text
? person!.firstname
: null,
lastname: person!.lastname != lastnameController.text
? person!.lastname
: null,
street: person!.street != streetController.text ? person!.street : null,
zip: person!.zip != zipController.text ? person!.zip : null,
city: person!.city != cityController.text ? person!.city : null,
country:
person!.country != countryController.text ? person!.country : null,
birthday:
DateFormat('dd.MM.yyyy').format(person!.birthday.toDateTime()) !=
birthdayController.text
? person!.birthday
: null);
if (personUpdate != person) {
navigator.pop(person);
}
}
// ignore: use_build_context_synchronously
await showModalBottomSheet(
context: context,
builder: (builder) {
return Background(
child: Scaffold(
bottomNavigationBar: BottomNavigation(
hideMenu: true,
children: [
BottomNavigationItem(
onPressed: () {
Navigator.pop(context, false);
},
icon: Icons.arrow_back,
color: Colors.white,
label: 'Zurück',
),
BottomNavigationItem(
onPressed: () {
Navigator.pop(context, false);
},
icon: Icons.home,
color: Colors.white,
label: 'Home',
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
height: 50,
),
Text(
person!.id == 0 ? 'Person anlegen' : 'Person anpassen',
style: const TextStyle(
fontFamily: 'sans-serif',
fontSize: 24,
height: 1.6,
fontWeight: FontWeight.normal,
letterSpacing: 6),
),
ChangeNotifierProvider<PersonsViewModel>(
create: (context) => PersonsViewModel(),
child: Consumer<PersonsViewModel>(
builder: (context, value, child) => Form(
key: formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(
height: 40,
),
TextFormField(
controller: firstnameController,
decoration: const InputDecoration(
fillColor: Color.fromARGB(30, 255, 255, 255),
filled: true,
suffix: Text('Vorname'),
hintStyle: TextStyle(
color: Colors.white38,
),
hintText: 'Vorname',
),
keyboardType: TextInputType.name,
validator: (value) {
if (value == null || !value.isValidName) {
return 'Bitte einen gültigen Vornamen eingeben';
}
return null;
},
),
TextFormField(
controller: lastnameController,
decoration: const InputDecoration(
fillColor: Color.fromARGB(30, 255, 255, 255),
filled: true,
suffix: Text('Nachname'),
hintStyle: TextStyle(
color: Colors.white38,
),
hintText: 'Nachname',
),
keyboardType: TextInputType.name,
validator: (value) {
if (value == null || !value.isValidName) {
return 'Bitte einen gültigen Nachnamen eingeben';
}
return null;
},
),
TextFormField(
readOnly: true,
onTap: () async {
DateTime? pickedDate = await showDatePicker(
context: context,
locale: const Locale('de', 'DE'),
initialDate: DateTime.now(),
firstDate: DateTime(1930),
lastDate: DateTime(DateTime.now().year + 1),
builder: (context, child) => Theme(
data: ThemeData.dark(),
child: child != null ? child : Text(''),
),
);
if (pickedDate != null) {
person!.birthday =
Timestamp.fromDateTime(pickedDate);
birthdayController.text =
DateFormat('dd.MM.yyyy')
.format(pickedDate);
}
},
controller: birthdayController,
decoration: const InputDecoration(
fillColor: Color.fromARGB(30, 255, 255, 255),
filled: true,
suffix: Text('Geburtstag'),
hintStyle: TextStyle(
color: Colors.white38,
),
hintText: 'Geburtstag',
),
keyboardType: TextInputType.name,
// validator: (value) {
// if (value == null || !value.isValidName) {
// return 'Bitte einen gültigen Nachnamen eingeben';
// }
// return null;
// },
),
TextFormField(
controller: streetController,
decoration: const InputDecoration(
fillColor: Color.fromARGB(30, 255, 255, 255),
filled: true,
suffix: Text('Straße'),
hintStyle: TextStyle(
color: Colors.white38,
),
hintText: 'Straße mit Hausnummer',
),
keyboardType: TextInputType.name,
validator: (value) {
if (value == null || !value.isValidName) {
return 'Bitte eine gültige Straße mit Hausnummer eingeben';
}
return null;
},
),
TextFormField(
controller: zipController,
decoration: const InputDecoration(
fillColor: Color.fromARGB(30, 255, 255, 255),
filled: true,
suffix: Text('PLZ'),
hintStyle: TextStyle(
color: Colors.white38,
),
hintText: 'PLZ',
),
keyboardType: TextInputType.name,
validator: (value) {
if (value == null || !value.isValidName) {
return 'Bitte eine gültige PLZ eingeben';
}
return null;
},
),
TextFormField(
controller: cityController,
decoration: const InputDecoration(
fillColor: Color.fromARGB(30, 255, 255, 255),
filled: true,
suffix: Text('Stadt'),
hintStyle: TextStyle(
color: Colors.white38,
),
hintText: 'Stadt',
),
keyboardType: TextInputType.name,
validator: (value) {
if (value == null || !value.isValidName) {
return 'Bitte eine gültige Stadt eingeben';
}
return null;
},
),
TextFormField(
controller: countryController,
decoration: const InputDecoration(
fillColor: Color.fromARGB(30, 255, 255, 255),
filled: true,
suffix: Text('Land'),
hintStyle: TextStyle(
color: Colors.white38,
),
hintText: 'Land',
),
keyboardType: TextInputType.name,
validator: (value) {
if (value == null || !value.isValidName) {
return 'Bitte ein gültiges Land eingeben';
}
return null;
},
),
const SizedBox(
height: 15,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () async {
person!.id.isZero
? await createPerson(context)
: await updatePerson(context);
},
child: const Icon(Icons.update),
),
],
)
],
),
),
),
),
],
),
),
),
);
},
useSafeArea: true,
isScrollControlled: true,
backgroundColor: Colors.black);
return person!;
}

View File

@ -2,7 +2,9 @@ import 'package:app/model/apis/api_response.dart';
import 'package:app/model/services/backend_service.dart';
import 'package:app/model/view_model/persons_vm.dart';
import 'package:app/pages/home_page.dart';
import 'package:app/pages/person_details_page.dart';
import 'package:app/pb/person.pb.dart';
import 'package:app/util/validation.dart';
import 'package:app/widgets/background.dart';
import 'package:app/widgets/bottom_navigation.dart';
import 'package:app/widgets/bottom_navigation_item.dart';
@ -61,13 +63,56 @@ class _PersonsPageState extends State<PersonsPage> {
}
List<Widget> _personsList(List<Person> persons) {
persons.sort((a, b) {
final comp = a.lastname.compareTo(b.lastname);
if (comp != 0) {
return comp;
}
return a.firstname.compareTo(b.firstname);
});
final List<Widget> list = [];
for (var p in persons) {
list.add(Card(
color: Colors.black,
child: Text(
'${p.firstname} ${p.lastname}',
style: const TextStyle(color: Colors.white),
list.add(TextButton(
onPressed: () async {
final Person per = await showPerson(context, person: p);
setState(() {
this.persons.add(per);
});
},
child: Card(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
color: const Color.fromARGB(100, 89, 88, 88),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 14),
child: Row(
children: [
Container(
height: 40,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
p.lastname.titleCase,
style: const TextStyle(color: Colors.white),
// overflow: TextOverflow.fade,
textAlign: TextAlign.start,
),
const Spacer(),
Text(
p.firstname.titleCase,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.start,
// overflow: TextOverflow.fade,
),
],
),
),
const Spacer(),
const Text('STATUS')
],
),
),
),
));
}
@ -80,7 +125,14 @@ class _PersonsPageState extends State<PersonsPage> {
child: Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: FloatingActionButton(
onPressed: () {},
onPressed: () async {
final p = await showPerson(context);
if (!p.id.isZero) {
setState(() {
persons.add(p);
});
}
},
child: const Icon(Icons.add),
),
appBar: AppBar(

View File

@ -1,9 +1,5 @@
import 'package:app/gapi/client.dart';
import 'package:app/pages_old/start_page.dart';
import 'package:app/pb/account_info.pb.dart';
import 'package:app/pb/rpc_get_account_info.pb.dart';
import 'package:app/widgets/background.dart';
import 'package:app/widgets/bottom_bar.dart';
import 'package:app/widgets/loading_widget.dart';
import 'package:app/widgets/side_drawer.dart';
import 'package:flutter/material.dart';

View File

@ -1,12 +1,9 @@
import 'package:app/gapi/client.dart';
import 'package:app/model/services/backend_service.dart';
import 'package:app/pages_old/start_page.dart';
import 'package:app/widgets/background.dart';
import 'package:app/widgets/bottom_bar.dart';
import 'package:app/widgets/loading_widget.dart';
import 'package:app/widgets/side_drawer.dart';
import 'package:flutter/material.dart';
import 'package:grpc/grpc.dart';
// GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();

View File

@ -1,11 +1,8 @@
import 'package:app/gapi/client.dart';
import 'package:app/pages_old/start_page.dart';
import 'package:app/widgets/background.dart';
import 'package:app/widgets/bottom_bar.dart';
import 'package:app/widgets/loading_widget.dart';
import 'package:app/widgets/side_drawer.dart';
import 'package:flutter/material.dart';
import 'package:grpc/grpc.dart';
class RegisterPage extends StatefulWidget {
const RegisterPage({

View File

@ -1,9 +1,6 @@
import 'package:app/gapi/client.dart';
import 'package:app/model/apis/api_response.dart';
import 'package:app/model/view_model/account_vm.dart';
import 'package:app/pages_old/dashboard_page.dart';
import 'package:app/pages_old/login_page.dart';
import 'package:app/pages_old/register_page.dart';
import 'package:app/pb/account.pb.dart';
import 'package:app/widgets/background.dart';
import 'package:app/widgets/bottom_bar.dart';

View File

@ -24,4 +24,10 @@ extension valString on String {
bool get isValidPhone {
return phoneRegExp.hasMatch(this);
}
String get titleCase {
return split(' ')
.map((str) => str[0].toUpperCase() + str.substring(1))
.join(' ');
}
}

View File

@ -7,10 +7,13 @@ class BottomNavigation extends StatelessWidget {
required this.children,
this.backgroundColor,
this.iconColor,
this.hideMenu,
}) {
hideMenu ??= false;
backgroundColor ??= Colors.black;
}
bool? hideMenu;
List<Widget> children;
Color? backgroundColor;
Color? iconColor;
@ -32,14 +35,18 @@ class BottomNavigation extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
...children,
Builder(builder: (context) {
return IconButton(
onPressed: () => Scaffold.of(context).openDrawer(),
icon: const Icon(
Icons.menu,
color: Colors.white,
));
}),
if (!hideMenu!)
Builder(
builder: (context) {
return IconButton(
onPressed: () => Scaffold.of(context).openDrawer(),
icon: const Icon(
Icons.menu,
color: Colors.white,
),
);
},
)
],
),
),

View File

@ -110,6 +110,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.3"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -155,6 +160,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.dev"
source: hosted
version: "0.18.1"
js:
dependency: transitive
description:

View File

@ -33,6 +33,8 @@ dependencies:
collection: ^1.15.0-nullsafety.4
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
@ -43,6 +45,7 @@ dependencies:
path: ^1.8.3
fixnum: ^1.1.0
provider: ^6.0.5
intl: ^0.18.1
dev_dependencies:
lints: ^2.0.0