import 'package:data_table_2/data_table_2.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart' hide Response; 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/group_definition.dart'; import 'package:tuuli_app/models/table_access.dart'; class GroupACLController extends GetxController { final GroupDefinition group; GroupACLController(this.group); @override void onInit() { super.onInit(); refreshData(); } final _tables = [].obs; List get tables => _tables.toList(); final _access = {}.obs; Map get access => _access; final _allowedColumns = {}.obs; Map get allowedColumns => _allowedColumns; Future refreshData() async { await refreshTables(); for (final table in tables) { _access[table.tableId] = TableAccess.none; } for (final table in tables) { await refreshTableAccess(table); } } Future refreshTables() async { try { final resp = await ApiController.to.apiClient.listTables(); final respData = resp.data; if (respData == null) { throw Exception("В ответе нет данных"); } _tables.clear(); _tables.addAll(respData.where((e) => e.hidden != true)); } on DioError catch (e) { final respData = e.response?.data; if (respData != null) { Get.snackbar( "Ошибка запроса таблиц", "${respData['error']}", ); } else { Get.snackbar( "Ошибка запроса таблиц", "$e", ); } } catch (e) { Get.snackbar( "Ошибка запроса таблиц", "$e", ); } } Future refreshTableAccess(TableDefinition table) async { try { final resp = await ApiController.to.apiClient.getItemsFromTable( tableName: "table_access", itemsSelector: ItemsSelector( fields: ["access_type", "allowed_columns"], where: [ ColumnConditionCompat( column: "user_group_id", operator_: ColumnConditionCompatOperator.eq, value: group.id, ), ColumnConditionCompat( column: "table_name", operator_: ColumnConditionCompatOperator.eq, value: table.tableName, ), ], ), ); final respData = resp.data; if (respData == null) { throw Exception("В ответе нет данных"); } if (respData.isNotEmpty) { _access[table.tableId] = TableAccess.fromString(respData.first["access_type"]); _allowedColumns[table.tableId] = respData.first["allowed_columns"]; } else { _access[table.tableId] = TableAccess.none; _allowedColumns[table.tableId] = null; } } on DioError catch (e) { final respData = e.response?.data; if (respData != null) { Get.snackbar( "Ошибка запроса информации доступа к таблицам", "${respData['error']}", ); } else { Get.snackbar( "Ошибка запроса информации доступа к таблицам", "$e", ); } } catch (e) { Get.snackbar( "Ошибка запроса информации доступа к таблицам", "$e", ); } } Future updateTableAccess( TableDefinition table, { required bool read, required bool write, }) async { final oldTableAccess = _access[table.tableId]; var newTableAccess = TableAccess.none; if (read && write) { newTableAccess = TableAccess.readWrite; } else if (read) { newTableAccess = TableAccess.read; } else if (write) { newTableAccess = TableAccess.write; } try { Response resp; if (newTableAccess == TableAccess.none) { resp = await ApiController.to.apiClient.deleteItemFromTable( tableName: "table_access", columnConditionCompat: [ ColumnConditionCompat( column: "user_group_id", operator_: ColumnConditionCompatOperator.eq, value: group.id, ), ColumnConditionCompat( column: "table_name", operator_: ColumnConditionCompatOperator.eq, value: table.tableName, ), ], ); } else if (oldTableAccess == TableAccess.none) { resp = await ApiController.to.apiClient.createItem( tableName: "table_access", itemDefinition: { "user_group_id": group.id, "table_name": table.tableName, "access_type": newTableAccess.def, }, ); } else { resp = await ApiController.to.apiClient.updateItemInTable( tableName: "table_access", itemUpdate: ItemUpdate( item: { "access_type": newTableAccess.def, }, oldItem: { "user_group_id": group.id, "table_name": table.tableName, }, ), ); } final respData = resp.data; if (respData == null) { throw Exception("В ответе нет данных"); } refreshTableAccess(table); } on DioError catch (e) { final respData = e.response?.data; if (respData != null) { Get.snackbar( "Ошибка обновления информации доступа к таблицам", "${respData['error']}", ); } else { Get.snackbar( "Ошибка обновления информации доступа к таблицам", "$e", ); } } catch (e) { Get.snackbar( "Ошибка обновления информации доступа к таблицам", "$e", ); } } Future changeAllowedColumns(TableDefinition table) async { final tableColumns = table.columns .split(",") .map((e) => e.split(":").first) .where((e) => e.isNotEmpty) .toList(); final currentlyAvailableColumns = _allowedColumns[table.tableId]!.split(","); if (currentlyAvailableColumns.length == 1 && currentlyAvailableColumns.first == "*") { currentlyAvailableColumns.clear(); currentlyAvailableColumns.addAll(tableColumns); } final selectedColumns = {}.obs; for (final column in tableColumns) { selectedColumns[column] = currentlyAvailableColumns.contains(column); } final confirm = await Get.dialog( AlertDialog( title: const Text("Разрешённые колонки"), content: Obx( () => Wrap( children: [ ElevatedButton( onPressed: () { for (final column in tableColumns) { selectedColumns[column] = !selectedColumns[column]!; } }, child: const Text("Перевернуть"), ), ...selectedColumns.entries.map((e) { return CheckboxListTile( title: Text(e.key), value: e.value, onChanged: (value) => selectedColumns[e.key] = value!, ); }), ], ), ), actions: [ TextButton( onPressed: () => Get.back(result: false), child: const Text("Отменить"), ), TextButton( onPressed: () => Get.back(result: true), child: const Text("Ок"), ), ], ), ); if (confirm != true) return; if (selectedColumns.values.every((e) => !e)) { await Get.dialog( AlertDialog( title: const Text("Ошибка"), content: const Text("Необходимо выбрать хотя бы одну колонку"), actions: [ TextButton( onPressed: () => Get.back(), child: const Text("Ок"), ), ], ), ); return; } try { Response resp = await ApiController.to.apiClient.updateItemInTable( tableName: "table_access", itemUpdate: ItemUpdate( item: { "allowed_columns": selectedColumns.keys .where((k) => selectedColumns[k]!) .join(","), }, oldItem: { "user_group_id": group.id, "table_name": table.tableName, }, ), ); final respData = resp.data; if (respData == null) { throw Exception("В ответе нет данных"); } refreshTableAccess(table); } on DioError catch (e) { final respData = e.response?.data; if (respData != null) { Get.snackbar( "Ошибка обновления информации доступа к таблицам", "${respData['error']}", ); } else { Get.snackbar( "Ошибка обновления информации доступа к таблицам", "$e", ); } } catch (e) { Get.snackbar( "Ошибка обновления информации доступа к таблицам", "$e", ); } } } class GroupACLDialog extends GetView { const GroupACLDialog({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Доступ группы'), content: Obx( () => DataTable2( columns: const [ DataColumn2(label: Text('Таблица'), size: ColumnSize.L), DataColumn2(label: Text('Чтение'), size: ColumnSize.S), DataColumn2(label: Text('Запись'), size: ColumnSize.S), DataColumn2(label: Text('Колонки'), size: ColumnSize.S), ], empty: const Text("Нет таблиц"), rows: controller.access.entries.map((e) { final table = controller.tables.firstWhere( (element) => element.tableId == e.key, ); final tableAccess = e.value; final read = tableAccess == TableAccess.read || tableAccess == TableAccess.readWrite; final write = tableAccess == TableAccess.write || tableAccess == TableAccess.readWrite; return DataRow(cells: [ DataCell(Text(table.tableName.pascalCase)), DataCell(Checkbox( value: read, onChanged: (value) => controller.updateTableAccess( table, read: value ?? false, write: write, ), )), DataCell(Checkbox( value: write, onChanged: (value) => controller.updateTableAccess( table, read: read, write: value ?? false, ), )), controller.allowedColumns[table.tableId] == null ? DataCell.empty : DataCell( Text(controller.allowedColumns[table.tableId]!), onTap: () => controller.changeAllowedColumns(table), ), ]); }).toList(growable: false), ).constrained(width: Get.width * 0.9, height: Get.height * 0.9), ), actions: [ TextButton( onPressed: () { Get.back(); }, child: const Text("Закрыть"), ), ], ); } static Future show(GroupDefinition group) async { Get.lazyPut(() => GroupACLController(group)); await Get.dialog( const GroupACLDialog(), barrierDismissible: false, ); Get.delete(); } }