completed migrating data scope from user to workspace

This commit is contained in:
2026-06-22 22:10:38 +02:00
parent 62c7f53de0
commit ae2705363a
37 changed files with 918 additions and 221 deletions

View File

@@ -36,6 +36,7 @@ class BatchScreenModel {
int batchItemsTotalElements = 0;
String batchItemsSearchQuery = '';
String? batchItemsStatusFilter;
String? batchItemsRecipientAccount;
bool batchItemsIsLoadingMore = false;
// Provider products cache: providerId -> list of products
@@ -196,7 +197,7 @@ class BatchController extends ChangeNotifier {
}
final raw = await http.get(
'/public/group-batches?${buildQueryParameters(params)}',
'/group-batches?${buildQueryParameters(params)}',
);
final response = _unwrap(raw);
final pageableModel = PageableModel.fromJson(
@@ -268,8 +269,7 @@ class BatchController extends ChangeNotifier {
String batchId, {
int page = 0,
int size = 20,
String? search,
String? status,
Map<String, String>? filters,
}) async {
if (page == 0) {
model.isLoading = true;
@@ -287,16 +287,12 @@ class BatchController extends ChangeNotifier {
'size': size.toString(),
'sort': 'createdAt,desc',
};
if (search != null && search.isNotEmpty) {
params['recipientName'] = search;
params['recipientPhone'] = search;
}
if (status != null && status.isNotEmpty) {
params['status'] = status;
if (filters != null) {
params.addAll(filters);
}
final raw = await http.get(
'/public/group-batches/items?${buildQueryParameters(params)}',
'/group-batches/items?${buildQueryParameters(params)}',
);
final response = _unwrap(raw);
final pageableModel = PageableModel.fromJson(
@@ -337,10 +333,20 @@ class BatchController extends ChangeNotifier {
String batchId, {
String? search,
String? status,
String? account,
}) async {
model.batchItemsSearchQuery = search ?? '';
model.batchItemsStatusFilter = status;
await listBatchItems(batchId, page: 0, search: search, status: status);
model.batchItemsRecipientAccount = account;
await listBatchItems(
batchId,
page: 0,
filters: _buildBatchItemFilters(
search: search,
status: status,
account: account,
),
);
}
Future<void> loadNextBatchItemsPage(String batchId) async {
@@ -351,22 +357,44 @@ class BatchController extends ChangeNotifier {
await listBatchItems(
batchId,
page: model.batchItemsCurrentPage + 1,
search: model.batchItemsSearchQuery.isNotEmpty
? model.batchItemsSearchQuery
: null,
status: model.batchItemsStatusFilter,
filters: _buildBatchItemFilters(
search: model.batchItemsSearchQuery.isNotEmpty
? model.batchItemsSearchQuery
: null,
status: model.batchItemsStatusFilter,
account: model.batchItemsRecipientAccount,
),
);
}
/// Builds the filters map for batch items queries from individual params.
Map<String, String>? _buildBatchItemFilters({
String? search,
String? status,
String? account,
}) {
final filters = <String, String>{};
if (search != null && search.isNotEmpty) {
filters['recipientName'] = search;
}
if (status != null && status.isNotEmpty) {
filters['status'] = status;
}
if (account != null && account.isNotEmpty) {
filters['recipientAccount'] = account;
}
return filters.isEmpty ? null : filters;
}
Future<ApiResponse<GroupBatch>> getBatch(
String batchId,
String userId,
String workspaceId,
) async {
model.isActing = true;
if (!_disposed) notifyListeners();
try {
final raw = await http.get(
'/public/group-batches/$batchId?userId=$userId',
'/group-batches/$batchId?workspaceId=$workspaceId',
);
final response = _unwrap(raw);
final batch = GroupBatch.fromJson(response as Map<String, dynamic>);
@@ -386,7 +414,7 @@ class BatchController extends ChangeNotifier {
model.isActing = true;
if (!_disposed) notifyListeners();
try {
final raw = await http.post('/public/group-batches', req.toJson());
final raw = await http.post('/group-batches', req.toJson());
final response = _unwrap(raw);
final batch = GroupBatch.fromJson(response as Map<String, dynamic>);
model.currentBatch = batch;
@@ -409,7 +437,7 @@ class BatchController extends ChangeNotifier {
if (!_disposed) notifyListeners();
try {
final raw = await http.put(
'/public/group-batches/${request.id}',
'/group-batches/${request.id}',
request.toJson(),
);
final response = _unwrap(raw);
@@ -440,7 +468,7 @@ class BatchController extends ChangeNotifier {
if (!_disposed) notifyListeners();
try {
final raw = await http.put(
'/public/group-batches/$batchId/items',
'/group-batches/$batchId/items',
updateList.toJson(),
);
final response = _unwrap(raw);
@@ -463,16 +491,21 @@ class BatchController extends ChangeNotifier {
Future<ApiResponse<GroupBatch>> confirmBatch(
String batchId,
String userId,
String workspaceId,
String authType,
) async {
model.isActing = true;
if (!_disposed) notifyListeners();
try {
final key =
'batch-$batchId-confirm-${DateTime.now().millisecondsSinceEpoch}';
final body = BatchActionRequest(userId: userId, idempotencyKey: key);
final body = BatchActionRequest(
workspaceId: workspaceId,
authType: authType,
idempotencyKey: key,
);
final raw = await http.post(
'/public/group-batches/$batchId/confirm',
'/group-batches/$batchId/confirm',
body.toJson(),
);
final response = _unwrap(raw);
@@ -491,16 +524,19 @@ class BatchController extends ChangeNotifier {
Future<ApiResponse<TransactionModel>> requestBatch(
String batchId,
String userId,
String workspaceId,
) async {
model.isActing = true;
if (!_disposed) notifyListeners();
try {
final key =
'batch-$batchId-request-${DateTime.now().millisecondsSinceEpoch}';
final body = BatchActionRequest(userId: userId, idempotencyKey: key);
final body = BatchActionRequest(
workspaceId: workspaceId,
idempotencyKey: key,
);
final raw = await http.post(
'/public/group-batches/$batchId/request',
'/group-batches/$batchId/request',
body.toJson(),
);
final response = _unwrap(raw);
@@ -519,13 +555,13 @@ class BatchController extends ChangeNotifier {
Future<ApiResponse<Map<String, dynamic>>> downloadBatchReport(
String batchId,
String userId,
String workspaceId,
) async {
model.isActing = true;
if (!_disposed) notifyListeners();
try {
final raw = await http.get(
'/public/group-batches/$batchId/report?userId=$userId',
'/group-batches/$batchId/report?workspaceId=$workspaceId',
);
final response = _unwrap(raw);
model.isActing = false;
@@ -542,16 +578,19 @@ class BatchController extends ChangeNotifier {
Future<ApiResponse<TransactionModel>> pollBatch(
String batchId,
String userId,
String workspaceId,
) async {
model.isActing = true;
if (!_disposed) notifyListeners();
try {
final key =
'batch-$batchId-poll-${DateTime.now().millisecondsSinceEpoch}';
final body = BatchActionRequest(userId: userId, idempotencyKey: key);
final body = BatchActionRequest(
workspaceId: workspaceId,
idempotencyKey: key,
);
final raw = await http.post(
'/public/group-batches/$batchId/poll',
'/group-batches/$batchId/poll',
body.toJson(),
);
final response = _unwrap(raw);
@@ -578,10 +617,10 @@ class BatchController extends ChangeNotifier {
Future<void> deleteBatch({
required String batchId,
required String userId,
required String workspaceId,
}) async {
try {
await http.delete('/public/group-batches/$batchId?userId=$userId');
await http.delete('/group-batches/$batchId?workspaceId=$workspaceId');
model.batches.removeWhere((b) => b.id == batchId);
model.selectedBatchIds.remove(batchId);
model.currentBatch = null;
@@ -595,7 +634,7 @@ class BatchController extends ChangeNotifier {
Future<void> deleteSelectedBatches({
required String groupId,
required String userId,
required String workspaceId,
}) async {
final idsToDelete = List<String>.from(model.selectedBatchIds);
model.selectMode = false;
@@ -603,7 +642,7 @@ class BatchController extends ChangeNotifier {
if (!_disposed) notifyListeners();
for (final id in idsToDelete) {
await deleteBatch(batchId: id, userId: userId);
await deleteBatch(batchId: id, workspaceId: workspaceId);
}
// Refresh after deletion

View File

@@ -53,7 +53,7 @@ class GroupController extends ChangeNotifier {
}
Future<ApiResponse<PageableModel>> listGroups({
required String userId,
required String workspaceId,
int page = 0,
int size = 20,
String? name,
@@ -69,7 +69,7 @@ class GroupController extends ChangeNotifier {
try {
final params = <String, String>{
'userId': userId,
'workspaceId': workspaceId,
'page': page.toString(),
'size': size.toString(),
};
@@ -78,7 +78,7 @@ class GroupController extends ChangeNotifier {
}
final raw = await http.get(
'/public/recipient-groups?${buildQueryParameters(params)}',
'/recipient-groups?${buildQueryParameters(params)}',
);
final response = _unwrap(raw);
final pageableModel = PageableModel.fromJson(
@@ -115,17 +115,17 @@ class GroupController extends ChangeNotifier {
}
}
Future<void> searchGroups({required String userId, String? name}) async {
Future<void> searchGroups({required String workspaceId, String? name}) async {
model.searchQuery = name ?? '';
await listGroups(userId: userId, page: 0, name: name);
await listGroups(workspaceId: workspaceId, page: 0, name: name);
}
Future<void> loadNextPage({required String userId}) async {
Future<void> loadNextPage({required String workspaceId}) async {
if (model.isLoadingMore || model.currentPage + 1 >= model.totalPages) {
return;
}
await listGroups(
userId: userId,
workspaceId: workspaceId,
page: model.currentPage + 1,
name: model.searchQuery.isNotEmpty ? model.searchQuery : null,
);
@@ -138,7 +138,7 @@ class GroupController extends ChangeNotifier {
if (!_disposed) notifyListeners();
try {
final raw = await http.get(
'/public/recipient-groups/members?recipientGroupId=$groupId',
'/recipient-groups/members?recipientGroupId=$groupId',
);
final response = _unwrap(raw);
final List<dynamic> content;
@@ -172,7 +172,7 @@ class GroupController extends ChangeNotifier {
model.isLoading = true;
if (!_disposed) notifyListeners();
try {
final raw = await http.post('/public/recipient-groups', req.toJson());
final raw = await http.post('/recipient-groups', req.toJson());
final response = _unwrap(raw);
final group = RecipientGroup.fromJson(response as Map<String, dynamic>);
model.groups = [group, ...model.groups];
@@ -190,11 +190,11 @@ class GroupController extends ChangeNotifier {
Future<void> deleteGroup({
required String groupId,
required String userId,
required String workspaceId,
}) async {
try {
await http.delete(
'/public/recipient-groups/delete/$groupId?userId=$userId',
'/recipient-groups/delete/$groupId?workspaceId=$workspaceId',
);
model.groups.removeWhere((g) => g.id == groupId);
model.selectedGroupIds.remove(groupId);
@@ -226,11 +226,11 @@ class GroupController extends ChangeNotifier {
Future<void> removeMember({
required String groupId,
required String recipientId,
required String userId,
required String workspaceId,
}) async {
try {
await http.delete(
'/public/recipient-groups/$groupId/members/$recipientId?userId=$userId',
'/recipient-groups/$groupId/members/$recipientId?workspaceId=$workspaceId',
);
model.members.removeWhere((m) => m.id == recipientId);
if (!_disposed) notifyListeners();
@@ -241,19 +241,20 @@ class GroupController extends ChangeNotifier {
}
}
Future<void> deleteSelectedGroups({required String userId}) async {
Future<void> deleteSelectedGroups({required String workspaceId}) async {
final idsToDelete = List<String>.from(model.selectedGroupIds);
model.selectMode = false;
model.selectedGroupIds.clear();
if (!_disposed) notifyListeners();
for (final id in idsToDelete) {
await deleteGroup(groupId: id, userId: userId);
await deleteGroup(groupId: id, workspaceId: workspaceId);
}
}
Future<ApiResponse<CreateGroupFromFileResponse>> createGroupFromFile({
required String groupName,
required String workspaceId,
required String userId,
String? description,
required String currency,
@@ -266,6 +267,7 @@ class GroupController extends ChangeNotifier {
try {
final fields = <String, String>{
'groupName': groupName,
'workspaceId': workspaceId,
'userId': userId,
'currency': currency,
};
@@ -274,7 +276,7 @@ class GroupController extends ChangeNotifier {
}
final raw = await http.multipartPost(
'/public/recipient-groups/create-from-file',
'/recipient-groups/create-from-file',
fields,
'file',
fileBytes,

View File

@@ -35,9 +35,9 @@ class GroupBatchItemUpdateRequest {
@JsonSerializable(explicitToJson: true)
class GroupBatchItemUpdateList {
final List<GroupBatchItemUpdateRequest>? items;
final String? userId;
final String? workspaceId;
const GroupBatchItemUpdateList({this.items, this.userId});
const GroupBatchItemUpdateList({this.items, this.workspaceId});
factory GroupBatchItemUpdateList.fromJson(Map<String, dynamic> json) =>
_$GroupBatchItemUpdateListFromJson(json);

View File

@@ -42,12 +42,12 @@ GroupBatchItemUpdateList _$GroupBatchItemUpdateListFromJson(
(e) => GroupBatchItemUpdateRequest.fromJson(e as Map<String, dynamic>),
)
.toList(),
userId: json['userId'] as String?,
workspaceId: json['workspaceId'] as String?,
);
Map<String, dynamic> _$GroupBatchItemUpdateListToJson(
GroupBatchItemUpdateList instance,
) => <String, dynamic>{
'items': instance.items?.map((e) => e.toJson()).toList(),
'userId': instance.userId,
'workspaceId': instance.workspaceId,
};

View File

@@ -7,6 +7,7 @@ class GroupBatch {
final String? id;
final String? recipientGroupId;
final String? userId;
final String? workspaceId;
final String? currency;
final String? description;
final double? value;
@@ -24,6 +25,7 @@ class GroupBatch {
this.id,
this.recipientGroupId,
this.userId,
this.workspaceId,
this.currency,
this.description,
this.value,
@@ -88,6 +90,7 @@ class GroupBatchItem {
class CreateBatchRequest {
final String recipientGroupId;
final String userId;
final String workspaceId;
final String currency;
final String description;
final double amount;
@@ -98,6 +101,7 @@ class CreateBatchRequest {
const CreateBatchRequest({
required this.recipientGroupId,
required this.userId,
required this.workspaceId,
required this.currency,
required this.description,
required this.amount,
@@ -114,12 +118,14 @@ class CreateBatchRequest {
@JsonSerializable()
class BatchActionRequest {
final String userId;
final String workspaceId;
final String idempotencyKey;
final String? authType;
const BatchActionRequest({
required this.userId,
required this.workspaceId,
required this.idempotencyKey,
this.authType,
});
factory BatchActionRequest.fromJson(Map<String, dynamic> json) =>
@@ -131,14 +137,14 @@ class BatchActionRequest {
@JsonSerializable(explicitToJson: true)
class GroupBatchUpdateRequest {
String? id;
String? userId;
String? workspaceId;
String? paymentProcessorLabel;
String? paymentProcessorName;
String? paymentProcessorImage;
GroupBatchUpdateRequest({
this.id,
this.userId,
this.workspaceId,
this.paymentProcessorLabel,
this.paymentProcessorName,
this.paymentProcessorImage,

View File

@@ -10,6 +10,7 @@ GroupBatch _$GroupBatchFromJson(Map<String, dynamic> json) => GroupBatch(
id: json['id'] as String?,
recipientGroupId: json['recipientGroupId'] as String?,
userId: json['userId'] as String?,
workspaceId: json['workspaceId'] as String?,
currency: json['currency'] as String?,
description: json['description'] as String?,
value: (json['value'] as num?)?.toDouble(),
@@ -29,6 +30,7 @@ Map<String, dynamic> _$GroupBatchToJson(GroupBatch instance) =>
'id': instance.id,
'recipientGroupId': instance.recipientGroupId,
'userId': instance.userId,
'workspaceId': instance.workspaceId,
'currency': instance.currency,
'description': instance.description,
'value': instance.value,
@@ -83,6 +85,7 @@ CreateBatchRequest _$CreateBatchRequestFromJson(Map<String, dynamic> json) =>
CreateBatchRequest(
recipientGroupId: json['recipientGroupId'] as String,
userId: json['userId'] as String,
workspaceId: json['workspaceId'] as String,
currency: json['currency'] as String,
description: json['description'] as String,
amount: (json['amount'] as num).toDouble(),
@@ -95,6 +98,7 @@ Map<String, dynamic> _$CreateBatchRequestToJson(CreateBatchRequest instance) =>
<String, dynamic>{
'recipientGroupId': instance.recipientGroupId,
'userId': instance.userId,
'workspaceId': instance.workspaceId,
'currency': instance.currency,
'description': instance.description,
'amount': instance.amount,
@@ -105,21 +109,23 @@ Map<String, dynamic> _$CreateBatchRequestToJson(CreateBatchRequest instance) =>
BatchActionRequest _$BatchActionRequestFromJson(Map<String, dynamic> json) =>
BatchActionRequest(
userId: json['userId'] as String,
workspaceId: json['workspaceId'] as String,
idempotencyKey: json['idempotencyKey'] as String,
authType: json['authType'] as String?,
);
Map<String, dynamic> _$BatchActionRequestToJson(BatchActionRequest instance) =>
<String, dynamic>{
'userId': instance.userId,
'workspaceId': instance.workspaceId,
'idempotencyKey': instance.idempotencyKey,
'authType': instance.authType,
};
GroupBatchUpdateRequest _$GroupBatchUpdateRequestFromJson(
Map<String, dynamic> json,
) => GroupBatchUpdateRequest(
id: json['id'] as String?,
userId: json['userId'] as String?,
workspaceId: json['workspaceId'] as String?,
paymentProcessorLabel: json['paymentProcessorLabel'] as String?,
paymentProcessorName: json['paymentProcessorName'] as String?,
paymentProcessorImage: json['paymentProcessorImage'] as String?,
@@ -129,7 +135,7 @@ Map<String, dynamic> _$GroupBatchUpdateRequestToJson(
GroupBatchUpdateRequest instance,
) => <String, dynamic>{
'id': instance.id,
'userId': instance.userId,
'workspaceId': instance.workspaceId,
'paymentProcessorLabel': instance.paymentProcessorLabel,
'paymentProcessorName': instance.paymentProcessorName,
'paymentProcessorImage': instance.paymentProcessorImage,

View File

@@ -9,6 +9,7 @@ class RecipientGroup {
final String name;
final String? description;
final String? userId;
final String? workspaceId;
final String? createdAt;
const RecipientGroup({
@@ -16,6 +17,7 @@ class RecipientGroup {
required this.name,
this.description,
this.userId,
this.workspaceId,
this.createdAt,
});
@@ -56,12 +58,14 @@ class CreateGroupRequest {
final String name;
final String? description;
final String userId;
final String workspaceId;
final List<Recipient> recipients;
const CreateGroupRequest({
required this.name,
this.description,
required this.userId,
required this.workspaceId,
required this.recipients,
});

View File

@@ -12,6 +12,7 @@ RecipientGroup _$RecipientGroupFromJson(Map<String, dynamic> json) =>
name: json['name'] as String,
description: json['description'] as String?,
userId: json['userId'] as String?,
workspaceId: json['workspaceId'] as String?,
createdAt: json['createdAt'] as String?,
);
@@ -21,6 +22,7 @@ Map<String, dynamic> _$RecipientGroupToJson(RecipientGroup instance) =>
'name': instance.name,
'description': instance.description,
'userId': instance.userId,
'workspaceId': instance.workspaceId,
'createdAt': instance.createdAt,
};
@@ -53,6 +55,7 @@ CreateGroupRequest _$CreateGroupRequestFromJson(Map<String, dynamic> json) =>
name: json['name'] as String,
description: json['description'] as String?,
userId: json['userId'] as String,
workspaceId: json['workspaceId'] as String,
recipients: (json['recipients'] as List<dynamic>)
.map((e) => Recipient.fromJson(e as Map<String, dynamic>))
.toList(),
@@ -63,5 +66,6 @@ Map<String, dynamic> _$CreateGroupRequestToJson(CreateGroupRequest instance) =>
'name': instance.name,
'description': instance.description,
'userId': instance.userId,
'workspaceId': instance.workspaceId,
'recipients': instance.recipients.map((e) => e.toJson()).toList(),
};

View File

@@ -90,10 +90,12 @@ class _BatchCreateScreenState extends State<BatchCreateScreen> {
final prefs = await SharedPreferences.getInstance();
final userId = prefs.getString('userId') ?? '';
final workspaceId = prefs.getString('workspaceId') ?? '';
final req = CreateBatchRequest(
recipientGroupId: widget.group.id ?? '',
userId: userId,
workspaceId: workspaceId,
currency: _currency,
description: _descriptionController.text.trim(),
amount: amount,
@@ -115,7 +117,7 @@ class _BatchCreateScreenState extends State<BatchCreateScreen> {
batch.id!,
selectedProvider,
selectedProduct,
userId,
workspaceId,
);
}
@@ -130,7 +132,7 @@ class _BatchCreateScreenState extends State<BatchCreateScreen> {
String batchId,
BillProvider provider,
BillProduct? product,
String userId,
String workspaceId,
) async {
// Fetch the batch items so we can update them
final itemsResult = await batchController.listBatchItems(batchId);
@@ -154,7 +156,7 @@ class _BatchCreateScreenState extends State<BatchCreateScreen> {
final updateList = GroupBatchItemUpdateList(
items: updateItems,
userId: userId,
workspaceId: workspaceId,
);
await batchController.updateBatchItems(batchId, updateList);

View File

@@ -33,12 +33,13 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
ConfirmController? _confirmController;
GroupBatchUpdateRequest? _cachedUpdateReq;
bool _showPollButton = false;
String _userId = '';
String _workspaceId = '';
bool _initialLoading = true;
String? _loadError;
PaymentProcessor? _selectedProcessor;
final TextEditingController _searchController = TextEditingController();
final TextEditingController _accountController = TextEditingController();
final ScrollController _scrollController = ScrollController();
bool _editMode = false;
@@ -85,9 +86,9 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
Future<void> _init() async {
final prefs = await SharedPreferences.getInstance();
_userId = prefs.getString('userId') ?? '';
_workspaceId = prefs.getString('workspaceId') ?? '';
final result = await controller.getBatch(widget.batchId, _userId);
final result = await controller.getBatch(widget.batchId, _workspaceId);
if (!mounted) return;
if (result.isSuccess) {
@@ -117,6 +118,7 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
@override
void dispose() {
_searchController.dispose();
_accountController.dispose();
_scrollController.dispose();
_entryController.dispose();
_confirmController?.dispose();
@@ -136,7 +138,7 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
// Persist the selected payment processor on the batch before confirming
final updateReq = GroupBatchUpdateRequest(
id: batch.id,
userId: _userId,
workspaceId: _workspaceId,
paymentProcessorLabel: processor.label,
paymentProcessorName: processor.name,
paymentProcessorImage: processor.image,
@@ -152,7 +154,11 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
_cachedUpdateReq = updateReq;
// Step 2: Confirm the batch
final result = await controller.confirmBatch(batch.id!, _userId);
final result = await controller.confirmBatch(
batch.id!,
_workspaceId,
_selectedProcessor!.authType,
);
if (!mounted) return;
if (!result.isSuccess) {
_showError(result.error ?? 'Confirm failed.');
@@ -431,7 +437,7 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
final batch = _batch;
if (batch == null) return;
final result = await controller.requestBatch(batch.id!, _userId);
final result = await controller.requestBatch(batch.id!, _workspaceId);
if (!mounted) return;
if (!result.isSuccess) {
@@ -484,7 +490,7 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
}
}
} else {
final pollResult = await controller.pollBatch(batch.id!, _userId);
final pollResult = await controller.pollBatch(batch.id!, _workspaceId);
if (!mounted) return;
if (pollResult.isSuccess) {
setState(() => _showPollButton = false);
@@ -672,7 +678,7 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
Future<void> _onPoll() async {
final batch = _batch;
if (batch == null) return;
final result = await controller.pollBatch(batch.id!, _userId);
final result = await controller.pollBatch(batch.id!, _workspaceId);
if (!mounted) return;
if (result.isSuccess) {
setState(() => _showPollButton = false);
@@ -685,7 +691,10 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
Future<void> _onDownloadReport() async {
final batch = _batch;
if (batch == null) return;
final result = await controller.downloadBatchReport(batch.id!, _userId);
final result = await controller.downloadBatchReport(
batch.id!,
_workspaceId,
);
if (!mounted) return;
if (result.isSuccess) {
final fileUrl = result.data?['fileUrl'] as String?;
@@ -1435,7 +1444,10 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
if (items.isEmpty) return false;
final updateList = GroupBatchItemUpdateList(items: items, userId: _userId);
final updateList = GroupBatchItemUpdateList(
items: items,
workspaceId: _workspaceId,
);
final result = await controller.updateBatchItems(batch.id!, updateList);
if (!mounted) return false;
@@ -1492,6 +1504,8 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
onPressed: () {
setSheetState(() {
controller.model.batchItemsStatusFilter = null;
controller.model.batchItemsRecipientAccount = null;
_accountController.clear();
});
},
child: const Text('Clear All'),
@@ -1517,14 +1531,18 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
items: const [
DropdownMenuItem(value: null, child: Text('All')),
DropdownMenuItem(
value: 'PENDING',
child: Text('Pending'),
value: 'CONFIRMED',
child: Text('CONFIRMED'),
),
DropdownMenuItem(
value: 'SUCCESS',
child: Text('Success'),
value: 'INTEGRATED',
child: Text('INTEGRATED'),
),
DropdownMenuItem(value: 'FAILED', child: Text('FAILED')),
DropdownMenuItem(
value: 'SKIPPED',
child: Text('SKIPPED'),
),
DropdownMenuItem(value: 'FAILED', child: Text('Failed')),
],
onChanged: (v) {
setSheetState(
@@ -1532,6 +1550,24 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
);
},
),
const SizedBox(height: 16),
TextField(
controller: _accountController,
decoration: InputDecoration(
labelText: 'Account Number',
hintText: 'Filter by account number',
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
@@ -1546,6 +1582,9 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
? _searchController.text
: null,
status: controller.model.batchItemsStatusFilter,
account: _accountController.text.isNotEmpty
? _accountController.text
: null,
);
}
},
@@ -2021,7 +2060,9 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
// Action Bar — search, filter, edit, refresh
// ──────────────────────────────────────────────────────────────
Widget _buildActionBar(ThemeData theme, bool isDark) {
final hasFilters = controller.model.batchItemsStatusFilter != null;
final hasFilters =
controller.model.batchItemsStatusFilter != null ||
controller.model.batchItemsRecipientAccount != null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -2033,7 +2074,7 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
child: TextFormField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search by name or phone',
hintText: 'Search by recipient name',
prefixIcon: const Icon(Icons.search, size: 20),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
@@ -2046,6 +2087,9 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
batchId,
status:
controller.model.batchItemsStatusFilter,
account: controller
.model
.batchItemsRecipientAccount,
);
}
},
@@ -2075,6 +2119,7 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
batchId,
search: value.isNotEmpty ? value : null,
status: controller.model.batchItemsStatusFilter,
account: controller.model.batchItemsRecipientAccount,
);
}
},
@@ -2147,6 +2192,26 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
search: _searchController.text.isNotEmpty
? _searchController.text
: null,
account: controller.model.batchItemsRecipientAccount,
);
}
},
theme,
),
if (controller.model.batchItemsRecipientAccount != null)
_activeFilterChip(
'Account: ${controller.model.batchItemsRecipientAccount}',
() {
controller.model.batchItemsRecipientAccount = null;
_accountController.clear();
final batchId = _batch?.id ?? '';
if (batchId.isNotEmpty) {
controller.searchBatchItems(
batchId,
search: _searchController.text.isNotEmpty
? _searchController.text
: null,
status: controller.model.batchItemsStatusFilter,
);
}
},

View File

@@ -65,10 +65,12 @@ class _CreateGroupFromFileScreenState extends State<CreateGroupFromFileScreen> {
}
final prefs = await SharedPreferences.getInstance();
final workspaceId = prefs.getString('workspaceId') ?? '';
final userId = prefs.getString('userId') ?? '';
final result = await controller.createGroupFromFile(
groupName: _groupNameController.text.trim(),
workspaceId: workspaceId,
userId: userId,
description: _descriptionController.text.trim().isEmpty
? null

View File

@@ -82,6 +82,7 @@ class _GroupCreateScreenState extends State<GroupCreateScreen> {
final prefs = await SharedPreferences.getInstance();
final userId = prefs.getString('userId') ?? '';
final workspaceId = prefs.getString('workspaceId') ?? '';
final req = CreateGroupRequest(
name: _nameController.text.trim(),
@@ -89,6 +90,7 @@ class _GroupCreateScreenState extends State<GroupCreateScreen> {
? null
: _descriptionController.text.trim(),
userId: userId,
workspaceId: workspaceId,
recipients: recipients,
);

View File

@@ -552,7 +552,7 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
await groupController.removeMember(
groupId: widget.group.id ?? '',
recipientId: member.id ?? '',
userId: widget.group.userId ?? '',
workspaceId: widget.group.workspaceId ?? '',
);
}
}
@@ -675,7 +675,7 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
}
void _onDeleteSelected() async {
final userId = widget.group.userId ?? '';
final workspaceId = widget.group.workspaceId ?? '';
final count = batchController.model.selectedBatchIds.length;
final confirmed = await showDialog<bool>(
@@ -700,7 +700,7 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
if (confirmed == true) {
await batchController.deleteSelectedBatches(
groupId: widget.group.id ?? '',
userId: userId,
workspaceId: workspaceId,
);
}
}

View File

@@ -30,26 +30,28 @@ class _GroupsScreenState extends State<GroupsScreen> {
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
controller.loadNextPage(userId: prefs.getString('userId') ?? '');
controller.loadNextPage(
workspaceId: prefs.getString('workspaceId') ?? '',
);
}
}
Future<void> _loadData() async {
prefs = await SharedPreferences.getInstance();
final userId = prefs.getString('userId') ?? '';
await controller.listGroups(userId: userId);
final workspaceId = prefs.getString('workspaceId') ?? '';
await controller.listGroups(workspaceId: workspaceId);
}
void _onSearchChanged(String value) {
final userId = prefs.getString('userId') ?? '';
final workspaceId = prefs.getString('workspaceId') ?? '';
controller.searchGroups(
userId: userId,
workspaceId: workspaceId,
name: value.isNotEmpty ? value : null,
);
}
void _onDeleteSelected() async {
final userId = prefs.getString('userId') ?? '';
final workspaceId = prefs.getString('workspaceId') ?? '';
final count = controller.model.selectedGroupIds.length;
final confirmed = await showDialog<bool>(
@@ -72,7 +74,7 @@ class _GroupsScreenState extends State<GroupsScreen> {
);
if (confirmed == true) {
await controller.deleteSelectedGroups(userId: userId);
await controller.deleteSelectedGroups(workspaceId: workspaceId);
}
}