Files
worker/lib/features/projects/presentation/providers/submissions_provider.dart
Phuoc Nguyen 6e7e848ad6 submission
2025-11-27 17:58:13 +07:00

354 lines
11 KiB
Dart

/// Providers: Project Submissions
///
/// Riverpod providers for managing project submissions state.
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/core/network/dio_client.dart';
import 'package:worker/features/projects/data/datasources/project_progress_local_datasource.dart';
import 'package:worker/features/projects/data/datasources/project_status_local_datasource.dart';
import 'package:worker/features/projects/data/datasources/submissions_remote_datasource.dart';
import 'package:worker/features/projects/data/models/project_submission_request.dart';
import 'package:worker/features/projects/data/repositories/submissions_repository_impl.dart';
import 'package:worker/features/projects/domain/entities/project_progress.dart';
import 'package:worker/features/projects/domain/entities/project_status.dart';
import 'package:worker/features/projects/domain/entities/project_submission.dart';
import 'package:worker/features/projects/domain/repositories/submissions_repository.dart';
part 'submissions_provider.g.dart';
/// Project Status Local Data Source Provider
@riverpod
ProjectStatusLocalDataSource projectStatusLocalDataSource(Ref ref) {
return ProjectStatusLocalDataSource();
}
/// Project Progress Local Data Source Provider
@riverpod
ProjectProgressLocalDataSource projectProgressLocalDataSource(Ref ref) {
return ProjectProgressLocalDataSource();
}
/// Submissions Remote Data Source Provider
@riverpod
Future<SubmissionsRemoteDataSource> submissionsRemoteDataSource(Ref ref) async {
final dioClient = await ref.watch(dioClientProvider.future);
return SubmissionsRemoteDataSourceImpl(dioClient);
}
/// Submissions Repository Provider
@riverpod
Future<SubmissionsRepository> submissionsRepository(Ref ref) async {
final remoteDataSource = await ref.watch(submissionsRemoteDataSourceProvider.future);
final statusLocalDataSource = ref.watch(projectStatusLocalDataSourceProvider);
final progressLocalDataSource = ref.watch(projectProgressLocalDataSourceProvider);
return SubmissionsRepositoryImpl(
remoteDataSource,
statusLocalDataSource,
progressLocalDataSource,
);
}
/// Project Status List Provider
///
/// Fetches project status options from API with cache-first pattern.
/// This is loaded before submissions to ensure filter options are available.
@riverpod
class ProjectStatusList extends _$ProjectStatusList {
@override
Future<List<ProjectStatus>> build() async {
final repository = await ref.watch(submissionsRepositoryProvider.future);
return repository.getProjectStatusList();
}
/// Refresh status list from remote (force refresh)
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final repository = await ref.read(submissionsRepositoryProvider.future);
return repository.getProjectStatusList(forceRefresh: true);
});
}
}
/// Project Progress List Provider
///
/// Fetches construction progress stages from API with cache-first pattern.
/// Used for dropdown selection when creating/updating project submissions.
@riverpod
class ProjectProgressList extends _$ProjectProgressList {
@override
Future<List<ProjectProgress>> build() async {
final repository = await ref.watch(submissionsRepositoryProvider.future);
return repository.getProjectProgressList();
}
/// Refresh progress list from remote (force refresh)
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final repository = await ref.read(submissionsRepositoryProvider.future);
return repository.getProjectProgressList(forceRefresh: true);
});
}
}
/// All Submissions Provider
///
/// Fetches and manages submissions data from remote.
/// Waits for project status list and progress list to be loaded first.
@riverpod
class AllSubmissions extends _$AllSubmissions {
@override
Future<List<ProjectSubmission>> build() async {
// Ensure status list and progress list are loaded first (for filter options)
await ref.watch(projectStatusListProvider.future);
await ref.watch(projectProgressListProvider.future);
// Then fetch submissions
final repository = await ref.watch(submissionsRepositoryProvider.future);
return repository.getSubmissions();
}
/// Refresh submissions from remote
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
// Also refresh status list and progress list
await ref.read(projectStatusListProvider.notifier).refresh();
await ref.read(projectProgressListProvider.notifier).refresh();
final repository = await ref.read(submissionsRepositoryProvider.future);
return repository.getSubmissions();
});
}
}
/// Submissions Filter State
///
/// Manages search and status filter state.
/// Status filter uses the status label string from API (e.g., "Chờ phê duyệt").
@riverpod
class SubmissionsFilter extends _$SubmissionsFilter {
@override
({String searchQuery, String? selectedStatus}) build() {
return (searchQuery: '', selectedStatus: null);
}
/// Update search query
void updateSearchQuery(String query) {
state = (searchQuery: query, selectedStatus: state.selectedStatus);
}
/// Select a status filter (uses Vietnamese label from API)
void selectStatus(String? status) {
state = (searchQuery: state.searchQuery, selectedStatus: status);
}
/// Clear status filter
void clearStatusFilter() {
state = (searchQuery: state.searchQuery, selectedStatus: null);
}
/// Clear search query
void clearSearchQuery() {
state = (searchQuery: '', selectedStatus: state.selectedStatus);
}
/// Clear all filters
void clearAllFilters() {
state = (searchQuery: '', selectedStatus: null);
}
}
/// Filtered Submissions Provider
///
/// Combines submissions data with filter state to return filtered results.
@riverpod
AsyncValue<List<ProjectSubmission>> filteredSubmissions(Ref ref) {
final dataAsync = ref.watch(allSubmissionsProvider);
final filter = ref.watch(submissionsFilterProvider);
return dataAsync.whenData((submissions) {
var filtered = submissions;
// Filter by status (matches Vietnamese label from API)
if (filter.selectedStatus != null) {
filtered = filtered.where((s) => s.status == filter.selectedStatus).toList();
}
// Filter by search query
if (filter.searchQuery.isNotEmpty) {
final query = filter.searchQuery.toLowerCase();
filtered = filtered.where((s) {
return s.submissionId.toLowerCase().contains(query) ||
s.designedArea.toLowerCase().contains(query);
}).toList();
}
// Sort by request date (newest first)
filtered.sort((a, b) => b.requestDate.compareTo(a.requestDate));
return filtered;
});
}
/// Submission Detail Provider
///
/// Fetches full project detail by name for editing.
/// Uses family modifier to cache by submission name.
@riverpod
Future<ProjectSubmission> submissionDetail(Ref ref, String name) async {
final repository = await ref.watch(submissionsRepositoryProvider.future);
return repository.getSubmissionDetail(name);
}
/// Save Submission Provider
///
/// Handles creating new project submissions via API.
@riverpod
class SaveSubmission extends _$SaveSubmission {
@override
AsyncValue<void> build() {
return const AsyncValue.data(null);
}
/// Save a new project submission
///
/// Returns the project name (ID) if successful, throws exception on failure.
Future<String> save(ProjectSubmissionRequest request) async {
state = const AsyncValue.loading();
try {
final repository = await ref.read(submissionsRepositoryProvider.future);
if (!ref.mounted) throw Exception('Provider disposed');
final projectName = await repository.saveSubmission(request);
if (!ref.mounted) return projectName;
state = const AsyncValue.data(null);
// Refresh submissions list after successful save
ref.invalidate(allSubmissionsProvider);
return projectName;
} catch (e, st) {
if (ref.mounted) {
state = AsyncValue.error(e, st);
}
rethrow;
}
}
}
/// Upload state for tracking individual file uploads
class FileUploadState {
final String filePath;
final bool isUploading;
final bool isUploaded;
final String? fileUrl;
final String? error;
const FileUploadState({
required this.filePath,
this.isUploading = false,
this.isUploaded = false,
this.fileUrl,
this.error,
});
FileUploadState copyWith({
bool? isUploading,
bool? isUploaded,
String? fileUrl,
String? error,
}) {
return FileUploadState(
filePath: filePath,
isUploading: isUploading ?? this.isUploading,
isUploaded: isUploaded ?? this.isUploaded,
fileUrl: fileUrl ?? this.fileUrl,
error: error,
);
}
}
/// Upload Project Files Provider
///
/// Handles uploading multiple files for a project submission.
/// Tracks upload state for each file individually.
@riverpod
class UploadProjectFiles extends _$UploadProjectFiles {
@override
List<FileUploadState> build() {
return [];
}
/// Initialize with file paths
void initFiles(List<String> filePaths) {
state = filePaths
.map((path) => FileUploadState(filePath: path))
.toList();
}
/// Upload all files for a project
/// Returns list of uploaded file URLs
Future<List<String>> uploadAll(String projectName) async {
final uploadedUrls = <String>[];
for (var i = 0; i < state.length; i++) {
if (!ref.mounted) break;
// Mark as uploading
state = [
...state.sublist(0, i),
state[i].copyWith(isUploading: true),
...state.sublist(i + 1),
];
try {
final repository = await ref.read(submissionsRepositoryProvider.future);
if (!ref.mounted) break;
final fileUrl = await repository.uploadProjectFile(
projectName: projectName,
filePath: state[i].filePath,
);
if (!ref.mounted) break;
// Mark as uploaded
state = [
...state.sublist(0, i),
state[i].copyWith(
isUploading: false,
isUploaded: true,
fileUrl: fileUrl,
),
...state.sublist(i + 1),
];
uploadedUrls.add(fileUrl);
} catch (e) {
if (!ref.mounted) break;
// Mark as failed
state = [
...state.sublist(0, i),
state[i].copyWith(
isUploading: false,
error: e.toString(),
),
...state.sublist(i + 1),
];
}
}
return uploadedUrls;
}
/// Clear all files
void clear() {
state = [];
}
}