Browse Source

feat : 上传问题

dev
徐振升 7 hours ago
parent
commit
ea8958e186
  1. 34
      lib/app/core/controllers/i_upload_controller.dart
  2. 8
      lib/app/core/pages/widgets/upload_progress_dialog.dart
  3. 26
      lib/app/features/enterprise/data/datasources/enterprise_local_data_source.dart
  4. 29
      lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart
  5. 4
      lib/app/features/enterprise/domain/repositories/enterprise_repository.dart
  6. 13
      lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart
  7. 15
      lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart
  8. 16
      lib/app/features/problem/data/datasources/problem_local_data_source.dart
  9. 118
      lib/app/features/problem/data/datasources/problem_remote_data_source.dart
  10. 145
      lib/app/features/problem/data/model/problem_dto.dart
  11. 3
      lib/app/features/problem/data/model/problem_filter_dto.dart
  12. 70
      lib/app/features/problem/data/repositories/problem_repository_impl.dart
  13. 6
      lib/app/features/problem/domain/entities/problem_filter_params.dart
  14. 2
      lib/app/features/problem/domain/repositories/problem_repository.dart
  15. 2
      lib/app/features/problem/domain/usecases/add_problem_usecase.dart
  16. 2
      lib/app/features/problem/domain/usecases/delete_problem.dart
  17. 2
      lib/app/features/problem/domain/usecases/get_all_problems_usecase.dart
  18. 2
      lib/app/features/problem/domain/usecases/get_problem_by_id_usecase.dart
  19. 10
      lib/app/features/problem/domain/usecases/update_problem_usecase.dart
  20. 131
      lib/app/features/problem/domain/usecases/upload_problems_usecase.dart
  21. 2
      lib/app/features/problem/presentation/bindings/problem_form_binding.dart
  22. 51
      lib/app/features/problem/presentation/bindings/problem_list_binding.dart
  23. 55
      lib/app/features/problem/presentation/bindings/problem_upload_binding.dart
  24. 43
      lib/app/features/problem/presentation/controllers/problem_form_controller.dart
  25. 58
      lib/app/features/problem/presentation/controllers/problem_upload_controller.dart
  26. 24
      lib/app/features/problem/presentation/pages/problem_upload_page.dart

34
lib/app/core/controllers/i_upload_controller.dart

@ -0,0 +1,34 @@
import 'package:get/get.dart';
import 'package:problem_check_system/app/core/domain/entities/upload_result.dart';
///
///
///
/// 使 UI ( UploadProgressDialog)
///
abstract class IUploadController {
// --- (State Properties) ---
///
RxBool get isUploading;
/// (0.0 to 1.0)
RxDouble get uploadProgress;
///
RxInt get uploadedCount;
///
RxInt get totalToUpload;
///
Rx<UploadResult?> get uploadResult;
// --- (Action Methods) ---
///
void cancelUpload();
/// ()
void closeUploadDialog();
}

8
lib/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart → lib/app/core/pages/widgets/upload_progress_dialog.dart

@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/app/core/controllers/i_upload_controller.dart';
import 'package:problem_check_system/app/core/domain/entities/upload_result.dart';
import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart';
import 'package:problem_check_system/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart';
class UploadProgressDialog extends GetView<EnterpriseUploadController> {
const UploadProgressDialog({super.key});
class UploadProgressDialog extends StatelessWidget {
final IUploadController controller;
const UploadProgressDialog({super.key, required this.controller});
@override
Widget build(BuildContext context) {

26
lib/app/features/enterprise/data/datasources/enterprise_local_data_source.dart

@ -23,6 +23,10 @@ abstract class EnterpriseLocalDataSource {
// []
Future<void> upsertEnterprise(EnterpriseModel enterprise);
Future<void> cacheEnterprises(List<EnterpriseModel> enterprises);
Future getEnterpriseById(String enterpriseId);
Future<void> deleteEnterprise(String id) async {}
}
class EnterpriseLocalDataSourceImpl implements EnterpriseLocalDataSource {
@ -210,4 +214,26 @@ class EnterpriseLocalDataSourceImpl implements EnterpriseLocalDataSource {
conflictAlgorithm: ConflictAlgorithm.replace, // replace
);
}
@override
Future getEnterpriseById(String enterpriseId) async {
final db = await _databaseService.database;
final List<Map<String, dynamic>> maps = await db.query(
_tableName,
where: 'id = ?',
whereArgs: [enterpriseId],
limit: 1, //
);
if (maps.isNotEmpty) {
return maps.first;
}
return null;
}
@override
Future<void> deleteEnterprise(String id) {
//TODO
throw UnimplementedError();
}
}

29
lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart

@ -98,14 +98,23 @@ class EnterpriseRepositoryImpl implements EnterpriseRepository {
syncedDto = await remoteDataSource.updateEnterprise(enterpriseModel);
break;
case SyncStatus.pendingDelete:
//
await remoteDataSource.deleteEnterprise(enterprise.id);
return enterprise;
//
//
await localDataSource.deleteEnterprise(enterprise.id);
//
return enterprise.copyWith(syncStatus: SyncStatus.synced);
case SyncStatus.synced:
case SyncStatus.untracked:
return enterprise;
}
return syncedDto.toModel().toEntity();
// [] DTO Model
// ID'synced'
final syncedModel = syncedDto.toModel(); //
await localDataSource.updateEnterprise(syncedModel);
// Model Entity
return syncedModel.toEntity();
}
@override
@ -192,4 +201,18 @@ class EnterpriseRepositoryImpl implements EnterpriseRepository {
final enterprises = models.map((model) => model.toEntity()).toList();
return enterprises;
}
@override
Future<Enterprise?> getEnterpriseById(String enterpriseId) async {
// 1. ID (Map)
final problemMap = await localDataSource.getEnterpriseById(enterpriseId);
// 2. null
if (problemMap == null) {
return null;
}
// 3. DTO
return EnterpriseModel.fromMap(problemMap).toEntity();
}
}

4
lib/app/features/enterprise/domain/repositories/enterprise_repository.dart

@ -23,7 +23,7 @@ abstract class EnterpriseRepository implements SyncableRepository<Enterprise> {
Future<Enterprise> syncEnterpriseToServer(Enterprise enterprise);
Future<void> updateEnterpriseSyncStatus(String id, SyncStatus synced) async {}
Future<void> updateEnterpriseSyncStatus(String id, SyncStatus synced);
// []
Future<SyncResult> syncWithServer();
@ -31,4 +31,6 @@ abstract class EnterpriseRepository implements SyncableRepository<Enterprise> {
Future<void> resolveConflictAndUpdate(Enterprise chosenEnterprise);
//
Future<List<Enterprise>> getAllEnterprises();
// ID获取企业信息
Future<Enterprise?> getEnterpriseById(String enterpriseId);
}

13
lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart

@ -20,7 +20,7 @@ class UploadEnterprisesUseCase {
}
///
/// todo -- (Request-Acknowledge-Update)
/// TODO Repository
/// [onProgress] (Controller)
Future<UploadResult> call({
required List<EnterpriseListItem> enterprisesToUpload,
@ -45,16 +45,7 @@ class UploadEnterprisesUseCase {
final enterprise = enterprisesToUpload[i].enterprise;
try {
// 1.
final syncedEnterprise = await repository.syncEnterpriseToServer(
enterprise,
);
// 2.
if (enterprise.syncStatus == SyncStatus.pendingDelete) {
// repository.deleteLocalEnterprise(enterprise.id);
} else {
await repository.updateEnterprise(syncedEnterprise);
}
await repository.syncEnterpriseToServer(enterprise);
successCount++;
} catch (e) {
//

15
lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart

@ -1,13 +1,15 @@
// lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart
import 'package:get/get.dart';
import 'package:problem_check_system/app/core/controllers/i_upload_controller.dart';
import 'package:problem_check_system/app/core/domain/entities/upload_result.dart';
import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart';
import 'package:problem_check_system/app/features/enterprise/domain/usecases/get_enterprise_list_usecase.dart';
import 'package:problem_check_system/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart';
import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart';
import 'package:problem_check_system/app/core/pages/widgets/upload_progress_dialog.dart';
class EnterpriseUploadController extends GetxController {
class EnterpriseUploadController extends GetxController
implements IUploadController {
final GetEnterpriseListUsecase getEnterpriseListUsecase;
final UploadEnterprisesUseCase uploadEnterprisesUseCase;
@ -21,10 +23,15 @@ class EnterpriseUploadController extends GetxController {
final enterprises = <EnterpriseListItem>[].obs;
final selectedEnterprises = <EnterpriseListItem>{}.obs;
// --- [] ---
@override
final isUploading = false.obs;
@override
final uploadProgress = 0.0.obs; // 0.0 1.0
@override
final uploadedCount = 0.obs;
@override
final totalToUpload = 0.obs;
@override
final uploadResult = Rxn<UploadResult>(); //
bool get allSelected =>
@ -74,7 +81,7 @@ class EnterpriseUploadController extends GetxController {
}
// 1.
Get.dialog(
const UploadProgressDialog(),
UploadProgressDialog(controller: this),
barrierDismissible: false, //
);
// 2.
@ -106,11 +113,13 @@ class EnterpriseUploadController extends GetxController {
}
/// []
@override
void cancelUpload() {
uploadEnterprisesUseCase.cancel();
}
/// []
@override
void closeUploadDialog() {
Get.back(); //
// true

16
lib/app/features/problem/data/datasources/problem_local_data_source.dart

@ -1,5 +1,4 @@
import 'package:get/get.dart';
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; // SyncStatus
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart';
import 'package:problem_check_system/app/core/services/database_service.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_filter_params.dart';
import 'package:sqflite/sqflite.dart'; // DatabaseService
@ -7,7 +6,7 @@ import 'package:sqflite/sqflite.dart'; // 导入你的 DatabaseService
/// IProblemLocalDataSource
/// 'problems' CRUD (, , , )
/// (Repository) ( Sqflite)
abstract class IProblemLocalDataSource {
abstract class ProblemLocalDataSource {
/// ID
///
/// [id] -
@ -40,11 +39,11 @@ abstract class IProblemLocalDataSource {
// IProblemLocalDataSource
class ProblemLocalDataSource implements IProblemLocalDataSource {
class ProblemLocalDataSourceImpl implements ProblemLocalDataSource {
final DatabaseService _databaseService;
final String _tableName = 'problems';
ProblemLocalDataSource({required DatabaseService databaseService})
ProblemLocalDataSourceImpl({required DatabaseService databaseService})
: _databaseService = databaseService;
@override
@ -102,10 +101,11 @@ class ProblemLocalDataSource implements IProblemLocalDataSource {
}
// 4.
if (filter.syncStatus != null) {
whereClauses.add('p.syncStatus = ?');
if (filter.isUploaded != null) {
final operator = filter.isUploaded! ? '=' : '!=';
whereClauses.add('p.syncStatus $operator ?');
// SyncStatus
arguments.add(filter.syncStatus!.name);
arguments.add(SyncStatus.synced.name);
}
// 5.

118
lib/app/features/problem/data/datasources/problem_remote_data_source.dart

@ -1 +1,117 @@
class ProblemRemoteDataSource {}
import 'package:dio/dio.dart';
import 'package:problem_check_system/app/core/services/http_provider.dart';
import 'package:problem_check_system/app/features/problem/data/model/problem_dto.dart';
import 'package:problem_check_system/app/features/problem/data/model/problem_filter_dto.dart';
///
///
/// API
/// DTO (Data Transfer Objects)
abstract class ProblemRemoteDataSource {
// --- (Read/Query) ---
///
///
/// [filter]: DTO
/// ProblemDto Future
Future<List<ProblemDto>> getProblems(ProblemFilterDto filter);
/// ID
///
/// [problemId]:
/// ProblemDto Future
Future<ProblemDto> getProblemById(String problemId);
// --- (Create) ---
///
///
/// [problem]: DTO
/// ID和时间戳
Future<ProblemDto> createProblem(ProblemDto problem);
// --- (Update) ---
///
///
/// [problem]: DTO
///
Future<ProblemDto> updateProblem(ProblemDto problem);
// --- (Delete) ---
/// ID
///
/// [problemId]:
/// 使 Future<void>
Future<void> deleteProblem(String problemId);
}
class ProblemRemoteDataSourceImpl implements ProblemRemoteDataSource {
final HttpProvider http;
static const String problemsEndpoint = '/api/Memorandum';
// Dio
ProblemRemoteDataSourceImpl({required this.http});
@override
Future<List<ProblemDto>> getProblems(ProblemFilterDto filter) async {
try {
final response = await http.get(
problemsEndpoint, //
queryParameters: filter.toJson(),
);
// 'data'
final List<dynamic> problemListJson = response.data['data'];
return problemListJson.map((json) => ProblemDto.fromJson(json)).toList();
} on DioException catch (e) {
//
throw Exception('获取问题列表失败: $e');
}
}
@override
Future<ProblemDto> getProblemById(String problemId) async {
try {
final response = await http.get('$problemsEndpoint/$problemId');
return ProblemDto.fromJson(response.data);
} on DioException catch (e) {
throw Exception('获取问题失败: $e');
}
}
@override
Future<ProblemDto> createProblem(ProblemDto problem) async {
try {
final response = await http.post(
problemsEndpoint,
data: problem.toJson(), // DTO Map
);
return ProblemDto.fromJson(response.data);
} on DioException catch (e) {
throw Exception('创建问题失败: $e');
}
}
@override
Future<ProblemDto> updateProblem(ProblemDto problem) async {
try {
final response = await http.put(
'$problemsEndpoint/${problem.id}', // PUT ID URL
data: problem.toJson(),
);
return ProblemDto.fromJson(response.data);
} on DioException catch (e) {
throw Exception('更新问题失败: $e');
}
}
@override
Future<void> deleteProblem(String problemId) async {
try {
// 204 No ContentDio
await http.delete('$problemsEndpoint/$problemId');
} on DioException catch (e) {
throw Exception('删除问题失败: $e');
}
}
}

145
lib/app/features/problem/data/model/problem_dto.dart

@ -1,115 +1,108 @@
import 'dart:convert';
import 'package:problem_check_system/app/core/extensions/map_extensions.dart';
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart';
import 'package:problem_check_system/app/features/enterprise/data/model/enterprise_model.dart';
import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart';
import 'package:problem_check_system/app/core/extensions/map_extensions.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart';
/// ProblemDto (Data Transfer Object)
///
/// API
/// API JSON
class ProblemDto {
final String? id;
final String id;
final String? title;
final String? location;
final String? censorTaskId;
final String? rowId;
final String? bindData;
final List<String?>? imageUrls;
final String? creationTime;
final List<String>? imageUrls;
final String creatorId;
final String creationTime;
final String? lastModifierId;
final String? lastModificationTime;
final String? companyId;
ProblemDto({
this.id,
required this.id,
this.title,
this.location,
this.censorTaskId,
this.rowId,
this.bindData,
this.imageUrls,
this.creationTime,
required this.creatorId,
required this.creationTime,
this.lastModifierId,
this.lastModificationTime,
this.companyId,
});
factory ProblemDto.fromModel(ProblemModel model) {
factory ProblemDto.fromEntity(ProblemEntity entity) {
return ProblemDto(
id: model.id,
title: model.description,
location: model.location,
bindData: model.bindData,
imageUrls: List<String>.from(jsonDecode(model.imageUrls)),
creationTime: model.creationTime,
companyId: model.enterpriseId,
id: entity.id,
title: entity.description,
location: entity.location,
bindData: entity.bindData,
imageUrls: entity.imageUrls,
creationTime: entity.creationTime.toIso8601String(),
creatorId: entity.creatorId,
companyId: entity.enterpriseId,
);
}
ProblemModel toModel() {
return ProblemModel(
id:
ProblemEntity toEntity() {
return ProblemEntity(
id: id,
description: title ?? '',
location: location ?? '',
imageUrls: imageUrls ?? [],
enterpriseId: companyId ?? '',
creatorId: creatorId,
creationTime: DateTime.tryParse(creationTime) ?? DateTime.now(),
lastModifierId: lastModifierId ?? creatorId,
lastModifiedTime:
DateTime.tryParse(lastModificationTime ?? '') ?? DateTime.now(),
syncStatus: SyncStatus.synced,
bindData: bindData,
);
}
// =======================================================================
//
// =======================================================================
/// [] fromJson JSON Map EnterpriseDto
///
/// JSON Dart
// FIX: fromJson -
factory ProblemDto.fromJson(Map<String, dynamic> json) {
final creationTime = DateTime.parse(json['creationTime'] as String);
final creatorId = json['creatorId'] as String;
final lastModTimeStr = json['lastModificationTime'] as String?;
return ProblemDto(
//
id: json['id'] as String,
creationTime: creationTime,
creatorId: creatorId,
lastModificationTime: lastModTimeStr != null
? DateTime.parse(lastModTimeStr)
: creationTime,
lastModifierId: json['lastModifierId'] as String? ?? creatorId,
companyName: json['companyName'] as String,
companyType: json['companyType'] as String? ?? "生产",
final creatorIdData = json['creatorId'] as String? ?? '';
final creationTimeData = json['creationTime'] as String? ?? '';
//
companyScope: json['companyScope'] as String?,
mainPrincipalName: json['mainPrincipalName'] as String?,
mainPrincipalPhone: json['mainPrincipalPhone'] as String?,
securityPrincipalName: json['securityPrincipalName'] as String?,
securityPrincipalPhone: json['securityPrincipalPhone'] as String?,
companyAddress: json['companyAddress'] as String?,
majorHazard: json['majorHazard'] as String?,
return ProblemDto(
id: json['id'] as String? ?? '',
creationTime: creationTimeData,
creatorId: creatorIdData,
lastModificationTime:
json['lastModificationTime'] as String? ?? creationTimeData,
lastModifierId: json['lastModifierId'] as String? ?? creatorIdData,
title: json['title'] as String?,
location: json['location'] as String?,
bindData: json['bindData'] as String?,
imageUrls: (json['imageUrls'] as List? ?? [])
.whereType<String>()
.toList(),
companyId: json['companyId'] as String?,
censorTaskId: json['censorTaskId'] as String?,
rowId: json['rowId'] as String?,
);
}
/// [] toJson EnterpriseDto JSON Map
///
/// JSON
// 🟡 :
Map<String, dynamic> toJson() {
final jsonMap = {
'id': id,
// 使 toIso8601String() DateTime
'creationTime': creationTime.toIso8601String(),
'creatorId': creatorId,
'lastModificationTime': lastModificationTime?.toIso8601String(),
'lastModifierId': lastModifierId,
'companyName': companyName,
'companyType': companyType,
'companyScope': companyScope,
'mainPrincipalName': mainPrincipalName,
'mainPrincipalPhone': mainPrincipalPhone,
'securityPrincipalName': securityPrincipalName,
'securityPrincipalPhone': securityPrincipalPhone,
'companyAddress': companyAddress,
'majorHazard': majorHazard,
//
"id": id,
"title": title,
"location": location,
"bindData": bindData,
"imageUrls": imageUrls,
"creatorId": creatorId,
"creationTime": creationTime,
"companyId": companyId,
"censorTaskId": censorTaskId,
"rowId": rowId,
"lastModifierId": lastModifierId,
"lastModificationTime": lastModificationTime,
};
return jsonMap.withoutNullOrEmptyValues;
}
/// [] DTO () Model (/)
///
///
}

3
lib/app/features/problem/data/model/problem_filter_dto.dart

@ -0,0 +1,3 @@
class ProblemFilterDto {
toJson() {}
}

70
lib/app/features/problem/data/repositories/problem_repository_impl.dart

@ -1,6 +1,8 @@
import 'dart:convert';
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart';
import 'package:problem_check_system/app/features/problem/data/datasources/problem_local_data_source.dart';
import 'package:problem_check_system/app/features/problem/data/datasources/problem_remote_data_source.dart';
import 'package:problem_check_system/app/features/problem/data/model/problem_dto.dart';
import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_filter_params.dart';
@ -10,18 +12,22 @@ import 'package:problem_check_system/app/features/problem/domain/repositories/pr
///
///
/// (ProblemEntity) (ProblemModel)
class ProblemRepository implements IProblemRepository {
final IProblemLocalDataSource problemLocalDataSource; // 2.
class ProblemRepositoryImpl implements ProblemRepository {
final ProblemLocalDataSource localDataSource;
final ProblemRemoteDataSource remoteDataSource;
ProblemRepository(this.problemLocalDataSource);
ProblemRepositoryImpl({
required this.localDataSource,
required this.remoteDataSource,
});
@override
Future<ProblemEntity> addProblem(ProblemEntity problem) async {
// 1. (Entity) (DTO)
final problemDto = ProblemModel.fromEntity(problem);
final problemModel = ProblemModel.fromEntity(problem);
// 2. DTO Map
await problemLocalDataSource.addProblem(problemDto.toMap());
await localDataSource.addProblem(problemModel.toMap());
// 3.
return problem;
@ -30,14 +36,14 @@ class ProblemRepository implements IProblemRepository {
@override
Future<void> deleteProblem(String id) async {
// ID
await problemLocalDataSource.deleteProblem(id);
await localDataSource.deleteProblem(id);
}
@override
Future<List<ProblemListItemEntity>> getAllProblemListItem({
required ProblemFilterParams filter,
}) async {
final problemDataMaps = await problemLocalDataSource.getAllProblems(filter);
final problemDataMaps = await localDataSource.getAllProblems(filter);
return problemDataMaps.map((map) {
// 1: Map ProblemEntity
@ -94,7 +100,7 @@ class ProblemRepository implements IProblemRepository {
@override
Future<ProblemEntity?> getProblemById(String id) async {
// 1. ID (Map)
final problemMap = await problemLocalDataSource.getProblemById(id);
final problemMap = await localDataSource.getProblemById(id);
// 2. null
if (problemMap == null) {
@ -108,37 +114,51 @@ class ProblemRepository implements IProblemRepository {
@override
Future<ProblemEntity> updateProblem(ProblemEntity problem) async {
// 1. (Entity) (DTO)
final problemDto = ProblemModel.fromEntity(problem);
final problemModel = ProblemModel.fromEntity(problem);
// 2. DTO Map
await problemLocalDataSource.updateProblem(problemDto.toMap());
await localDataSource.updateProblem(problemModel.toMap());
// 3.
return problem;
}
// TODO:
@override
Future<ProblemEntity> syncProblemToServer(ProblemEntity problem) {
// Domain Data Model
final problemModel = ProblemModel.fromEntity(problem);
EnterpriseDto syncedDto;
// ****
// Repository DataSource
switch (enterprise.syncStatus) {
Future<ProblemEntity> syncProblemToServer(ProblemEntity problem) async {
// Entity DTO
final problemDto = ProblemDto.fromEntity(problem);
ProblemDto syncedDto;
switch (problem.syncStatus) {
case SyncStatus.pendingCreate:
syncedDto = await remoteDataSource.createEnterprise(enterpriseModel);
syncedDto = await remoteDataSource.createProblem(problemDto);
break;
case SyncStatus.pendingUpdate:
syncedDto = await remoteDataSource.updateEnterprise(enterpriseModel);
syncedDto = await remoteDataSource.updateProblem(problemDto);
break;
case SyncStatus.pendingDelete:
await remoteDataSource.deleteEnterprise(enterprise.id);
return enterprise;
//
//
await remoteDataSource.deleteProblem(problemDto.id);
//
await localDataSource.deleteProblem(problem.id);
//
return problem.copyWith(syncStatus: SyncStatus.synced);
case SyncStatus.synced:
case SyncStatus.untracked:
return enterprise;
return problem;
}
return syncedDto.toModel().toEntity();
// DTO Entity
// syncedEntity ID
final syncedEntity = syncedDto.toEntity();
// Entity Model
//
final modelToSave = ProblemModel.fromEntity(syncedEntity);
await localDataSource.updateProblem(modelToSave.toMap());
// Entity
return syncedEntity;
}
}

6
lib/app/features/problem/domain/entities/problem_filter_params.dart

@ -1,5 +1,3 @@
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart';
///
class ProblemFilterParams {
/// ()
@ -12,7 +10,7 @@ class ProblemFilterParams {
final DateTime? endTime;
/// ()
final SyncStatus? syncStatus;
final bool? isUploaded;
/// (true: , false: , null: )
final bool? isBound;
@ -21,7 +19,7 @@ class ProblemFilterParams {
this.enterpriseName,
this.startTime,
this.endTime,
this.syncStatus,
this.isUploaded,
this.isBound,
});

2
lib/app/features/problem/domain/repositories/problem_repository.dart

@ -4,7 +4,7 @@ import 'package:problem_check_system/app/features/problem/domain/entities/proble
/// Problem
///
abstract class IProblemRepository {
abstract class ProblemRepository {
Future<List<ProblemListItemEntity>> getAllProblemListItem({
required ProblemFilterParams filter,
});

2
lib/app/features/problem/domain/usecases/add_problem_usecase.dart

@ -6,7 +6,7 @@ import 'package:problem_check_system/app/features/problem/domain/repositories/pr
import 'package:uuid/uuid.dart';
class AddProblemUsecase {
final IProblemRepository problemRepository;
final ProblemRepository problemRepository;
final AuthRepository authRepository;
final Uuid uuid;

2
lib/app/features/problem/domain/usecases/delete_problem.dart

@ -1,7 +1,7 @@
import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart';
class DeleteProblem {
final IProblemRepository problemRepository;
final ProblemRepository problemRepository;
DeleteProblem({required this.problemRepository});
Future<void> call(String id) async {

2
lib/app/features/problem/domain/usecases/get_all_problems_usecase.dart

@ -3,7 +3,7 @@ import 'package:problem_check_system/app/features/problem/domain/entities/proble
import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart';
class GetAllProblemsUsecase {
final IProblemRepository problemRepository;
final ProblemRepository problemRepository;
GetAllProblemsUsecase({required this.problemRepository});

2
lib/app/features/problem/domain/usecases/get_problem_by_id_usecase.dart

@ -2,7 +2,7 @@ import 'package:problem_check_system/app/features/problem/domain/entities/proble
import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart';
class GetProblemByIdUsecase {
final IProblemRepository problemRepository;
final ProblemRepository problemRepository;
GetProblemByIdUsecase({required this.problemRepository});
Future<ProblemEntity?> call(String id) async {

10
lib/app/features/problem/domain/usecases/update_problem_usecase.dart

@ -4,11 +4,11 @@ import 'package:problem_check_system/app/features/problem/domain/entities/proble
import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart';
class UpdateProblemUsecase {
final IProblemRepository repository;
final ProblemRepository problemRepository;
final AuthRepository authRepository;
UpdateProblemUsecase({
required this.repository,
required this.problemRepository,
required this.authRepository,
});
@ -18,8 +18,10 @@ class UpdateProblemUsecase {
final newProblem = entity.copyWith(
lastModifiedTime: nowUtc,
lastModifierId: userId,
syncStatus: SyncStatus.pendingUpdate,
syncStatus: entity.syncStatus == SyncStatus.pendingCreate
? SyncStatus.pendingCreate
: SyncStatus.pendingUpdate,
);
return await repository.updateProblem(newProblem);
return await problemRepository.updateProblem(newProblem);
}
}

131
lib/app/features/problem/domain/usecases/upload_problems_usecase.dart

@ -1,42 +1,57 @@
import 'dart:async';
import 'package:get/get.dart'; // GetX
import 'package:get/get.dart';
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart';
import 'package:problem_check_system/app/core/domain/entities/upload_result.dart';
import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart';
import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart';
import 'package:problem_check_system/app/features/enterprise/domain/repositories/enterprise_repository.dart';
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart';
import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository_impl.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_list_item_entity.dart';
///
///
///
/// :
/// - UI
/// -
/// -
/// - 便 UI
/// -
class UploadProblemsUsecase {
final ProblemRepository repository;
UploadProblemsUsecase({required this.repository});
final ProblemRepository problemRepository;
final EnterpriseRepository enterpriseRepository;
UploadProblemsUsecase({
required this.problemRepository,
required this.enterpriseRepository,
});
// 使 GetX RxBool
// Controller UseCase
//
final RxBool _isCancelled = false.obs;
/// []
/// [API]
void cancel() {
_isCancelled.value = true;
}
///
/// [onProgress] (Controller)
/// [API]
///
/// [problemsToUpload]:
/// [onProgress]: (, )
/// [onFailure]: () (, )
Future<UploadResult> call({
required List<ProblemListItemEntity> problemsToUpload,
required List<ProblemEntity> problemsToUpload,
required void Function(int uploaded, int total) onProgress,
void Function(ProblemEntity failedProblem, String error)? onFailure,
}) async {
// 便
_isCancelled.value = false;
_isCancelled.value = false; // 便
int successCount = 0;
int failureCount = 0;
final total = problemsToUpload.length;
// [2]
final Set<String> syncedEnterpriseIdsInThisSession = {};
for (int i = 0; i < total; i++) {
//
//
if (_isCancelled.value) {
//
return UploadResult(
successCount: successCount,
failureCount: failureCount,
@ -44,28 +59,80 @@ class UploadProblemsUsecase {
);
}
final problem = problemsToUpload[i].problemEntity;
final problem = problemsToUpload[i];
try {
// 1.
final syncedProblem = await repository.syncProblemToServer(problem);
// 2.
if (problem.syncStatus == SyncStatus.pendingDelete) {
// repository.deleteLocalEnterprise(enterprise.id);
} else {
await repository.updateProblem(syncedProblem);
}
// [3] 使
await _syncSingleProblemWithDependencies(
problem,
syncedEnterpriseIdsInThisSession,
);
successCount++;
} catch (e) {
//
failureCount++;
Get.log('上传失败: ${problem.description}, 错误: $e');
final errorMessage = '问题 [${problem.description}] 上传失败';
Get.log('$errorMessage, 错误: $e');
// [1] Controller
onFailure?.call(problem, e.toString());
}
// 3.
//
onProgress(i + 1, total);
}
//
return UploadResult(successCount: successCount, failureCount: failureCount);
return UploadResult(
successCount: successCount,
failureCount: failureCount,
wasCancelled: false,
);
}
/// []
Future<void> _syncSingleProblemWithDependencies(
ProblemEntity problem,
Set<String> syncedEnterpriseIds,
) async {
// --- A: ---
final enterprise = await enterpriseRepository.getEnterpriseById(
problem.enterpriseId,
);
//
if (enterprise == null) {
throw Exception('数据不一致:关联的企业 (ID: ${problem.enterpriseId}) 在本地数据库中不存在。');
}
//
final bool needsEnterpriseSync =
(enterprise.syncStatus != SyncStatus.synced &&
enterprise.syncStatus != SyncStatus.untracked);
if (needsEnterpriseSync && !syncedEnterpriseIds.contains(enterprise.id)) {
Get.log('优先同步依赖的企业 [${enterprise.name}]...');
try {
final syncedEnterprise = await enterpriseRepository
.syncEnterpriseToServer(enterprise);
Get.log('企业 [${syncedEnterprise.name}] 同步成功!');
// ID
syncedEnterpriseIds.add(syncedEnterprise.id);
} catch (e) {
//
throw Exception('其依赖的企业 [${enterprise.name}] 上传失败: $e');
}
}
// --- B: ---
//
// enterpriseId ID
//
final refreshedProblem = await problemRepository.getProblemById(problem.id);
if (refreshedProblem == null) {
throw Exception('数据不一致:在企业同步后,问题 (ID: ${problem.id}) 从本地数据库消失。');
}
Get.log('正在同步问题 [${refreshedProblem.description}]...');
await problemRepository.syncProblemToServer(refreshedProblem);
}
}

2
lib/app/features/problem/presentation/bindings/problem_form_binding.dart

@ -57,7 +57,7 @@ class ProblemFormBinding extends BaseBindings {
);
Get.lazyPut(
() => UpdateProblemUsecase(
repository: Get.find(),
problemRepository: Get.find(),
authRepository: Get.find(),
),
);

51
lib/app/features/problem/presentation/bindings/problem_list_binding.dart

@ -1,35 +1,52 @@
import 'package:get/get.dart';
import 'package:problem_check_system/app/core/bindings/base_bindings.dart';
import 'package:problem_check_system/app/core/services/database_service.dart';
import 'package:problem_check_system/app/features/problem/data/datasources/problem_local_data_source.dart';
import 'package:problem_check_system/app/features/problem/data/datasources/problem_remote_data_source.dart';
import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository_impl.dart';
import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart';
import 'package:problem_check_system/app/features/problem/domain/usecases/get_all_problems_usecase.dart';
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_list_controller.dart';
class ProblemListBinding extends Bindings {
class ProblemListBinding extends BaseBindings {
@override
void dependencies() {
//
Get.lazyPut<IProblemLocalDataSource>(
() =>
ProblemLocalDataSource(databaseService: Get.find<DatabaseService>()),
void register1Services() {
// TODO: implement register1Services
}
@override
void register2DataSource() {
Get.lazyPut<ProblemLocalDataSource>(
() => ProblemLocalDataSourceImpl(
databaseService: Get.find<DatabaseService>(),
),
);
//
Get.lazyPut<IProblemRepository>(
() => ProblemRepository(Get.find<IProblemLocalDataSource>()),
Get.lazyPut<ProblemRemoteDataSource>(
() => ProblemRemoteDataSourceImpl(http: Get.find()),
);
//
Get.lazyPut<GetAllProblemsUsecase>(
() => GetAllProblemsUsecase(
problemRepository: Get.find<IProblemRepository>(),
}
@override
void register3Repositories() {
Get.lazyPut<ProblemRepository>(
() => ProblemRepositoryImpl(
localDataSource: Get.find(),
remoteDataSource: Get.find(),
),
);
}
@override
void register4Usecases() {
Get.lazyPut<GetAllProblemsUsecase>(
() => GetAllProblemsUsecase(problemRepository: Get.find()),
);
}
///
@override
void register5Controllers() {
Get.lazyPut<ProblemListController>(
() => ProblemListController(
getAllProblemsUsecase: Get.find<GetAllProblemsUsecase>(),
),
() => ProblemListController(getAllProblemsUsecase: Get.find()),
);
}
}

55
lib/app/features/problem/presentation/bindings/problem_upload_binding.dart

@ -1,5 +1,15 @@
import 'package:get/get.dart';
import 'package:problem_check_system/app/core/bindings/base_bindings.dart';
import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_local_data_source.dart';
import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart';
import 'package:problem_check_system/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart';
import 'package:problem_check_system/app/features/enterprise/domain/repositories/enterprise_repository.dart';
import 'package:problem_check_system/app/features/problem/data/datasources/problem_local_data_source.dart';
import 'package:problem_check_system/app/features/problem/data/datasources/problem_remote_data_source.dart';
import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository_impl.dart';
import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart';
import 'package:problem_check_system/app/features/problem/domain/usecases/get_all_problems_usecase.dart';
import 'package:problem_check_system/app/features/problem/domain/usecases/upload_problems_usecase.dart';
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_upload_controller.dart';
class ProblemUploadBinding extends BaseBindings {
@ -10,21 +20,58 @@ class ProblemUploadBinding extends BaseBindings {
@override
void register2DataSource() {
// TODO: implement register2DataSource
Get.lazyPut<ProblemLocalDataSource>(
() => ProblemLocalDataSourceImpl(databaseService: Get.find()),
);
Get.lazyPut<ProblemRemoteDataSource>(
() => ProblemRemoteDataSourceImpl(http: Get.find()),
);
Get.lazyPut<EnterpriseLocalDataSource>(
() => EnterpriseLocalDataSourceImpl(databaseService: Get.find()),
);
Get.lazyPut<EnterpriseRemoteDataSource>(
() => EnterpriseRemoteDataSourceImpl(http: Get.find()),
);
}
@override
void register3Repositories() {
// TODO: implement register3Repositories
Get.lazyPut<ProblemRepository>(
() => ProblemRepositoryImpl(
localDataSource: Get.find(),
remoteDataSource: Get.find(),
),
);
Get.lazyPut<EnterpriseRepository>(
() => EnterpriseRepositoryImpl(
localDataSource: Get.find(),
remoteDataSource: Get.find(),
networkStatusService: Get.find(),
uuid: Get.find(),
),
);
}
@override
void register4Usecases() {
// TODO: implement register4Usecases
Get.lazyPut<GetAllProblemsUsecase>(
() => GetAllProblemsUsecase(problemRepository: Get.find()),
);
Get.lazyPut<UploadProblemsUsecase>(
() => UploadProblemsUsecase(
problemRepository: Get.find(),
enterpriseRepository: Get.find(),
),
);
}
@override
void register5Controllers() {
// Get.lazyPut(() => ProblemUploadController());
Get.lazyPut(
() => ProblemUploadController(
getAllProblemsUsecase: Get.find(),
uploadProblemsUsecase: Get.find(),
),
);
}
}

43
lib/app/features/problem/presentation/controllers/problem_form_controller.dart

@ -251,40 +251,49 @@ class ProblemFormController extends GetxController {
}
Get.back(result: true); //
} catch (e) {
Get.log('$e');
Get.snackbar('错误', '保存问题失败: $e');
} finally {
isLoading.value = false;
}
}
//
Future<List<String>> _saveImagesToLocal() async {
final List<String> imagePaths = [];
final List<String> finalImagePaths = [];
final directory = await getApplicationDocumentsDirectory();
final imagesDir = Directory('${directory.path}/problem_images');
//
if (!await imagesDir.exists()) {
await imagesDir.create(recursive: true);
}
//
for (var image in selectedImages) {
try {
final String fileName = '${Uuid().v4()}_${path.basename(image.name)}';
final String imagePath = '${imagesDir.path}/$fileName';
final File imageFile = File(imagePath);
//
final imageBytes = await image.readAsBytes();
await imageFile.writeAsBytes(imageBytes);
imagePaths.add(imagePath);
} catch (e) {
throw Exception(e);
// : XFile
//
if (image.path.startsWith(directory.path)) {
// 使
finalImagePaths.add(image.path);
} else {
//
try {
// ( .jpg)
final String extension = path.extension(image.path);
//
final String newFileName = '${const Uuid().v4()}$extension';
final String newImagePath = path.join(imagesDir.path, newFileName);
//
await image.saveTo(newImagePath); // XFile.saveTo()
finalImagePaths.add(newImagePath);
} catch (e) {
//
throw Exception('保存新图片失败: ${image.path}. 错误: $e');
}
}
}
return imagePaths;
return finalImagePaths;
}
// /

58
lib/app/features/problem/presentation/controllers/problem_upload_controller.dart

@ -1,14 +1,16 @@
// lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart
import 'package:get/get.dart';
import 'package:problem_check_system/app/core/controllers/i_upload_controller.dart';
import 'package:problem_check_system/app/core/domain/entities/upload_result.dart';
import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart';
import 'package:problem_check_system/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart';
import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart';
import 'package:problem_check_system/app/core/pages/widgets/upload_progress_dialog.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_filter_params.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_list_item_entity.dart';
import 'package:problem_check_system/app/features/problem/domain/usecases/get_all_problems_usecase.dart';
import 'package:problem_check_system/app/features/problem/domain/usecases/upload_problems_usecase.dart';
class ProblemUploadController extends GetxController {
class ProblemUploadController extends GetxController
implements IUploadController {
final GetAllProblemsUsecase getAllProblemsUsecase;
final UploadProblemsUsecase uploadProblemsUsecase;
@ -19,18 +21,22 @@ class ProblemUploadController extends GetxController {
// --- ---
final isLoading = false.obs;
final enterprises = <EnterpriseListItem>[].obs;
final selectedEnterprises = <EnterpriseListItem>{}.obs;
final problems = <ProblemListItemEntity>[].obs;
final selectedProblems = <ProblemListItemEntity>{}.obs;
// --- [] ---
@override
final isUploading = false.obs;
@override
final uploadProgress = 0.0.obs; // 0.0 1.0
@override
final uploadedCount = 0.obs;
@override
final totalToUpload = 0.obs;
@override
final uploadResult = Rxn<UploadResult>(); //
bool get allSelected =>
enterprises.isNotEmpty &&
enterprises.length == selectedEnterprises.length;
problems.isNotEmpty && problems.length == selectedProblems.length;
@override
void onInit() {
@ -38,20 +44,20 @@ class ProblemUploadController extends GetxController {
fetchPendingUploads();
}
void onSelectionChanged(EnterpriseListItem enterprise) {
if (selectedEnterprises.contains(enterprise)) {
selectedEnterprises.remove(enterprise);
void onSelectionChanged(ProblemListItemEntity problem) {
if (selectedProblems.contains(problem)) {
selectedProblems.remove(problem);
} else {
selectedEnterprises.add(enterprise);
selectedProblems.add(problem);
}
}
void toggleSelectAll() {
if (allSelected) {
selectedEnterprises.clear();
selectedProblems.clear();
} else {
//
selectedEnterprises.addAll(enterprises);
selectedProblems.addAll(problems);
}
}
@ -59,8 +65,10 @@ class ProblemUploadController extends GetxController {
Future<void> fetchPendingUploads() async {
isLoading.value = true;
try {
final data = await getEnterpriseListUsecase(isUploaded: false);
enterprises.assignAll(data);
final data = await getAllProblemsUsecase(
filter: ProblemFilterParams(isUploaded: false),
);
problems.assignAll(data);
} catch (e) {
Get.snackbar('错误', '加载待上传列表失败: $e');
} finally {
@ -69,13 +77,13 @@ class ProblemUploadController extends GetxController {
}
void confirmUpload() {
if (selectedEnterprises.isEmpty) {
Get.snackbar('提示', '请至少选择一个企业进行上传');
if (selectedProblems.isEmpty) {
Get.snackbar('提示', '请至少选择一个问题进行上传');
return;
}
// 1.
Get.dialog(
const UploadProgressDialog(),
UploadProgressDialog(controller: this),
barrierDismissible: false, //
);
// 2.
@ -88,12 +96,14 @@ class ProblemUploadController extends GetxController {
isUploading.value = true;
uploadProgress.value = 0.0;
uploadResult.value = null;
totalToUpload.value = selectedEnterprises.length;
totalToUpload.value = selectedProblems.length;
uploadedCount.value = 0;
// UseCase onProgress
final result = await uploadEnterprisesUseCase.call(
enterprisesToUpload: selectedEnterprises.toList(),
final result = await uploadProblemsUsecase(
problemsToUpload: selectedProblems
.map((item) => item.problemEntity)
.toList(),
onProgress: (uploaded, total) {
//
uploadedCount.value = uploaded;
@ -107,11 +117,13 @@ class ProblemUploadController extends GetxController {
}
/// []
@override
void cancelUpload() {
uploadEnterprisesUseCase.cancel();
uploadProblemsUsecase.cancel();
}
/// []
@override
void closeUploadDialog() {
Get.back(); //
// true

24
lib/app/features/problem/presentation/pages/problem_upload_page.dart

@ -3,8 +3,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import 'package:get/get_state_manager/src/simple/get_view.dart';
import 'package:problem_check_system/app/core/pages/widgets/upload_app_bar.dart';
import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart';
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_upload_controller.dart';
import 'package:problem_check_system/app/features/problem/presentation/pages/widgets/problem_card.dart';
class ProblemUploadPage extends GetView<ProblemUploadController> {
const ProblemUploadPage({super.key});
@ -16,9 +16,9 @@ class ProblemUploadPage extends GetView<ProblemUploadController> {
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Obx(
() => UploadAppBar(
selectedCount: controller.selectedEnterprises.length,
selectedCount: controller.selectedProblems.length,
allSelected: controller.allSelected,
buttonVisible: controller.enterprises.isNotEmpty,
buttonVisible: controller.problems.isNotEmpty,
onButtonPressed: () => controller.toggleSelectAll(),
),
),
@ -32,7 +32,7 @@ class ProblemUploadPage extends GetView<ProblemUploadController> {
),
child: Obx(
() => ElevatedButton(
onPressed: controller.selectedEnterprises.isNotEmpty
onPressed: controller.selectedProblems.isNotEmpty
? controller.confirmUpload
: null,
style: ElevatedButton.styleFrom(
@ -43,7 +43,7 @@ class ProblemUploadPage extends GetView<ProblemUploadController> {
),
minimumSize: Size(double.infinity, 48.h),
),
child: Text('确认上传 (${controller.selectedEnterprises.length})'),
child: Text('确认上传 (${controller.selectedProblems.length})'),
),
),
),
@ -54,11 +54,11 @@ class ProblemUploadPage extends GetView<ProblemUploadController> {
// 使 Obx controller Rx
return Obx(() {
//
if (controller.isLoading.value && controller.enterprises.isEmpty) {
if (controller.isLoading.value && controller.problems.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
//
if (controller.enterprises.isEmpty) {
if (controller.problems.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@ -83,18 +83,18 @@ class ProblemUploadPage extends GetView<ProblemUploadController> {
onRefresh: () async => controller.fetchPendingUploads(),
child: ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
itemCount: controller.enterprises.length,
itemCount: controller.problems.length,
itemBuilder: (context, index) {
final item = controller.enterprises[index];
final item = controller.problems[index];
// : base controller
return Obx(() {
final isSelected = controller.selectedEnterprises.contains(item);
final isSelected = controller.selectedProblems.contains(item);
return Padding(
padding: EdgeInsets.only(bottom: 12.h),
child: UnifiedEnterpriseCard(
enterpriseListItem: item,
child: ProblemCard(
problemListItem: item,
isSelected: isSelected,
// itemMode mode
// --- [] controller ---

Loading…
Cancel
Save