tuuli_app/lib/pages/dialogs/open_table_dialog.dart

943 lines
33 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:data_table_2/data_table_2.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.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 = <int, UserDefinition?>{}.obs;
UserDefinition? getUserFromCache(int id) {
return _userCache[id];
}
void putUserInCache(UserDefinition user) {
_userCache[user.id] = user;
}
final _assetsCache = <int, Asset?>{}.obs;
Asset? getAssetFromCache(int id) {
return _assetsCache[id];
}
void putAssetInCache(Asset asset) {
_assetsCache[asset.id] = asset;
}
final _tableData = <Map<String, dynamic>>[].obs;
List<Map<String, dynamic>> get tableData => _tableData;
final _newRowData = <String, dynamic>{}.obs;
Map<String, dynamic> get newRowData => _newRowData;
void setNewRowData(String key, dynamic value) {
_newRowData[key] = value;
}
void clearNewRowData() {
_newRowData.clear();
}
Future<void> 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("В ответе нет данных");
}
_tableData.clear();
_tableData.addAll(respData);
} 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<UserDefinition?> showUserPicker() async {
final username = "".obs;
final user = await Get.dialog<UserDefinition>(
AlertDialog(
title: const Text("Выберите пользователя"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
onChanged: (value) {
username.value = value;
},
decoration: const InputDecoration(
labelText: "Логин",
),
),
Obx(
() => FutureBuilder<List<UserDefinition>>(
future: () async {
if (username.value.isEmpty) {
return <UserDefinition>[];
}
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("В ответе нет данных");
}
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("Отменить"),
),
],
),
);
return user;
}
Future<Asset?> showAssetPicker() async {
final name = "".obs;
final asset = await Get.dialog<Asset>(
AlertDialog(
title: const Text("Выберите ресурс"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
onChanged: (value) {
name.value = value;
},
decoration: const InputDecoration(
labelText: "Имя файла",
),
),
Obx(
() => FutureBuilder<List<Asset>>(
future: () async {
if (name.value.isEmpty) {
return <Asset>[];
}
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("В ответе нет данных");
}
return respData
.map((e) => Asset(
id: e["id"],
name: e["name"],
description: e["description"],
fid: "",
tags: "",
mime: "",
))
.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("Отменить"),
),
],
),
);
return asset;
}
Future<void> 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("В ответе нет данных");
}
clearNewRowData();
refreshTableData();
} 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<void> updateItem(
Map<String, dynamic> 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("В ответе нет данных");
}
refreshTableData();
} 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<void> deleteItem(Map<String, dynamic> 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("В ответе нет данных");
}
refreshTableData();
} 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 OpenTableDialog extends GetView<OpenTableController> {
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("Действия")),
],
empty: const Text("Нет данных"),
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 "###";
}
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<String>(
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 "###";
}
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 ?? "###",
child: Text(
snapshot.data ?? "###",
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<String>(
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 "###";
}
controller.putAssetInCache(Asset(
id: e[col.name],
name: ad.first["name"],
description: "",
fid: "",
tags: "",
mime: "",
));
return ad.first["name"].toString();
}(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
}
return Tooltip(
message: snapshot.data ?? "###",
child: Text(
snapshot.data ?? "###",
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<void> show(TableDefinition table) async {
Get.lazyPut<OpenTableController>(() => OpenTableController(table: table));
await Get.dialog(
const OpenTableDialog(),
barrierDismissible: false,
);
}
}