tuuli_app/lib/pages/dialogs/open_table_dialog.dart

942 lines
32 KiB
Dart

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 = <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("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<UserDefinition?> showUserPicker() async {
final username = "".obs;
final user = await Get.dialog<UserDefinition>(
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<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("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<Asset?> showAssetPicker() async {
final name = "".obs;
final asset = await Get.dialog<Asset>(
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<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("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<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("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<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("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<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("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<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("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<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 "#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<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 "#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<void> show(TableDefinition table) async {
Get.lazyPut<OpenTableController>(() => OpenTableController(table: table));
await Get.dialog(
const OpenTableDialog(),
barrierDismissible: false,
);
}
}