import 'package:data_table_2/data_table_2.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_fast_forms/flutter_fast_forms.dart'; import 'package:get/get.dart'; import 'package:omni_datetime_picker/omni_datetime_picker.dart'; import 'package:recase/recase.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:tuuli_api/tuuli_api.dart'; import 'package:tuuli_app/api_controller.dart'; import 'package:tuuli_app/models/db_column_definition.dart'; import 'package:tuuli_app/models/user_definition.dart'; import 'package:tuuli_app/utils.dart'; import 'package:tuuli_app/widgets/data_input_dialog.dart'; class OpenTableController extends GetxController { final TableDefinition table; OpenTableController({required this.table}); @override void onInit() { super.onInit(); refreshTableData(); } final _userCache = {}.obs; UserDefinition? getUserFromCache(int id) { return _userCache[id]; } void putUserInCache(UserDefinition user) { _userCache[user.id] = user; } final _assetsCache = {}.obs; Asset? getAssetFromCache(int id) { return _assetsCache[id]; } void putAssetInCache(Asset asset) { _assetsCache[asset.id] = asset; } final _tableData = >[].obs; List> get tableData => _tableData; final _newRowData = {}.obs; Map get newRowData => _newRowData; void setNewRowData(String key, dynamic value) { _newRowData[key] = value; } void clearNewRowData() { _newRowData.clear(); } Future refreshTableData() async { try { final resp = await ApiController.to.apiClient.getItemsFromTable( tableName: table.tableName, itemsSelector: const ItemsSelector( fields: [ "*", ], where: [], ), ); final respData = resp.data; if (respData == null) { throw Exception("No data in response"); } _tableData.clear(); _tableData.addAll(respData); } on DioError catch (e) { final respData = e.response?.data; if (respData != null) { Get.snackbar( "Error trying to get table data", "${respData['error']}", ); } else { Get.snackbar( "Error trying to get table data", "$e", ); } } catch (e) { Get.snackbar( "Error trying to get table data", "$e", ); } } Future showUserPicker() async { final username = "".obs; final user = await Get.dialog( AlertDialog( title: const Text("Select user"), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( onChanged: (value) { username.value = value; }, decoration: const InputDecoration( labelText: "Username", ), ), Obx( () => FutureBuilder>( future: () async { if (username.value.isEmpty) { return []; } final resp = await ApiController.to.apiClient.getItemsFromTable( tableName: "users", itemsSelector: ItemsSelector( fields: [ "id", "username", ], where: [ ColumnConditionCompat( column: "username", operator_: ColumnConditionCompatOperator.contains, value: username.value, ) ], )); final respData = resp.data; if (respData == null) { throw Exception("No data in response"); } return respData .map((e) => UserDefinition( id: e["id"], username: e["username"], password: "", accessToken: "", )) .toList(growable: false); }(), initialData: const [], builder: (context, snapshot) { if (snapshot.hasError) { return Text("${snapshot.error}"); } if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } final users = snapshot.data!; return SingleChildScrollView( child: Column( children: [ for (final user in users) ListTile( title: Text(user.username), onTap: () { Get.back(result: user); }, ), ], ), ); }, ), ), ], ), actions: [ TextButton( onPressed: () { Get.back(result: false); }, child: const Text("Cancel"), ), ], ), ); return user; } Future showAssetPicker() async { final name = "".obs; final asset = await Get.dialog( AlertDialog( title: const Text("Select asset"), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( onChanged: (value) { name.value = value; }, decoration: const InputDecoration( labelText: "Filename", ), ), Obx( () => FutureBuilder>( future: () async { if (name.value.isEmpty) { return []; } final resp = await ApiController.to.apiClient.getItemsFromTable( tableName: "assets", itemsSelector: ItemsSelector( fields: [ "id", "name", "description", ], where: [ ColumnConditionCompat( column: "name", operator_: ColumnConditionCompatOperator.contains, value: name.value, ) ], )); final respData = resp.data; if (respData == null) { throw Exception("No data in response"); } return respData .map((e) => Asset( id: e["id"], name: e["name"], description: e["description"], fid: "", tags: "", )) .toList(growable: false); }(), initialData: const [], builder: (context, snapshot) { if (snapshot.hasError) { return Text("${snapshot.error}"); } if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } final assets = snapshot.data!; return SingleChildScrollView( child: Column( children: [ for (final asset in assets) ListTile( title: Text(asset.name), subtitle: Text(asset.description), onTap: () { Get.back(result: asset); }, ), ], ), ); }, ), ), ], ), actions: [ TextButton( onPressed: () { Get.back(result: null); }, child: const Text("Cancel"), ), ], ), ); return asset; } Future addNewRow() async { try { final resp = await ApiController.to.apiClient.createItem( tableName: table.tableName, itemDefinition: convertToPayload(newRowData), ); final respData = resp.data; if (respData == null) { throw Exception("No data in response"); } clearNewRowData(); refreshTableData(); } on DioError catch (e) { final respData = e.response?.data; if (respData != null) { Get.snackbar( "Error trying to get table data", "${respData['error']}", ); } else { Get.snackbar( "Error trying to get table data", "$e", ); } } catch (e) { Get.snackbar( "Error trying to get table data", "$e", ); } } Future updateItem( Map originalItem, String columnName, dynamic data, ) async { final idCol = table.parsedColumns .firstWhereOrNull((e) => e is PrimarySerialColumnDefinition); try { final resp = await ApiController.to.apiClient.updateItemInTable( tableName: table.tableName, itemUpdate: ItemUpdate( oldItem: idCol == null ? convertToPayload(originalItem) : { idCol.name: originalItem[idCol.name], }, item: convertToPayload({columnName: data}), ), ); final respData = resp.data; if (respData == null) { throw Exception("No data in response"); } refreshTableData(); } on DioError catch (e) { final respData = e.response?.data; if (respData != null) { Get.snackbar( "Error trying to update table data", "${respData['error']}", ); } else { Get.snackbar( "Error trying to update table data", "$e", ); } } catch (e) { Get.snackbar( "Error trying to update table data", "$e", ); } } Future deleteItem(Map e) async { final idCol = table.parsedColumns .firstWhereOrNull((e) => e is PrimarySerialColumnDefinition); try { final resp = await ApiController.to.apiClient.deleteItemFromTable( tableName: table.tableName, columnConditionCompat: [ if (idCol != null) ColumnConditionCompat( column: idCol.name, operator_: ColumnConditionCompatOperator.eq, value: e[idCol.name], ) else ...convertToPayload(e).entries.map( (e) => ColumnConditionCompat( column: e.key, operator_: ColumnConditionCompatOperator.eq, value: e.value, ), ), ], ); final respData = resp.data; if (respData == null) { throw Exception("No data in response"); } refreshTableData(); } on DioError catch (e) { final respData = e.response?.data; if (respData != null) { Get.snackbar( "Error trying to update table data", "${respData['error']}", ); } else { Get.snackbar( "Error trying to update table data", "$e", ); } } catch (e) { Get.snackbar( "Error trying to update table data", "$e", ); } } } class OpenTableDialog extends GetView { const OpenTableDialog({super.key}); @override Widget build(BuildContext context) { return AlertDialog( title: Row( children: [ Text(controller.table.tableName.pascalCase), const Spacer(), IconButton( onPressed: () => controller.refreshTableData(), icon: const Icon(Icons.refresh), ), IconButton( onPressed: () { Get.back(); }, icon: const Icon(Icons.close), ), ], ), content: Obx( () => DataTable2( columns: [ for (final col in controller.table.parsedColumns) DataColumn( label: Text( col.name.pascalCase, ), ), const DataColumn(label: Text("Actions")), ], empty: const Text("No data"), rows: [ DataRow( cells: [ for (final col in controller.table.parsedColumns) if (col is PrimarySerialColumnDefinition) const DataCell( Text( "AUTO", ), ) else if (col is TextColumnDefinition) DataCell( Obx( () => TextField( controller: TextEditingController( text: controller.newRowData[col.name] ?? ""), decoration: InputDecoration( label: Text(col.name.pascalCase), ), onChanged: (value) => controller.setNewRowData( col.name, value, ), ), ), ) else if (col is BooleanColumnDefinition) DataCell( Obx( () => Row( children: [ Checkbox( value: controller.newRowData[col.name] ?? false, onChanged: (value) => controller.setNewRowData( col.name, value, ), ), ], ), ), ) else if (col is TimestampColumnDefinition) DataCell( Obx( () => TextField( controller: TextEditingController( text: () { final dt = controller.newRowData[col.name]; if (dt == null || dt is! DateTime) { return ""; } return postgresDateFormat(dt); }(), ), decoration: InputDecoration( label: Text(col.name.pascalCase), ), readOnly: true, onTap: () async { final dt = await showOmniDateTimePicker( context: context, is24HourMode: true, isForce2Digits: true, ); if (dt != null) { controller.setNewRowData( col.name, dt, ); } }, ), ), ) else if (col is DoubleColumnDefinition) DataCell( Obx( () => TextField( controller: TextEditingController( text: (controller.newRowData[col.name] as double?) ?.toString() ?? ""), decoration: InputDecoration( label: Text(col.name.pascalCase), ), onChanged: (value) => controller.setNewRowData( col.name, double.tryParse(value), ), ), ), ) else if (col is IntegerColumnDefinition) DataCell( Obx( () => TextField( controller: TextEditingController( text: (controller.newRowData[col.name] as int?) ?.toString() ?? ""), decoration: InputDecoration( label: Text(col.name.pascalCase), ), onChanged: (value) => controller.setNewRowData( col.name, int.tryParse(value), ), ), ), ) else if (col is UserRefColumnDefinition) DataCell(Obx( () => TextField( controller: TextEditingController( text: (controller.newRowData[col.name] as UserDefinition?) ?.username ?? ""), decoration: InputDecoration( label: Text(col.name.pascalCase), ), readOnly: true, onTap: () async { final user = await controller.showUserPicker(); if (user == null) return; controller.setNewRowData( col.name, user, ); }, ), )) else if (col is AssetRefColumnDefinition) DataCell(Obx( () => TextField( controller: TextEditingController( text: (controller.newRowData[col.name] as Asset?) ?.name ?? ""), decoration: InputDecoration( label: Text(col.name.pascalCase), ), readOnly: true, onTap: () async { final asset = await controller.showAssetPicker(); if (asset == null) return; controller.setNewRowData( col.name, asset, ); }, ), )) else DataCell.empty, DataCell(Row( children: [ IconButton( onPressed: () { controller.addNewRow(); }, icon: const Icon(Icons.add), ), IconButton( onPressed: () => controller.clearNewRowData(), icon: const Icon(Icons.clear), ), ], )), ], ), ...controller.tableData.map((e) { return DataRow( cells: [ for (final col in controller.table.parsedColumns) if (col is PrimarySerialColumnDefinition) DataCell( Tooltip( message: e[col.name].toString(), child: Text( e[col.name].toString(), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ) else if (col is TextColumnDefinition) DataCell( Tooltip( message: e[col.name].toString(), child: Text( e[col.name].toString(), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), onDoubleTap: () async { final text = await showStringInputDialog( originalValue: e[col.name].toString()); if (text != null) { await controller.updateItem( e, col.name, text, ); } }, ) else if (col is BooleanColumnDefinition) DataCell( Checkbox( value: e[col.name], onChanged: (v) async { await controller.updateItem( e, col.name, v ?? false, ); }, ), ) else if (col is TimestampColumnDefinition) DataCell( () { final msg = () { final dt = e[col.name]; if (dt == null) { return "#error#"; } if (dt is String) { final rdt = DateTime.parse(dt); return postgresDateFormat(rdt); } return postgresDateFormat(dt); }(); return Tooltip( message: msg, child: Text( msg, overflow: TextOverflow.ellipsis, maxLines: 1, ), ); }(), onDoubleTap: () async { final dt = await showOmniDateTimePicker( context: context, is24HourMode: true, isForce2Digits: true, ); if (dt != null) { await controller.updateItem( e, col.name, dt, ); } }, ) else if (col is DoubleColumnDefinition) DataCell( Tooltip( message: e[col.name].toString(), child: Text( e[col.name].toString(), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), onDoubleTap: () async { final dblVal = await showDoubleInputDialog( originalValue: e[col.name]); if (dblVal != null) { await controller.updateItem( e, col.name, dblVal, ); } }, ) else if (col is IntegerColumnDefinition) DataCell( Tooltip( message: e[col.name].toString(), child: Text( e[col.name].toString(), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), onDoubleTap: () async { final intVal = await showIntInputDialog( originalValue: e[col.name]); if (intVal != null) { await controller.updateItem( e, col.name, intVal, ); } }, ) else if (col is UserRefColumnDefinition) DataCell( FutureBuilder( future: () async { final cachedUser = controller.getUserFromCache(e[col.name]); if (cachedUser != null) { return cachedUser.username; } final user = await ApiController.to.apiClient .getItemsFromTable( tableName: "users", itemsSelector: ItemsSelector( fields: ["username"], where: [ ColumnConditionCompat( column: "id", operator_: ColumnConditionCompatOperator.eq, value: e[col.name], ), ], ), ); final ud = user.data; if (ud == null || ud.isEmpty || ud.first["username"] == null) { return "#error#"; } controller.putUserInCache(UserDefinition( id: e[col.name], username: ud.first["username"], password: "", accessToken: "", )); return ud.first["username"].toString(); }(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } return Tooltip( message: snapshot.data ?? "#error#", child: Text( snapshot.data ?? "#error#", overflow: TextOverflow.ellipsis, maxLines: 1, ), ); }, ), onDoubleTap: () async { final user = await controller.showUserPicker(); if (user != null) { await controller.updateItem( e, col.name, user.id, ); } }, ) else if (col is AssetRefColumnDefinition) DataCell( FutureBuilder( future: () async { final cachedAsset = controller.getAssetFromCache(e[col.name]); if (cachedAsset != null) { return cachedAsset.name; } final asset = await ApiController.to.apiClient .getItemsFromTable( tableName: "assets", itemsSelector: ItemsSelector( fields: ["name"], where: [ ColumnConditionCompat( column: "id", operator_: ColumnConditionCompatOperator.eq, value: e[col.name], ), ], ), ); final ad = asset.data; if (ad == null || ad.isEmpty || ad.first["name"] == null) { return "#error#"; } controller.putAssetInCache(Asset( id: e[col.name], name: ad.first["name"], description: "", fid: "", tags: "", )); return ad.first["name"].toString(); }(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } return Tooltip( message: snapshot.data ?? "#error#", child: Text( snapshot.data ?? "#error#", overflow: TextOverflow.ellipsis, maxLines: 1, ), ); }, ), onDoubleTap: () async { final asset = await controller.showAssetPicker(); if (asset != null) { await controller.updateItem( e, col.name, asset.id, ); } }, ) else DataCell.empty, DataCell(Row( children: [ IconButton( onPressed: () => controller.deleteItem(e), icon: const Icon(Icons.delete), ), ], )), ], ); }), ], ), ).constrained(width: Get.width * 0.9, height: Get.height * 0.9), ); } static Future show(TableDefinition table) async { Get.lazyPut(() => OpenTableController(table: table)); await Get.dialog( const OpenTableDialog(), barrierDismissible: false, ); } }