From: Hamatoma Date: Sat, 9 Oct 2021 06:03:18 +0000 (+0200) Subject: ListUserPage, CreateUserPage and EditUserPage work, I18N X-Git-Url: https://gitweb.hamatoma.de/?a=commitdiff_plain;h=9dec32d283b3d51f117ef8d6da2a3728c700a2cf;p=exhibition.git ListUserPage, CreateUserPage and EditUserPage work, I18N * I18N: new: trDyn() * new global_widget.dart: fetching data from the backend, building combobox items * new: base/validators.dart * create_user_custom: modified to work * global_data.dart: ** new loginUserName, loginUserId, loginUserRole ** new: GlobalTranslations, GlobalThemeData * new widget/message_line.dart * WidgetForm: refactored: switched from widget list to FormItem list. * sql_generator: ** byId, delete: fix: missing ':' in ':id' ** INSERT: fix: ignoring primary key ** UPDATE: fix: ignoring hidden fields, errors respecting the primary key * sql_storage: fix: parameters has strings not dynamics. * AttendePage: new getRecord(), loadRecord(), hasRecordResponse(), hasResponse(), listResponse(), recordResponse() * new: global.sql.yaml, local.sql.yaml --- diff --git a/lib/base/i18n.dart b/lib/base/i18n.dart index ab6c457..e0ccf44 100644 --- a/lib/base/i18n.dart +++ b/lib/base/i18n.dart @@ -87,6 +87,31 @@ class I18N { return rc; } + /// Translates the [key] into the local language using the namespace [module]. + /// + /// Note: Difference to tr(): the parameter [key] is not a constant. Therefore + /// that argument cannot be used from the parser. + /// A often used example is a database value. + /// + /// Returns the translation or (if not found) the key. + String trDyn(String key, [String module = '!global']) { + String? rc; + final mapModule = mapModules[module]; + if (mapModule == null) { + rc = removeInvisibleTag(key); + } else { + rc = mapModule[key]; + if (rc == null) { + if (module == globalModule) { + rc = removeInvisibleTag(key); + } else { + rc = tr(key, globalModule); + } + } + } + return rc; + } + /// Translates the [key] into the local language using the namespace [module]. /// [key] contains placeholders "{0}", "{1}"... that is replaced by /// the corresponding entry in the array [args]. diff --git a/lib/base/validators.dart b/lib/base/validators.dart new file mode 100644 index 0000000..8baf63a --- /dev/null +++ b/lib/base/validators.dart @@ -0,0 +1,50 @@ +import 'i18n.dart'; + +final i18n = I18N(); + +final _regExprEMailChar = RegExp(r'[\/ "!$%&()?;:,*<>|^°{}\[\]\\=]'); + +final _regExprEMailFormat = RegExp(r'^[^@]+@[^@]+\.[a-zA-Z]+$'); + +/// Tests whether [input] is an correct email address. +/// +/// Returns null on success, the error message otherwise. +String? isEmail(String? input) { + String? rc; + if (input != null) { + RegExpMatch? match = _regExprEMailChar.firstMatch(input); + if (match != null) { + rc = i18n.trArgs('Illegal character "{0} in email address', '!global', + [match.group(0)!]); + } else if (_regExprEMailFormat.firstMatch(input) == null) { + rc = i18n.tr('Not an email address: ') + input; + } + } + return rc; +} + +/// Tests whether the input is not empty. +String? notEmpty(String? input) { + final rc = input == null || input.isEmpty ? i18n.tr('Please fill in.') : null; + return rc; +} + +/// Executes multiple validators for a given [input]. +/// +/// If any returns an error message this error message is returned. +/// +/// Returns null: all validators are successful. +/// Otherwise: the first error message returned by the [validators]. +String? validateMultiple(String? input, List validators) { + String? rc; + for (var validator in validators) { + final rc2 = validator(input); + if (rc2 != null) { + rc = rc2; + break; + } + } + return rc; +} + +typedef Validator = String? Function(String? input); diff --git a/lib/page/log_page.dart b/lib/page/log_page.dart index 522b9c7..81782e3 100644 --- a/lib/page/log_page.dart +++ b/lib/page/log_page.dart @@ -22,7 +22,7 @@ class LogPageState extends State { @override Widget build(BuildContext context) { - const padding = 16.0; + final padding = GlobalThemeData.padding; final listItems = (globalData.logger as MemoryLogger) .messages .map((line) => Text(line, @@ -35,10 +35,10 @@ class LogPageState extends State { body: Form( key: _formKey, child: Card( - margin: const EdgeInsets.symmetric( - vertical: padding, horizontal: padding), + margin: + EdgeInsets.symmetric(vertical: padding, horizontal: padding), child: Padding( - padding: const EdgeInsets.symmetric( + padding: EdgeInsets.symmetric( vertical: padding, horizontal: padding), child: ListView( children: listItems, diff --git a/lib/page/roles/create_role_custom.dart b/lib/page/roles/create_role_custom.dart index 4768a02..15a4f82 100644 --- a/lib/page/roles/create_role_custom.dart +++ b/lib/page/roles/create_role_custom.dart @@ -17,7 +17,7 @@ class CreateRoleCustom extends State { CreateRoleCustom(); @override Widget build(BuildContext context) { - final padding = 16.0; + final padding = GlobalThemeData.padding; final rc = Scaffold( appBar: globalData.appBarBuilder(i18n.tr('Change Role data')), drawer: globalData.drawerBuilder(context), diff --git a/lib/page/roles/edit_role_custom.dart b/lib/page/roles/edit_role_custom.dart index 5f2006d..e5326de 100644 --- a/lib/page/roles/edit_role_custom.dart +++ b/lib/page/roles/edit_role_custom.dart @@ -18,7 +18,7 @@ class EditRoleCustom extends State { EditRoleCustom(this.primaryKey); @override Widget build(BuildContext context) { - final padding = 16.0; + final padding = GlobalThemeData.padding; final rc = Scaffold( appBar: globalData.appBarBuilder(i18n.tr('Change Role data')), drawer: globalData.drawerBuilder(context), diff --git a/lib/page/roles/list_role_custom.dart b/lib/page/roles/list_role_custom.dart index d23445c..b73090e 100644 --- a/lib/page/roles/list_role_custom.dart +++ b/lib/page/roles/list_role_custom.dart @@ -17,7 +17,7 @@ class ListRoleCustom extends State { ListRoleCustom(); @override Widget build(BuildContext context) { - final padding = 16.0; + final padding = GlobalThemeData.padding; final rc = Scaffold( appBar: globalData.appBarBuilder(i18n.tr('Change Role data')), drawer: globalData.drawerBuilder(context), diff --git a/lib/page/start_page.dart b/lib/page/start_page.dart index d2876bf..8c79d6f 100644 --- a/lib/page/start_page.dart +++ b/lib/page/start_page.dart @@ -36,7 +36,7 @@ class StartPageState extends State { @override Widget build(BuildContext context) { - final padding = 16.0; + final padding = GlobalThemeData.padding; final listItems = [ Text( 'Herzlich Willkommen', diff --git a/lib/page/structures/create_structure_custom.dart b/lib/page/structures/create_structure_custom.dart index a0f39d6..1132463 100644 --- a/lib/page/structures/create_structure_custom.dart +++ b/lib/page/structures/create_structure_custom.dart @@ -17,7 +17,7 @@ class CreateStructureCustom extends State { CreateStructureCustom(); @override Widget build(BuildContext context) { - final padding = 16.0; + final padding = GlobalThemeData.padding; final rc = Scaffold( appBar: globalData.appBarBuilder(i18n.tr('Change Structure data')), drawer: globalData.drawerBuilder(context), diff --git a/lib/page/structures/delete_structure_custom.dart b/lib/page/structures/delete_structure_custom.dart index c6b9ba0..d1dcfb2 100644 --- a/lib/page/structures/delete_structure_custom.dart +++ b/lib/page/structures/delete_structure_custom.dart @@ -18,7 +18,7 @@ class DeleteStructureCustom extends State { DeleteStructureCustom(this.primaryKey); @override Widget build(BuildContext context) { - final padding = 16.0; + final padding = GlobalThemeData.padding; final rc = Scaffold( appBar: globalData.appBarBuilder(i18n.tr('Change Structure data')), drawer: globalData.drawerBuilder(context), diff --git a/lib/page/structures/edit_structure_custom.dart b/lib/page/structures/edit_structure_custom.dart index 3f2aa2a..b331083 100644 --- a/lib/page/structures/edit_structure_custom.dart +++ b/lib/page/structures/edit_structure_custom.dart @@ -18,7 +18,7 @@ class EditStructureCustom extends State { EditStructureCustom(this.primaryKey); @override Widget build(BuildContext context) { - final padding = 16.0; + final padding = GlobalThemeData.padding; final rc = Scaffold( appBar: globalData.appBarBuilder(i18n.tr('Change Structure data')), drawer: globalData.drawerBuilder(context), diff --git a/lib/page/structures/list_structure_custom.dart b/lib/page/structures/list_structure_custom.dart index 3bf1b62..eb54b7d 100644 --- a/lib/page/structures/list_structure_custom.dart +++ b/lib/page/structures/list_structure_custom.dart @@ -17,7 +17,7 @@ class ListStructureCustom extends State { ListStructureCustom(); @override Widget build(BuildContext context) { - final padding = 16.0; + final padding = GlobalThemeData.padding; final rc = Scaffold( appBar: globalData.appBarBuilder(i18n.tr('Change Structure data')), drawer: globalData.drawerBuilder(context), diff --git a/lib/page/users/create_user_custom.dart b/lib/page/users/create_user_custom.dart index 48a581e..99296b1 100644 --- a/lib/page/users/create_user_custom.dart +++ b/lib/page/users/create_user_custom.dart @@ -3,29 +3,101 @@ import 'package:flutter/material.dart'; import '../../base/i18n.dart'; +import '../../base/validators.dart'; +import '../../services/global_widget.dart'; import '../../setting/global_data.dart'; import '../../widget/attended_page.dart'; +import '../../widget/message_line.dart'; import '../../widget/widget_form.dart'; import 'create_user_page.dart'; final i18n = I18N(); -class CreateUserCustom extends State { +class CreateUserCustom extends State with MessageLine { final globalData = GlobalData(); AttendedPage? attendedPage; - + final _fieldData = _FieldData(); + final GlobalKey _formKey = + GlobalKey(debugLabel: 'CreateUser'); CreateUserCustom(); @override Widget build(BuildContext context) { - final padding = 16.0; + final padding = GlobalThemeData.padding; + comboRolesFromBackend( + attendedPage: attendedPage!, onDone: () => setState(() => 1)); + final itemsRole = comboRoles( + i18n.trDyn(GlobalTranslations.comboboxSelect), attendedPage!); + final formItems = [ + FormItem( + TextFormField( + initialValue: _fieldData.name, + style: TextStyle( + backgroundColor: GlobalThemeData.formTextBackgroundColor, + decorationStyle: TextDecorationStyle.double), + decoration: InputDecoration( + labelText: i18n.tr('Name'), + ), + validator: (input) => notEmpty(input), + onSaved: (value) => _fieldData.name = value ?? '', + ), + weight: 6), + FormItem( + TextFormField( + initialValue: _fieldData.displayName, + decoration: InputDecoration(labelText: i18n.tr('Display name')), + validator: (input) => notEmpty(input), + onSaved: (value) => _fieldData.displayName = value ?? '', + ), + weight: 6), + FormItem( + TextFormField( + initialValue: _fieldData.email, + decoration: InputDecoration(labelText: i18n.tr('Email')), + validator: (input) => validateMultiple( + input, [(input) => notEmpty(input), (input) => isEmail(input)]), + onSaved: (value) => _fieldData.email = value ?? '', + ), + weight: 6), + FormItem( + DropdownButtonFormField( + value: _fieldData.role, + items: itemsRole, + isExpanded: true, + decoration: InputDecoration(labelText: i18n.tr('Role')), + onChanged: (value) => _fieldData.role = value ?? 0, + ), + weight: 6), + FormItem( + ElevatedButton( + onPressed: () => verifyAndStore(), child: Text(i18n.tr('Save'))), + weight: 8, + gapAbove: padding), + FormItem( + ElevatedButton( + onPressed: () { + globalData.navigate(context, '/Users/list'); + }, + child: Text(i18n.tr('Cancel')), + ), + weight: 4) + ]; final rc = Scaffold( - appBar: globalData.appBarBuilder(i18n.tr('Change User data')), + appBar: globalData.appBarBuilder(i18n.tr('Create an User')), drawer: globalData.drawerBuilder(context), body: SafeArea( - child: WidgetForm.flexibleGridAttended( - attendedPage!.attendedWidgets(), - screenWidth: attendedPage!.pageStates.screenWidth, - padding: padding))); + child: Card( + color: GlobalThemeData.formBackgroundColor, + elevation: GlobalThemeData.formElevation, + margin: EdgeInsets.symmetric( + vertical: padding, horizontal: padding), + child: Padding( + padding: EdgeInsets.symmetric( + vertical: padding, horizontal: padding), + child: WidgetForm.flexibleGrid( + formItems, + screenWidth: attendedPage!.pageStates.screenWidth, + padding: padding, + ))))); return rc; } @@ -33,4 +105,46 @@ class CreateUserCustom extends State { void initState() { super.initState(); } + + void store() { + final parameters = { + 'module': 'Users', + 'sql': 'insert', + }; + _fieldData.toMap(parameters); + globalData.restPersistence! + .store(what: 'store', map: parameters) + .then((answer) { + if (answer.startsWith('id:')) { + final id = int.tryParse(answer.substring(3)); + if (id == null || id == 0) { + setError(i18n.tr('Saving data failed: $answer')); + setState(() => 1); + } else { + globalData.navigate(context, '/Users/edit;$id'); + } + } + }); + } + + void verifyAndStore() { + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + store(); + } + } +} + +class _FieldData { + String name = ''; + String displayName = ''; + String email = ''; + int role = 0; + void toMap(Map map) { + map[':name'] = name; + map[':displayName'] = displayName; + map[':email'] = email; + map[':role'] = role; + map[':createdBy'] = GlobalData.loginUserName; + } } diff --git a/lib/page/users/delete_user_custom.dart b/lib/page/users/delete_user_custom.dart index 7fa7866..7ab1bce 100644 --- a/lib/page/users/delete_user_custom.dart +++ b/lib/page/users/delete_user_custom.dart @@ -18,7 +18,7 @@ class DeleteUserCustom extends State { DeleteUserCustom(this.primaryKey); @override Widget build(BuildContext context) { - final padding = 16.0; + final padding = GlobalThemeData.padding; final rc = Scaffold( appBar: globalData.appBarBuilder(i18n.tr('Change User data')), drawer: globalData.drawerBuilder(context), diff --git a/lib/page/users/edit_user_custom.dart b/lib/page/users/edit_user_custom.dart index d4ab150..bf6fc64 100644 --- a/lib/page/users/edit_user_custom.dart +++ b/lib/page/users/edit_user_custom.dart @@ -3,30 +3,106 @@ import 'package:flutter/material.dart'; import '../../base/i18n.dart'; +import '../../base/validators.dart'; +import '../../services/global_widget.dart'; import '../../setting/global_data.dart'; import '../../widget/attended_page.dart'; +import '../../widget/message_line.dart'; import '../../widget/widget_form.dart'; import 'edit_user_page.dart'; final i18n = I18N(); -class EditUserCustom extends State { +class EditUserCustom extends State with MessageLine { final int primaryKey; final globalData = GlobalData(); AttendedPage? attendedPage; - + final _fieldData = _FieldData(); + final GlobalKey _formKey = + GlobalKey(debugLabel: 'EditUser'); + final nameController = TextEditingController(); + final displayNameController = TextEditingController(); + final emailController = TextEditingController(); EditUserCustom(this.primaryKey); @override Widget build(BuildContext context) { - final padding = 16.0; + final padding = GlobalThemeData.padding; + comboRolesFromBackend( + attendedPage: attendedPage!, onDone: () => setState(() => 1)); + final itemsRole = comboRoles( + i18n.trDyn(GlobalTranslations.comboboxSelect), attendedPage!); + attendedPage?.loadRecord( + name: 'record', + reload: () => setState(() => 1), + onDone: (record) => _fieldData.fromMap(record), + parameters: {'module': 'Users', 'sql': 'byId', ':id': primaryKey}); + nameController.text = _fieldData.name; + displayNameController.text = _fieldData.displayName; + emailController.text = _fieldData.email; + final formItems = [ + FormItem( + TextFormField( + controller: nameController, + decoration: InputDecoration(labelText: i18n.tr('Name')), + validator: (input) => notEmpty(input), + onSaved: (value) => _fieldData.name = value ?? '', + ), + weight: 6), + FormItem( + TextFormField( + controller: displayNameController, + decoration: InputDecoration(labelText: i18n.tr('Display name')), + validator: (input) => notEmpty(input), + onSaved: (value) => _fieldData.displayName = value ?? '', + ), + weight: 6), + FormItem( + TextFormField( + controller: emailController, + decoration: InputDecoration(labelText: i18n.tr('Email')), + validator: (input) => validateMultiple( + input, [(input) => notEmpty(input), (input) => isEmail(input)]), + onSaved: (value) => _fieldData.email = value ?? '', + ), + weight: 6), + FormItem( + DropdownButtonFormField( + value: _fieldData.role, + items: itemsRole, + isExpanded: true, + decoration: InputDecoration(labelText: i18n.tr('Role')), + onChanged: (value) => _fieldData.role = value ?? 0, + ), + weight: 6), + FormItem( + ElevatedButton( + onPressed: () => verifyAndStore(), child: Text(i18n.tr('Save'))), + weight: 8, + gapAbove: 2 * padding), + FormItem( + ElevatedButton( + onPressed: () { + attendedPage!.pageStates.dbDataState.clear(); + globalData.navigate(context, '/Users/list'); + }, + child: Text(i18n.tr('Cancel')), + ), + weight: 4) + ]; final rc = Scaffold( - appBar: globalData.appBarBuilder(i18n.tr('Change User data')), + appBar: globalData.appBarBuilder(i18n.tr('Change an User')), drawer: globalData.drawerBuilder(context), body: SafeArea( - child: WidgetForm.flexibleGridAttended( - attendedPage!.attendedWidgets(), - screenWidth: attendedPage!.pageStates.screenWidth, - padding: padding))); + child: Form( + key: _formKey, + child: Card( + margin: EdgeInsets.symmetric( + vertical: padding, horizontal: padding), + child: WidgetForm.flexibleGrid( + formItems, + screenWidth: attendedPage!.pageStates.screenWidth, + padding: padding, + ))))); return rc; } @@ -34,4 +110,60 @@ class EditUserCustom extends State { void initState() { super.initState(); } + + void store() { + final parameters = { + 'module': 'Users', + 'sql': 'update', + ':id': primaryKey, + ':name': _fieldData.name, + ':displayName': _fieldData.displayName, + ':email': _fieldData.email, + ':role': _fieldData.role, + ':createdBy': GlobalData.loginUserName, + }; + _fieldData.toMap(parameters); + globalData.restPersistence! + .store(what: 'store', map: parameters) + .then((answer) { + if (answer.startsWith('id:')) { + final id = int.tryParse(answer.substring(3)); + if (id == null || id == 0) { + setError(i18n.tr('Saving data failed: $answer')); + setState(() => 1); + } else { + attendedPage!.pageStates.dbDataState.clear(); + globalData.navigate(context, '/Users/edit;$id'); + } + } + }); + } + + void verifyAndStore() { + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + store(); + } + } +} + +class _FieldData { + String name = ''; + String displayName = ''; + String email = ''; + int role = 0; + void fromMap(Map map) { + name = map['user_name']; + displayName = map['user_displayname']; + email = map['user_email']; + role = map['user_role']; + } + + void toMap(Map map) { + map[':name'] = name; + map[':displayName'] = displayName; + map[':email'] = email; + map[':role'] = role; + map[':createdBy'] = GlobalData.loginUserName; + } } diff --git a/lib/page/users/list_user_custom.dart b/lib/page/users/list_user_custom.dart index c90b9ec..3cf2c19 100644 --- a/lib/page/users/list_user_custom.dart +++ b/lib/page/users/list_user_custom.dart @@ -6,39 +6,63 @@ import '../../base/i18n.dart'; import '../../setting/global_data.dart'; import '../../widget/attended_page.dart'; import '../../widget/widget_form.dart'; +import '../../services/global_widget.dart'; import 'list_user_page.dart'; final i18n = I18N(); class ListUserCustom extends State { final globalData = GlobalData(); - final fieldData = FieldData(); AttendedPage? attendedPage; + final _fieldData = _FieldData(); + final GlobalKey _formKey = + GlobalKey(debugLabel: 'CreateUser'); final textController = TextEditingController(); ListUserCustom(); @override Widget build(BuildContext context) { - final padding = 16.0; - final itemsRole = >[]; - final fieldWidgets = [ - TextFormField( - controller: textController, - decoration: InputDecoration(labelText: i18n.tr('Text')), - onChanged: (value) => fieldData.text = value, - ), - DropdownButtonFormField( - value: fieldData.role, - items: itemsRole, - isExpanded: true, - decoration: InputDecoration(labelText: i18n.tr('Role')), - onChanged: (value) => fieldData.role = value ?? 0, - ), + final padding = GlobalThemeData.padding; + comboRolesFromBackend( + attendedPage: attendedPage!, onDone: () => setState(() => 1)); + final itemsRole = comboRoles( + i18n.trDyn(GlobalTranslations.comboboxItemAll), attendedPage!); + final formItems = [ + FormItem( + TextFormField( + controller: textController, + decoration: InputDecoration(labelText: i18n.tr('Text')), + onChanged: (value) => _fieldData.text = value, + ), + weight: 6), + FormItem( + DropdownButtonFormField( + value: _fieldData.role, + items: itemsRole, + isExpanded: true, + decoration: InputDecoration(labelText: i18n.tr('Role')), + onChanged: (value) => _fieldData.role = value ?? 0, + ), + weight: 6), + FormItem( + ElevatedButton( + onPressed: () => search(), child: Text(i18n.tr('Search'))), + weight: 12, + gapAbove: padding) ]; - final weights = [6, 6]; - final form = WidgetForm.flexibleGrid(fieldWidgets, - weights: weights, - screenWidth: attendedPage!.pageStates.screenWidth, - padding: padding); + final form = Form( + key: _formKey, + child: Card( + color: GlobalThemeData.formBackgroundColor, + elevation: GlobalThemeData.formElevation, + margin: + EdgeInsets.symmetric(vertical: padding, horizontal: padding), + child: Padding( + padding: EdgeInsets.symmetric( + vertical: padding, horizontal: padding), + child: WidgetForm.flexibleGrid(formItems, + screenWidth: attendedPage!.pageStates.screenWidth, + padding: padding)))); + final rows = attendedPage!.getRows( columnList: 'user_id;user_displayname;user_email;role', what: 'query', @@ -46,10 +70,12 @@ class ListUserCustom extends State { 'module': 'Users', 'sql': 'list', 'offset': '0', - 'size': '10' + 'size': '10', + ':text': _fieldData.text, + ':role': _fieldData.role.toString(), }, onDone: () => setState(() => 1), - routeEdit: '/users/edit', + routeEdit: '/Users/edit', context: context); final table = DataTable( columns: [ @@ -74,7 +100,7 @@ class ListUserCustom extends State { SizedBox(width: double.infinity, child: table), ]); final rc = Scaffold( - appBar: globalData.appBarBuilder(i18n.tr('Change User data')), + appBar: globalData.appBarBuilder(i18n.tr('Overview users')), drawer: globalData.drawerBuilder(context), floatingActionButton: FloatingActionButton( onPressed: () { @@ -87,13 +113,16 @@ class ListUserCustom extends State { return rc; } + void search() { + //@ToDo + } @override void initState() { super.initState(); } } -class FieldData { +class _FieldData { String text = ''; int role = 0; } diff --git a/lib/services/global_widget.dart b/lib/services/global_widget.dart new file mode 100644 index 0000000..c826b27 --- /dev/null +++ b/lib/services/global_widget.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +import '../../base/i18n.dart'; +import '../setting/global_data.dart'; +import '../widget/attended_page.dart'; + +final i18n = I18N(); + +/// Returns the combobox items filled with data of the table roles. +/// +/// [textUndefined]: null: no additional entry. Otherwise: a additional entry is +/// inserted at position 0 with this text and the value 0. +/// +/// [attendedPage]: the context of the calling page. +/// +/// Returns a list of combobox items selecting a role. +List>? comboRoles( + String textUndefined, AttendedPage attendedPage) { + final rc = _combo( + 'comboRoles.global', 'role_id', 'role_name', textUndefined, attendedPage); + return rc; +} + +/// Requests the database data for combo boxes from the backend. +/// +/// [dbDataState]: Stores the request and the answer of the request. +/// +/// [onDone]: A method executed after arriving the response. +void comboRolesFromBackend( + {required AttendedPage attendedPage, required Function() onDone}) { + _requestData('comboRoles.global', attendedPage, + {'module': 'global', 'sql': 'comboRoles'}, onDone); +} + +List>? _combo(String name, String columnValue, + String columnText, String? textUndefined, AttendedPage attendedPage) { + final records = attendedPage.pageStates.dbDataState.listResponse(name); + final rc = records + .map((element) => DropdownMenuItem( + value: element[columnValue], + child: Text(i18n.trDyn(element[columnText])))) + .toList(growable: textUndefined != null); + if (textUndefined != null) { + rc.insert(0, DropdownMenuItem(value: 0, child: Text(textUndefined))); + } + return rc; +} + +/// Requests the data from the backend. +/// +/// The request is inserted into the +void _requestData(String name, AttendedPage attendedPage, + Map parameters, Function() onDone) { + if (!attendedPage.pageStates.dbDataState.hasEntry(name)) { + attendedPage.pageStates.dbDataState.addRequest(name).then((_) => + GlobalData() + .restPersistence! + .query(what: 'query', data: parameters) + .then((value) { + attendedPage.pageStates.dbDataState + .storeDbResult(name, value) + .then((_) => onDone()); + })); + } +} diff --git a/lib/setting/drawer_exhibition.dart b/lib/setting/drawer_exhibition.dart index d8a156d..1b46ee3 100644 --- a/lib/setting/drawer_exhibition.dart +++ b/lib/setting/drawer_exhibition.dart @@ -114,7 +114,7 @@ class MenuItem { final logger = globalData.logger; final collection = PageManager(); return [ - MenuItem('Users', () => collection.newPageByRoute('/users/list'), + MenuItem('Users', () => collection.newPageByRoute('/Users/list'), converter.iconByName('people_outlined', logger)), MenuItem('Protokoll', () => collection.newPageByRoute('/log'), converter.iconByName('line_weight_outlined', logger)), diff --git a/lib/setting/global_data.dart b/lib/setting/global_data.dart index e470cb0..bab2129 100644 --- a/lib/setting/global_data.dart +++ b/lib/setting/global_data.dart @@ -1,12 +1,11 @@ import 'dart:io'; import 'package:dart_bones/dart_bones.dart'; -import 'package:exhibition/page/page_manager.dart'; -import 'package:exhibition/persistence/rest_persistence.dart'; -import 'package:exhibition/widget/attended_page.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; +import '../persistence/rest_persistence.dart'; +import '../widget/attended_page.dart'; import '../base/application_name.dart'; import '../base/defines.dart'; import '../page/page_controller_exhibition.dart'; @@ -48,6 +47,18 @@ class HomeDirectories { } } +class GlobalTranslations { + static final comboboxItemAll = i18n.tr(''); + static final comboboxSelect = i18n.tr(''); +} + +class GlobalThemeData { + static final formBackgroundColor = Color.fromARGB(0xee, 0xee, 0xee, 0xee); + static final formElevation = 10.0; + static final formTextBackgroundColor = Colors.white; + static final padding = 16.0; +} + /// Storage for global resources. This is a singleton. class GlobalData { static const version = '2021.08.24.00'; @@ -61,8 +72,10 @@ class GlobalData { final FooterBuilder footerBuilder; final BaseConfiguration configuration; final RestPersistence? restPersistence; + static String loginUserName = 'guest'; + static int loginUserId = 0; + static int loginUserRole = 90; HomeDirectories homeDirectories = HomeDirectories.dummy(); - AttendedPage? currentPage; factory GlobalData() => _instance ?? GlobalData(); GlobalData.dummy() @@ -89,10 +102,6 @@ class GlobalData { /// Switches the page given by a [route]. void navigate(BuildContext context, String route) { - final page = PageManager().existingPageByRoute(route); - if (page != null) { - currentPage = page as AttendedPage; - } Navigator.pushNamed(context, route); } } diff --git a/lib/widget/attended_page.dart b/lib/widget/attended_page.dart index fa19c31..936318b 100644 --- a/lib/widget/attended_page.dart +++ b/lib/widget/attended_page.dart @@ -1,3 +1,4 @@ +import 'package:exhibition/base/defines.dart'; import 'package:flutter/material.dart'; import 'package:synchronized/synchronized.dart'; @@ -48,6 +49,32 @@ class AttendedPage { return rc; } + /// Returns a database record with a given [primaryKey]. + JsonMap? getRecord( + {required int primaryKey, + required String what, + required Map parameters, + required Function() onDone}) { + JsonMap? rc; + String name = 'record'; + if (!pageStates.dbDataState.hasEntry(name)) { + pageStates.dbDataState.addRequest(name).then((_) => globalData + .restPersistence! + .query(what: what, data: parameters) + .then((value) { + pageStates.dbDataState + .storeDbResult(name, value) + .then((_) => onDone()); + })); + } else { + final dbData = pageStates.dbDataState.dataOf(name); + if (dbData != null && dbData.singleRecord != null) { + rc = dbData.singleRecord!; + } + } + return rc; + } + /// Returns a list of DataRow instances to show the content of some /// database records in a [DataTable]. /// @@ -78,9 +105,10 @@ class AttendedPage { final cells = []; for (var column in columnList.split(';')) { final primaryKey = moduleMetaData.primaryOf()?.columnName; - cells.add(DataCell(Text(record[column].toString()), - onTap: () => globalData.navigate( - context, routeEdit + ';${record[primaryKey]}'))); + cells.add(DataCell(Text(record[column].toString()), onTap: () { + globalData.navigate( + context, routeEdit + ';${record[primaryKey]}'); + })); } final rc3 = DataRow(cells: cells); rc!.add(rc3); @@ -90,6 +118,38 @@ class AttendedPage { return rc ?? []; } + /// Loads a record from the backend. + /// + /// [name]: a unique name over all backend data requests. + /// + /// [reload]: a function calling setState(). + /// + /// [onDone]: a function storing the record fetched from the backend. + /// + /// [parameters]: a map specifying the request. + /// Example: { 'module': 'Roles', 'sql': 'byId', ':id': primaryKey}./// + void loadRecord( + {required String name, + required void Function() reload, + required void Function(JsonMap) onDone, + required Map parameters}) { + if (!pageStates.dbDataState.hasEntry(name)) { + pageStates.dbDataState.addRequest(name).then((_) => globalData + .restPersistence! + .query(what: 'query', data: parameters) + .then((value) { + pageStates.dbDataState + .storeDbResult(name, value) + .then((_) => reload()); + })); + } else if (pageStates.dbDataState.dataOf('record') != null) { + final dbData = pageStates.dbDataState.dataOf('record'); + if (dbData != null && dbData.singleRecord != null) { + onDone(dbData.singleRecord!); + } + } + } + /// Validates the text [value] of the [textAttended]. /// /// Returns null on success or the error message on error. @@ -138,12 +198,37 @@ class DbDataState { /// Tests whether the map contains an entry with [name]. bool hasEntry(String name) => dbDataMap.containsKey(name); + /// Tests whether the map with [name] contains a response from the backend. + bool hasRecordResponse(String name) => + dbDataMap.containsKey(name) && dbDataMap[name]!.singleRecord != null; + + /// Tests whether the map with [name] contains a response from the backend. + bool hasResponse(String name) { + final info = dbDataMap[name]; + final rc = info == null + ? false + : (info.recordList != null || + info.singleRecord != null || + info.message != null); + return rc; + } + /// Tests whether all requests are answered. /// /// Return true, if all answers of the requested DB data are stored in /// [dbDataMap]. bool isWaiting() => currentCount < expectedCount; + /// Returns the response of a given [name] or [] otherwise. + /// + /// Returns a list of records or []. + JsonList listResponse(String name) => dbDataMap[name]?.recordList ?? []; + + /// Returns the response of a given [name] or {} otherwise. + /// + /// Returns a record or {}. + JsonMap recordResponse(String name) => dbDataMap[name]?.singleRecord ?? {}; + /// Stores the DB data fetched from the backend. /// /// [name] is a (over all requests) unique name to identify the request. diff --git a/lib/widget/message_line.dart b/lib/widget/message_line.dart new file mode 100644 index 0000000..ed2f691 --- /dev/null +++ b/lib/widget/message_line.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +/// A mixin to handle a message line. +/// This message line is only visible if the message is not empty. +abstract class MessageLine { + String message = ''; + void setMessage(String message, {bool isError = true}) { + this.message = message; + } + + void setError(String message, {bool isError = true}) { + this.message = '+++ $message'; + } + + void addToList(List widgetList, double padding) { + if (message.isNotEmpty) { + widgetList.add(SizedBox(height: padding)); + widgetList.add(Text(message, style: TextStyle(color: Colors.red))); + } + } + + void addToFormList(List widgetList, double padding) { + if (message.isNotEmpty) { + widgetList.add(Text(message, style: TextStyle(color: Colors.red))); + } + } +} diff --git a/lib/widget/widget_form.dart b/lib/widget/widget_form.dart index b820fae..9a86b35 100644 --- a/lib/widget/widget_form.dart +++ b/lib/widget/widget_form.dart @@ -1,6 +1,21 @@ import 'package:exhibition/widget/attended_widget.dart'; import 'package:flutter/material.dart'; +class FormItem { + final Widget widget; + final int weight; + final double gapAbove; + + /// Constructor. + /// + /// [weight]: defines the widget width in a 12 column model: 6 means standard + /// width in a 2 column form. + /// + /// [gapAbove]: ignored if 0.0. Otherwise the widget always starts a row + /// and the row has that distance to the prior row or top of the form. + FormItem(this.widget, {required this.weight, this.gapAbove = 0.0}); +} + class WidgetForm { /// Places the widgets in a flexible grid. /// @@ -9,30 +24,36 @@ class WidgetForm { /// The place of the row is divided in 12 segments with the same width. /// Each widget has a "weight" which means the count of segments to use. /// - /// [widgets] is the list of widgets to position in the grid. + /// [formItems] is the list of widgets to position in the grid. /// /// [screenWidth] is the width of the output device in pixel. /// /// [minWidth] is the minimum width of the above defined segment. /// If the [screenwidth] has not place for 6 segments all widgets are placed /// in a single row. - static Widget flexibleGrid(List widgets, - {required List weights, - required double screenWidth, + static Widget flexibleGrid(List formItems, + {required double screenWidth, double minWidth = 800, double padding = 16.0}) { Widget rc; - assert(widgets.length == weights.length); if (minWidth > screenWidth) { + final widgets = []; + formItems.forEach((element) { + if (element.gapAbove > 0) { + widgets.add(SizedBox(height: element.gapAbove)); + } + widgets.add(element.widget); + }); rc = Column(children: widgets); } else { int position = 0; final List childrenColumn = []; List childrenRow = []; - for (var ix = 0; ix < widgets.length; ix++) { - final widget = widgets[ix]; - final flex = weights[ix]; - if (position + flex <= 12) { + for (var ix = 0; ix < formItems.length; ix++) { + final widget = formItems[ix].widget; + final flex = formItems[ix].weight; + final gap = formItems[ix].gapAbove; + if (position + flex <= 12 && gap <= 0.0) { var child2 = position == 0 ? widget : Row( @@ -42,11 +63,14 @@ class WidgetForm { childrenRow.add(Expanded(flex: flex, child: child2)); position += flex; } else { - if (position < 12) { + if (position < 12 || gap > 0.0) { childrenRow .add(Expanded(flex: 12 - position, child: SizedBox(width: 1))); } position = 0; + if (childrenColumn.isNotEmpty || gap > 0.0) { + childrenColumn.add(SizedBox(height: gap == 0.0 ? padding : gap)); + } childrenColumn.add(Row(children: childrenRow)); childrenRow = []; final child2 = position == 0 @@ -84,13 +108,17 @@ class WidgetForm { /// in a single row. static Widget flexibleGridAttended(List widgets, {required double screenWidth, + List? buttons, double minWidth = 800, double padding = 16.0}) { - final weights = - widgets.map((element) => element.propertyMetaData.weight).toList(); - final widgets2 = widgets.map((element) => element.widgetOf()).toList(); - final rc = - flexibleGrid(widgets2, weights: weights, screenWidth: screenWidth); + final widgets2 = widgets + .map((element) => FormItem(element.widgetOf(), + weight: element.propertyMetaData.weight)) + .toList(); + if (buttons != null) { + widgets2.addAll(buttons); + } + final rc = flexibleGrid(widgets2, screenWidth: screenWidth); return rc; } } diff --git a/metatool/bin/page_generator.dart b/metatool/bin/page_generator.dart index 369b373..c241876 100644 --- a/metatool/bin/page_generator.dart +++ b/metatool/bin/page_generator.dart @@ -62,7 +62,7 @@ DEF_PRIMARY final globalData = GlobalData(); EditUserCustom(THIS_PRIMARY); @override Widget build(BuildContext context) { - final padding = 16.0; + final padding = GlobalThemeData.padding; final rc = Scaffold( appBar: globalData.appBarBuilder(i18n.tr('Change User data')), drawer: globalData.drawerBuilder(context), diff --git a/metatool/bin/sql_generator.dart b/metatool/bin/sql_generator.dart index bce0547..ecb791f 100644 --- a/metatool/bin/sql_generator.dart +++ b/metatool/bin/sql_generator.dart @@ -1,9 +1,9 @@ import 'dart:io'; import 'package:dart_bones/dart_bones.dart'; + import '../lib/meta/module_meta_data.dart'; import '../lib/meta/modules.dart'; - import 'generator.dart'; import 'generator_base.dart'; @@ -54,11 +54,11 @@ JOINS ;" byId: type: record - parameters: [ "${list[0].name}" ] + parameters: [ ":${list[0].name}" ] sql: "SELECT * FROM $tableName WHERE ${list[0].columnName}=:${list[0].name};" delete: type: delete - parameters: [ "${list[0].name}" ] + parameters: [ ":${list[0].name}" ] sql: "DELETE * FROM $tableName WHERE ${list[0].columnName}=:${list[0].name};" update: type: update @@ -68,19 +68,25 @@ update: var items = module.standardColumns('changedBy'); var parameters = addToBuffer(' parameters: [', maxLength: 80); var assignments = addToBuffer(' ', maxLength: 80); - var first = true; + var firstParameter = true; + var firstAssignment = true; for (var item in items) { - addToBuffer('":${item.name}"', - maxLength: 80, - separator: first ? null : ',', - indent: 4, - buffer: parameters); - addToBuffer('${item.columnName}=:${item.name}', - maxLength: 80, - separator: first ? null : ',', - indent: 4, - buffer: assignments); - first = false; + if (!item.hasOption('hidden')) { + addToBuffer('":${item.name}"', + maxLength: 80, + separator: firstParameter ? null : ',', + indent: 4, + buffer: parameters); + firstParameter = false; + if (!item.hasOption('primary')) { + addToBuffer('${item.columnName}=:${item.name}', + maxLength: 80, + separator: firstAssignment ? null : ',', + indent: 4, + buffer: assignments); + firstAssignment = false; + } + } } parameters.write(']'); var item = module.properties['changed']!; @@ -97,24 +103,26 @@ update: parameters = addToBuffer(' parameters: [', maxLength: 80); final sql1 = addToBuffer(' sql: "INSERT INTO $tableName(', maxLength: 80); final sql2 = addToBuffer(' VALUES(', maxLength: 80); - first = true; + var first = true; for (var item in items) { - addToBuffer('":${item.name}"', - maxLength: 80, - separator: first ? null : ',', - indent: 4, - buffer: parameters); - addToBuffer('${item.columnName}', - maxLength: 80, - separator: first ? null : ',', - indent: 6, - buffer: sql1); - addToBuffer(':${item.name}', - maxLength: 80, - separator: first ? null : ',', - indent: 6, - buffer: sql2); - first = false; + if (!item.hasOption('primary')) { + addToBuffer('":${item.name}"', + maxLength: 80, + separator: first ? null : ',', + indent: 4, + buffer: parameters); + addToBuffer('${item.columnName}', + maxLength: 80, + separator: first ? null : ',', + indent: 6, + buffer: sql1); + addToBuffer(':${item.name}', + maxLength: 80, + separator: first ? null : ',', + indent: 6, + buffer: sql2); + first = false; + } } parameters.write(']'); item = module.properties['created']!; @@ -122,7 +130,7 @@ update: maxLength: 80, separator: ',', indent: 4, buffer: sql1); addToBuffer('NOW());"', maxLength: 80, separator: ',', indent: 4, buffer: sql2); - // parameters: [":id", ":name", ":displayname", ":email", ":changedby"] + // parameters: [":name", ":displayname", ":email", ":changedby"] buffer.write('''insert: type: insert '''); @@ -132,20 +140,6 @@ update: return buffer.toString(); } - /// Generates the Sql statement file for each module defined in the meta data. - void updateSql(Generator generator) { - final modules = moduleNames(); - for (var name in modules) { - ModuleMetaData? module = moduleByName(name); - if (module == null) { - logger.error('+++ unknown module: $name'); - } else { - logger.log('current directory: ${Directory.current.path}'); - String filename = '../rest_server/data/sql/${name.toLowerCase()}.sql.yaml'; - writeFile(filename, generator.createSqlStatements(module)); - } - } - } /// Handles the foreign keys in the [sqlText] of the given [module]. /// /// Replaces the placeholders SELECTS and JOINS in [sqlText]. @@ -155,16 +149,18 @@ update: var joins = ''; var selects = ''; var referenceNo = 0; - for (var property in module.propertyList){ - if (property.foreignKey != null){ + for (var property in module.propertyList) { + if (property.foreignKey != null) { // foreignKey: 'roles.role_id;role_name;role' final parts = property.foreignKey!.split(';'); - if (parts.length != 3){ - logger.error('wrong foreignKey format: ${property.foreignKey} in ${module.moduleName}'); + if (parts.length != 3) { + logger.error( + 'wrong foreignKey format: ${property.foreignKey} in ${module.moduleName}'); } else { final keyParts = parts[0].split('.'); - if (keyParts.length != 2){ - logger.error('wrong foreignKey format: ${property.foreignKey} in ${module.moduleName}'); + if (keyParts.length != 2) { + logger.error( + 'wrong foreignKey format: ${property.foreignKey} in ${module.moduleName}'); } else { ++referenceNo; joins += ' JOIN ${keyParts[0]} t$referenceNo ON ' @@ -174,7 +170,24 @@ update: } } } - final rc = sqlText.replaceFirst('JOINS', joins).replaceFirst('SELECTS', selects); + final rc = + sqlText.replaceFirst('JOINS', joins).replaceFirst('SELECTS', selects); return rc; } + + /// Generates the Sql statement file for each module defined in the meta data. + void updateSql(Generator generator) { + final modules = moduleNames(); + for (var name in modules) { + ModuleMetaData? module = moduleByName(name); + if (module == null) { + logger.error('+++ unknown module: $name'); + } else { + logger.log('current directory: ${Directory.current.path}'); + String filename = + '../rest_server/data/sql/${name.toLowerCase()}.sql.yaml'; + writeFile(filename, generator.createSqlStatements(module)); + } + } + } } diff --git a/rest_server/data/sql/precedence/global.sql.yaml b/rest_server/data/sql/precedence/global.sql.yaml new file mode 100644 index 0000000..3dde806 --- /dev/null +++ b/rest_server/data/sql/precedence/global.sql.yaml @@ -0,0 +1,18 @@ +--- +# This file contains contains SQL queries/requests +# that can be used by all projects derived from exhibition. +# +# Note: There is a file local.sql.yaml for usage only in one project. +# +# This file should be maintained by the programmers. + +module: global +comboRoles: + type: list + parameters: [] + sql: "SELECT role_id, role_name from roles order by role_name;" +comboActiveUsers: + type: list + parameters: [] + sql: "SELECT user_id, user_displayname from users + WHERE user_status = 1 ORDER BY by user_displayname;" diff --git a/rest_server/data/sql/precedence/local.sql.yaml b/rest_server/data/sql/precedence/local.sql.yaml new file mode 100644 index 0000000..ecf0e2d --- /dev/null +++ b/rest_server/data/sql/precedence/local.sql.yaml @@ -0,0 +1,7 @@ +--- +# This file contains contains SQL queries/requests +# that should be used only by the current project (derived from exhibition). +# +# Note: There is a file global.sql.yaml for usage in all projects (derived from exhibition). +# +# This file should be maintained by the programmers. diff --git a/rest_server/data/sql/roles.sql.yaml b/rest_server/data/sql/roles.sql.yaml index 8e5f31c..616389e 100644 --- a/rest_server/data/sql/roles.sql.yaml +++ b/rest_server/data/sql/roles.sql.yaml @@ -13,20 +13,20 @@ list: ;" byId: type: record - parameters: [ "id" ] + parameters: [ ":id" ] sql: "SELECT * FROM roles WHERE role_id=:id;" delete: type: delete - parameters: [ "id" ] + parameters: [ ":id" ] sql: "DELETE * FROM roles WHERE role_id=:id;" update: type: update - parameters: [":id",":name",":changedBy"] + parameters: [":id",":name"] sql: "UPDATE roles SET - role_id=:id,role_name=:name,role_changedby=:changedBy,role_changed=NOW() + role_name=:name,role_changed=NOW() WHERE role_id=:id;" insert: type: insert - parameters: [":id",":name",":createdBy"] - sql: "INSERT INTO roles(role_id,role_name,role_createdby,role_created) - VALUES(:id,:name,:createdBy,NOW());" + parameters: [":name",":createdBy"] + sql: "INSERT INTO roles(role_name,role_createdby,role_created) + VALUES(:name,:createdBy,NOW());" diff --git a/rest_server/data/sql/structures.sql.yaml b/rest_server/data/sql/structures.sql.yaml index f09192a..111f673 100644 --- a/rest_server/data/sql/structures.sql.yaml +++ b/rest_server/data/sql/structures.sql.yaml @@ -13,23 +13,22 @@ list: ;" byId: type: record - parameters: [ "id" ] + parameters: [ ":id" ] sql: "SELECT * FROM structures WHERE structure_id=:id;" delete: type: delete - parameters: [ "id" ] + parameters: [ ":id" ] sql: "DELETE * FROM structures WHERE structure_id=:id;" update: type: update - parameters: [":id",":scope",":name",":value",":position",":changedBy"] + parameters: [":id",":scope",":name",":value",":position"] sql: "UPDATE structures SET - structure_id=:id,structure_scope=:scope,structure_name=:name, - structure_value=:value,structure_position=:position, - structure_changedby=:changedBy,structure_changed=NOW() + structure_scope=:scope,structure_name=:name,structure_value=:value, + structure_position=:position,structure_changed=NOW() WHERE structure_id=:id;" insert: type: insert - parameters: [":id",":scope",":name",":value",":position",":createdBy"] - sql: "INSERT INTO structures(structure_id,structure_scope,structure_name, - structure_value,structure_position,structure_createdby,structure_created) - VALUES(:id,:scope,:name,:value,:position,:createdBy,NOW());" + parameters: [":scope",":name",":value",":position",":createdBy"] + sql: "INSERT INTO structures(structure_scope,structure_name,structure_value, + structure_position,structure_createdby,structure_created) + VALUES(:scope,:name,:value,:position,:createdBy,NOW());" diff --git a/rest_server/data/sql/users.sql.yaml b/rest_server/data/sql/users.sql.yaml index 67cddba..7050e27 100644 --- a/rest_server/data/sql/users.sql.yaml +++ b/rest_server/data/sql/users.sql.yaml @@ -13,22 +13,22 @@ list: ;" byId: type: record - parameters: [ "id" ] + parameters: [ ":id" ] sql: "SELECT * FROM users WHERE user_id=:id;" delete: type: delete - parameters: [ "id" ] + parameters: [ ":id" ] sql: "DELETE * FROM users WHERE user_id=:id;" update: type: update - parameters: [":id",":name",":displayName",":email",":role",":changedBy"] + parameters: [":id",":name",":displayName",":email",":role"] sql: "UPDATE users SET - user_id=:id,user_name=:name,user_displayname=:displayName,user_email=:email, - user_role=:role,user_changedby=:changedBy,user_changed=NOW() + user_name=:name,user_displayname=:displayName,user_email=:email, + user_role=:role,user_changed=NOW() WHERE user_id=:id;" insert: type: insert - parameters: [":id",":name",":displayName",":email",":role",":createdBy"] - sql: "INSERT INTO users(user_id,user_name,user_displayname,user_email, - user_role,user_createdby,user_created) - VALUES(:id,:name,:displayName,:email,:role,:createdBy,NOW());" + parameters: [":name",":displayName",":email",":role",":createdBy"] + sql: "INSERT INTO users(user_name,user_displayname,user_email,user_role, + user_createdby,user_created) + VALUES(:name,:displayName,:email,:role,:createdBy,NOW());" diff --git a/rest_server/lib/sql_storage.dart b/rest_server/lib/sql_storage.dart index 8a18a64..9f17920 100644 --- a/rest_server/lib/sql_storage.dart +++ b/rest_server/lib/sql_storage.dart @@ -86,7 +86,7 @@ class SqlStatement { if (!map.containsKey(parameter)) { throw SqlException('${toString()}: missing parameter "$parameter"'); } else { - parameters.add(map[parameter]); + parameters.add(map[parameter].toString()); } } return sqlPrepared;