/// 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(Ref ref) async { final dioClient = await ref.watch(dioClientProvider.future); return SubmissionsRemoteDataSourceImpl(dioClient); } /// Submissions Repository Provider @riverpod Future 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> build() async { final repository = await ref.watch(submissionsRepositoryProvider.future); return repository.getProjectStatusList(); } /// Refresh status list from remote (force refresh) Future 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> build() async { final repository = await ref.watch(submissionsRepositoryProvider.future); return repository.getProjectProgressList(); } /// Refresh progress list from remote (force refresh) Future 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> 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 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> 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 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 build() { return const AsyncValue.data(null); } /// Save a new project submission /// /// Returns the project name (ID) if successful, throws exception on failure. Future 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 build() { return []; } /// Initialize with file paths void initFiles(List filePaths) { state = filePaths .map((path) => FileUploadState(filePath: path)) .toList(); } /// Upload all files for a project /// Returns list of uploaded file URLs Future> uploadAll(String projectName) async { final uploadedUrls = []; 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 = []; } }