diff --git a/html/project-submission.html b/html/project-submission.html index 47508fa..9b34551 100644 --- a/html/project-submission.html +++ b/html/project-submission.html @@ -46,12 +46,22 @@ +
+ + +

Chi tiết dự án

+
+ + +
+ +
@@ -425,7 +435,7 @@ } } - // function resetForm() { + function resetForm() { if (confirm('Bạn có chắc muốn nhập lại toàn bộ thông tin?')) { document.getElementById('projectForm').reset(); uploadedFiles = []; diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 1b62964..f1f0f81 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -41,6 +41,7 @@ import 'package:worker/features/price_policy/price_policy.dart'; import 'package:worker/features/products/presentation/pages/product_detail_page.dart'; import 'package:worker/features/products/presentation/pages/products_page.dart'; import 'package:worker/features/products/presentation/pages/write_review_page.dart'; +import 'package:worker/features/projects/presentation/pages/submissions_page.dart'; import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart'; import 'package:worker/features/quotes/presentation/pages/quotes_page.dart'; import 'package:worker/features/showrooms/presentation/pages/design_request_create_page.dart'; @@ -354,6 +355,14 @@ final routerProvider = Provider((ref) { }, ), + // Submissions Route + GoRoute( + path: RouteNames.submissions, + name: RouteNames.submissions, + pageBuilder: (context, state) => + MaterialPage(key: state.pageKey, child: const SubmissionsPage()), + ), + // Quotes Route GoRoute( path: RouteNames.quotes, @@ -554,6 +563,7 @@ class RouteNames { static const String projects = '/projects'; static const String projectDetail = '/projects/:id'; static const String projectCreate = '/projects/create'; + static const String submissions = '/submissions'; static const String quotes = '/quotes'; static const String quoteDetail = '/quotes/:id'; static const String quoteCreate = '/quotes/create'; diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index a587540..97aa753 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -225,8 +225,7 @@ class _HomePageState extends ConsumerState { QuickAction( icon: FontAwesomeIcons.building, label: 'Đăng ký dự án', - onTap: () => - _showComingSoon(context, 'Đăng ký dự án', l10n), + onTap: () => context.push(RouteNames.submissions), ), ], ), diff --git a/lib/features/projects/data/datasources/submissions_remote_datasource.dart b/lib/features/projects/data/datasources/submissions_remote_datasource.dart new file mode 100644 index 0000000..ec4f838 --- /dev/null +++ b/lib/features/projects/data/datasources/submissions_remote_datasource.dart @@ -0,0 +1,168 @@ +/// Submissions Remote Data Source +/// +/// Handles remote API calls for project submissions. +library; + +import 'package:worker/features/projects/domain/entities/project_submission.dart'; + +/// Submissions Remote Data Source +/// +/// Abstract interface for remote submissions operations. +abstract class SubmissionsRemoteDataSource { + /// Fetch all submissions from remote API + Future> getSubmissions(); + + /// Fetch a single submission by ID + Future getSubmissionById(String submissionId); + + /// Create a new submission + Future createSubmission(ProjectSubmission submission); + + /// Update an existing submission + Future updateSubmission(ProjectSubmission submission); + + /// Delete a submission + Future deleteSubmission(String submissionId); +} + +/// Mock Implementation of Submissions Remote Data Source +/// +/// Provides mock data for development and testing. +class SubmissionsRemoteDataSourceImpl implements SubmissionsRemoteDataSource { + @override + Future> getSubmissions() async { + // Simulate network delay + await Future.delayed(const Duration(milliseconds: 500)); + + return [ + ProjectSubmission( + submissionId: 'DA001', + userId: 'user123', + projectName: 'Chung cư Vinhomes Grand Park - Block A1', + projectAddress: 'TP.HCM', + projectValue: 850000000, + projectType: ProjectType.residential, + status: SubmissionStatus.approved, + beforePhotos: [], + afterPhotos: [], + invoices: [], + submittedAt: DateTime(2023, 11, 15), + reviewedAt: DateTime(2023, 11, 20), + pointsEarned: 8500, + ), + ProjectSubmission( + submissionId: 'DA002', + userId: 'user123', + projectName: 'Trung tâm thương mại Bitexco', + projectAddress: 'TP.HCM', + projectValue: 2200000000, + projectType: ProjectType.commercial, + status: SubmissionStatus.pending, + beforePhotos: [], + afterPhotos: [], + invoices: [], + submittedAt: DateTime(2023, 11, 25), + ), + ProjectSubmission( + submissionId: 'DA003', + userId: 'user123', + projectName: 'Biệt thự sinh thái Ecopark', + projectAddress: 'Hà Nội', + projectValue: 420000000, + projectType: ProjectType.residential, + status: SubmissionStatus.approved, + beforePhotos: [], + afterPhotos: [], + invoices: [], + submittedAt: DateTime(2023, 10, 10), + reviewedAt: DateTime(2023, 10, 15), + pointsEarned: 4200, + ), + ProjectSubmission( + submissionId: 'DA004', + userId: 'user123', + projectName: 'Nhà xưởng sản xuất ABC', + projectAddress: 'Bình Dương', + projectValue: 1500000000, + projectType: ProjectType.industrial, + status: SubmissionStatus.rejected, + beforePhotos: [], + afterPhotos: [], + invoices: [], + submittedAt: DateTime(2023, 11, 20), + reviewedAt: DateTime(2023, 11, 28), + rejectionReason: 'Thiếu giấy phép xây dựng và báo cáo tác động môi trường', + ), + ProjectSubmission( + submissionId: 'DA005', + userId: 'user123', + projectName: 'Khách sạn 5 sao Diamond Plaza', + projectAddress: 'Đà Nẵng', + projectValue: 5800000000, + projectType: ProjectType.commercial, + status: SubmissionStatus.pending, + beforePhotos: [], + afterPhotos: [], + invoices: [], + submittedAt: DateTime(2023, 12, 1), + ), + ProjectSubmission( + submissionId: 'DA006', + userId: 'user123', + projectName: 'Khu đô thị thông minh Smart City', + projectAddress: 'Hà Nội', + projectValue: 8500000000, + projectType: ProjectType.residential, + status: SubmissionStatus.approved, + beforePhotos: [], + afterPhotos: [], + invoices: [], + submittedAt: DateTime(2023, 11, 10), + reviewedAt: DateTime(2023, 11, 18), + pointsEarned: 85000, + ), + ]; + } + + @override + Future getSubmissionById(String submissionId) async { + // Simulate network delay + await Future.delayed(const Duration(milliseconds: 300)); + + final submissions = await getSubmissions(); + return submissions.firstWhere( + (s) => s.submissionId == submissionId, + orElse: () => throw Exception('Submission not found'), + ); + } + + @override + Future createSubmission( + ProjectSubmission submission, + ) async { + // Simulate network delay + await Future.delayed(const Duration(milliseconds: 800)); + + // In real implementation, this would call the API + return submission; + } + + @override + Future updateSubmission( + ProjectSubmission submission, + ) async { + // Simulate network delay + await Future.delayed(const Duration(milliseconds: 600)); + + // In real implementation, this would call the API + return submission; + } + + @override + Future deleteSubmission(String submissionId) async { + // Simulate network delay + await Future.delayed(const Duration(milliseconds: 400)); + + // In real implementation, this would call the API + } +} diff --git a/lib/features/projects/data/repositories/submissions_repository_impl.dart b/lib/features/projects/data/repositories/submissions_repository_impl.dart new file mode 100644 index 0000000..b62dfbe --- /dev/null +++ b/lib/features/projects/data/repositories/submissions_repository_impl.dart @@ -0,0 +1,68 @@ +/// Submissions Repository Implementation +/// +/// Implements the submissions repository interface. +library; + +import 'package:worker/features/projects/data/datasources/submissions_remote_datasource.dart'; +import 'package:worker/features/projects/domain/entities/project_submission.dart'; +import 'package:worker/features/projects/domain/repositories/submissions_repository.dart'; + +/// Submissions Repository Implementation +/// +/// Handles data operations for project submissions. +class SubmissionsRepositoryImpl implements SubmissionsRepository { + + const SubmissionsRepositoryImpl(this._remoteDataSource); + final SubmissionsRemoteDataSource _remoteDataSource; + + @override + Future> getSubmissions() async { + try { + return await _remoteDataSource.getSubmissions(); + } catch (e) { + // In real implementation, handle errors properly + // For now, rethrow + rethrow; + } + } + + @override + Future getSubmissionById(String submissionId) async { + try { + return await _remoteDataSource.getSubmissionById(submissionId); + } catch (e) { + rethrow; + } + } + + @override + Future createSubmission( + ProjectSubmission submission, + ) async { + try { + return await _remoteDataSource.createSubmission(submission); + } catch (e) { + rethrow; + } + } + + @override + Future updateSubmission( + ProjectSubmission submission, + ) async { + try { + return await _remoteDataSource.updateSubmission(submission); + } catch (e) { + rethrow; + } + } + + @override + Future deleteSubmission(String submissionId) async { + try { + await _remoteDataSource.deleteSubmission(submissionId); + } catch (e) { + rethrow; + } + } +} diff --git a/lib/features/projects/domain/repositories/submissions_repository.dart b/lib/features/projects/domain/repositories/submissions_repository.dart new file mode 100644 index 0000000..3430d8c --- /dev/null +++ b/lib/features/projects/domain/repositories/submissions_repository.dart @@ -0,0 +1,26 @@ +/// Submissions Repository +/// +/// Repository interface for project submissions operations. +library; + +import 'package:worker/features/projects/domain/entities/project_submission.dart'; + +/// Submissions Repository +/// +/// Defines contract for project submissions data operations. +abstract class SubmissionsRepository { + /// Get all project submissions for the current user + Future> getSubmissions(); + + /// Get a single submission by ID + Future getSubmissionById(String submissionId); + + /// Create a new project submission + Future createSubmission(ProjectSubmission submission); + + /// Update an existing submission + Future updateSubmission(ProjectSubmission submission); + + /// Delete a submission + Future deleteSubmission(String submissionId); +} diff --git a/lib/features/projects/domain/usecases/get_submissions.dart b/lib/features/projects/domain/usecases/get_submissions.dart new file mode 100644 index 0000000..10fb1a1 --- /dev/null +++ b/lib/features/projects/domain/usecases/get_submissions.dart @@ -0,0 +1,23 @@ +/// Get Submissions Use Case +/// +/// Retrieves all project submissions for the current user. +library; + +import 'package:worker/features/projects/domain/entities/project_submission.dart'; +import 'package:worker/features/projects/domain/repositories/submissions_repository.dart'; + +/// Get Submissions Use Case +/// +/// Business logic for retrieving project submissions. +class GetSubmissions { + + const GetSubmissions(this._repository); + final SubmissionsRepository _repository; + + /// Execute the use case + /// + /// Returns list of all project submissions for the current user. + Future> call() async { + return await _repository.getSubmissions(); + } +} diff --git a/lib/features/projects/presentation/pages/submissions_page.dart b/lib/features/projects/presentation/pages/submissions_page.dart new file mode 100644 index 0000000..f38865d --- /dev/null +++ b/lib/features/projects/presentation/pages/submissions_page.dart @@ -0,0 +1,383 @@ +/// Page: Project Submissions List +/// +/// Displays list of user's project submissions with filters. +library; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:intl/intl.dart'; +import 'package:worker/core/constants/ui_constants.dart'; +import 'package:worker/core/theme/colors.dart'; +import 'package:worker/features/projects/domain/entities/project_submission.dart'; +import 'package:worker/features/projects/presentation/providers/submissions_provider.dart'; + +/// Project Submissions Page +class SubmissionsPage extends ConsumerWidget { + const SubmissionsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final submissionsAsync = ref.watch(filteredSubmissionsProvider); + final filter = ref.watch(submissionsFilterProvider); + final selectedStatus = filter.selectedStatus; + + return Scaffold( + backgroundColor: const Color(0xFFF4F6F8), + appBar: AppBar( + leading: IconButton( + icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), + onPressed: () => Navigator.of(context).pop(), + ), + title: const Text( + 'Danh sách Dự án', + style: TextStyle(color: Colors.black), + ), + actions: [ + IconButton( + icon: const FaIcon(FontAwesomeIcons.plus, color: Colors.black, size: 20), + onPressed: () { + // TODO: Navigate to create submission page + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Tạo dự án mới - Đang phát triển')), + ); + }, + ), + const SizedBox(width: AppSpacing.sm), + ], + elevation: AppBarSpecs.elevation, + backgroundColor: AppColors.white, + centerTitle: false, + ), + body: Column( + children: [ + // Search Bar + Padding( + padding: const EdgeInsets.all(16), + child: TextField( + decoration: InputDecoration( + hintText: 'Mã dự án hoặc tên dự án', + prefixIcon: const Icon(Icons.search, color: AppColors.grey500), + filled: true, + fillColor: AppColors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppColors.grey100), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppColors.grey100), + ), + ), + onChanged: (value) { + ref.read(submissionsFilterProvider.notifier).updateSearchQuery(value); + }, + ), + ), + + // Status Filter Tabs + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + _buildFilterChip( + context, + ref, + label: 'Tất cả', + isSelected: selectedStatus == null, + onTap: () => ref.read(submissionsFilterProvider.notifier).clearStatusFilter(), + ), + const SizedBox(width: 8), + ...SubmissionStatus.values.map((status) => Padding( + padding: const EdgeInsets.only(right: 8), + child: _buildFilterChip( + context, + ref, + label: status.displayName, + isSelected: selectedStatus == status, + onTap: () => ref.read(submissionsFilterProvider.notifier).selectStatus(status), + ), + )), + ], + ), + ), + + const SizedBox(height: 16), + + // Submissions List + Expanded( + child: submissionsAsync.when( + data: (submissions) { + if (submissions.isEmpty) { + return RefreshIndicator( + onRefresh: () async { + await ref.read(allSubmissionsProvider.notifier).refresh(); + }, + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.5, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const FaIcon( + FontAwesomeIcons.folderOpen, + size: 64, + color: AppColors.grey500, + ), + const SizedBox(height: 16), + const Text( + 'Không có dự án nào', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 8), + const Text( + 'Không tìm thấy dự án phù hợp', + style: TextStyle(color: AppColors.grey500), + ), + ], + ), + ), + ), + ], + ), + ); + } + + return RefreshIndicator( + onRefresh: () async { + await ref.read(allSubmissionsProvider.notifier).refresh(); + }, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: submissions.length, + itemBuilder: (context, index) { + final submission = submissions[index]; + return _buildSubmissionCard(context, submission); + }, + ), + ); + }, + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, stack) => RefreshIndicator( + onRefresh: () async { + await ref.read(allSubmissionsProvider.notifier).refresh(); + }, + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.5, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 64, + color: AppColors.danger, + ), + const SizedBox(height: 16), + const Text( + 'Có lỗi xảy ra', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 8), + Text( + error.toString(), + style: const TextStyle(color: AppColors.grey500), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + const Text( + 'Kéo xuống để thử lại', + style: TextStyle(color: AppColors.grey500), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildFilterChip( + BuildContext context, + WidgetRef ref, { + required String label, + required bool isSelected, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? AppColors.primaryBlue : AppColors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected ? AppColors.primaryBlue : AppColors.grey100, + ), + ), + child: Text( + label, + style: TextStyle( + color: isSelected ? AppColors.white : AppColors.grey900, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + ); + } + + Widget _buildSubmissionCard(BuildContext context, ProjectSubmission submission) { + return Card( + margin: const EdgeInsets.only(bottom: 12), + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: InkWell( + onTap: () { + // TODO: Navigate to submission detail + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Chi tiết dự án ${submission.submissionId}')), + ); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: _getStatusColor(submission.status), + width: 4, + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '#${submission.submissionId}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.grey900, + ), + ), + _buildStatusBadge(submission.status), + ], + ), + const SizedBox(height: 8), + Text( + 'Tên công trình: ${submission.projectName}', + style: const TextStyle( + fontSize: 14, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 4), + Text( + 'Ngày nộp: ${DateFormat('dd/MM/yyyy').format(submission.submittedAt)}', + style: const TextStyle( + fontSize: 13, + color: AppColors.grey500, + ), + ), + const SizedBox(height: 4), + Text( + 'Diện tích: ${submission.projectAddress ?? "N/A"}', + style: const TextStyle( + fontSize: 13, + color: AppColors.grey500, + ), + ), + if (submission.rejectionReason != null) ...[ + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFFFEF2F2), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + const FaIcon( + FontAwesomeIcons.triangleExclamation, + size: 14, + color: AppColors.danger, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + submission.rejectionReason!, + style: const TextStyle( + fontSize: 12, + color: AppColors.danger, + ), + ), + ), + ], + ), + ), + ], + ], + ), + ), + ), + ); + } + + Widget _buildStatusBadge(SubmissionStatus status) { + final color = _getStatusColor(status); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + status.displayName, + style: TextStyle( + color: color, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ); + } + + Color _getStatusColor(SubmissionStatus status) { + switch (status) { + case SubmissionStatus.pending: + return AppColors.warning; + case SubmissionStatus.reviewing: + return AppColors.info; + case SubmissionStatus.approved: + return AppColors.success; + case SubmissionStatus.rejected: + return AppColors.danger; + } + } +} diff --git a/lib/features/projects/presentation/providers/submissions_provider.dart b/lib/features/projects/presentation/providers/submissions_provider.dart new file mode 100644 index 0000000..6b823c8 --- /dev/null +++ b/lib/features/projects/presentation/providers/submissions_provider.dart @@ -0,0 +1,122 @@ +/// Providers: Project Submissions +/// +/// Riverpod providers for managing project submissions state. +library; + +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:worker/features/projects/data/datasources/submissions_remote_datasource.dart'; +import 'package:worker/features/projects/data/repositories/submissions_repository_impl.dart'; +import 'package:worker/features/projects/domain/entities/project_submission.dart'; +import 'package:worker/features/projects/domain/repositories/submissions_repository.dart'; +import 'package:worker/features/projects/domain/usecases/get_submissions.dart'; + +part 'submissions_provider.g.dart'; + +/// Submissions Remote Data Source Provider +@riverpod +SubmissionsRemoteDataSource submissionsRemoteDataSource(Ref ref) { + return SubmissionsRemoteDataSourceImpl(); +} + +/// Submissions Repository Provider +@riverpod +SubmissionsRepository submissionsRepository(Ref ref) { + final remoteDataSource = ref.watch(submissionsRemoteDataSourceProvider); + return SubmissionsRepositoryImpl(remoteDataSource); +} + +/// Get Submissions Use Case Provider +@riverpod +GetSubmissions getSubmissions(Ref ref) { + final repository = ref.watch(submissionsRepositoryProvider); + return GetSubmissions(repository); +} + +/// All Submissions Provider +/// +/// Fetches and manages submissions data from remote. +@riverpod +class AllSubmissions extends _$AllSubmissions { + @override + Future> build() async { + final useCase = ref.watch(getSubmissionsProvider); + return await useCase(); + } + + /// Refresh submissions from remote + Future refresh() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + final useCase = ref.read(getSubmissionsProvider); + return await useCase(); + }); + } +} + +/// Submissions Filter State +/// +/// Manages search and status filter state. +@riverpod +class SubmissionsFilter extends _$SubmissionsFilter { + @override + ({String searchQuery, SubmissionStatus? selectedStatus}) build() { + return (searchQuery: '', selectedStatus: null); + } + + /// Update search query + void updateSearchQuery(String query) { + state = (searchQuery: query, selectedStatus: state.selectedStatus); + } + + /// Select a status filter + void selectStatus(SubmissionStatus? 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 + 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.projectName.toLowerCase().contains(query); + }).toList(); + } + + // Sort by submitted date (newest first) + filtered.sort((a, b) => b.submittedAt.compareTo(a.submittedAt)); + + return filtered; + }); +} diff --git a/lib/features/projects/presentation/providers/submissions_provider.g.dart b/lib/features/projects/presentation/providers/submissions_provider.g.dart new file mode 100644 index 0000000..14966a4 --- /dev/null +++ b/lib/features/projects/presentation/providers/submissions_provider.g.dart @@ -0,0 +1,377 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'submissions_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Submissions Remote Data Source Provider + +@ProviderFor(submissionsRemoteDataSource) +const submissionsRemoteDataSourceProvider = + SubmissionsRemoteDataSourceProvider._(); + +/// Submissions Remote Data Source Provider + +final class SubmissionsRemoteDataSourceProvider + extends + $FunctionalProvider< + SubmissionsRemoteDataSource, + SubmissionsRemoteDataSource, + SubmissionsRemoteDataSource + > + with $Provider { + /// Submissions Remote Data Source Provider + const SubmissionsRemoteDataSourceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'submissionsRemoteDataSourceProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$submissionsRemoteDataSourceHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + SubmissionsRemoteDataSource create(Ref ref) { + return submissionsRemoteDataSource(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(SubmissionsRemoteDataSource value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$submissionsRemoteDataSourceHash() => + r'dc2dd71b6ca22d26382c1dfdf13b88d2249bb5ce'; + +/// Submissions Repository Provider + +@ProviderFor(submissionsRepository) +const submissionsRepositoryProvider = SubmissionsRepositoryProvider._(); + +/// Submissions Repository Provider + +final class SubmissionsRepositoryProvider + extends + $FunctionalProvider< + SubmissionsRepository, + SubmissionsRepository, + SubmissionsRepository + > + with $Provider { + /// Submissions Repository Provider + const SubmissionsRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'submissionsRepositoryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$submissionsRepositoryHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + SubmissionsRepository create(Ref ref) { + return submissionsRepository(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(SubmissionsRepository value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$submissionsRepositoryHash() => + r'4fa33107966470c07f050b27e669ec1dc4f13fda'; + +/// Get Submissions Use Case Provider + +@ProviderFor(getSubmissions) +const getSubmissionsProvider = GetSubmissionsProvider._(); + +/// Get Submissions Use Case Provider + +final class GetSubmissionsProvider + extends $FunctionalProvider + with $Provider { + /// Get Submissions Use Case Provider + const GetSubmissionsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'getSubmissionsProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$getSubmissionsHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + GetSubmissions create(Ref ref) { + return getSubmissions(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(GetSubmissions value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$getSubmissionsHash() => r'91b497f826ae6dc72618ba879289fc449a7ef5cb'; + +/// All Submissions Provider +/// +/// Fetches and manages submissions data from remote. + +@ProviderFor(AllSubmissions) +const allSubmissionsProvider = AllSubmissionsProvider._(); + +/// All Submissions Provider +/// +/// Fetches and manages submissions data from remote. +final class AllSubmissionsProvider + extends $AsyncNotifierProvider> { + /// All Submissions Provider + /// + /// Fetches and manages submissions data from remote. + const AllSubmissionsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'allSubmissionsProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$allSubmissionsHash(); + + @$internal + @override + AllSubmissions create() => AllSubmissions(); +} + +String _$allSubmissionsHash() => r'40ea0460a8962a4105dabb482bc80573452d4c80'; + +/// All Submissions Provider +/// +/// Fetches and manages submissions data from remote. + +abstract class _$AllSubmissions + extends $AsyncNotifier> { + FutureOr> build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = + this.ref + as $Ref< + AsyncValue>, + List + >; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + AsyncValue>, + List + >, + AsyncValue>, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +/// Submissions Filter State +/// +/// Manages search and status filter state. + +@ProviderFor(SubmissionsFilter) +const submissionsFilterProvider = SubmissionsFilterProvider._(); + +/// Submissions Filter State +/// +/// Manages search and status filter state. +final class SubmissionsFilterProvider + extends + $NotifierProvider< + SubmissionsFilter, + ({String searchQuery, SubmissionStatus? selectedStatus}) + > { + /// Submissions Filter State + /// + /// Manages search and status filter state. + const SubmissionsFilterProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'submissionsFilterProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$submissionsFilterHash(); + + @$internal + @override + SubmissionsFilter create() => SubmissionsFilter(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue( + ({String searchQuery, SubmissionStatus? selectedStatus}) value, + ) { + return $ProviderOverride( + origin: this, + providerOverride: + $SyncValueProvider< + ({String searchQuery, SubmissionStatus? selectedStatus}) + >(value), + ); + } +} + +String _$submissionsFilterHash() => r'049dd9fa4f6f1bff0d49c6cba0975f9714621883'; + +/// Submissions Filter State +/// +/// Manages search and status filter state. + +abstract class _$SubmissionsFilter + extends + $Notifier<({String searchQuery, SubmissionStatus? selectedStatus})> { + ({String searchQuery, SubmissionStatus? selectedStatus}) build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = + this.ref + as $Ref< + ({String searchQuery, SubmissionStatus? selectedStatus}), + ({String searchQuery, SubmissionStatus? selectedStatus}) + >; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + ({String searchQuery, SubmissionStatus? selectedStatus}), + ({String searchQuery, SubmissionStatus? selectedStatus}) + >, + ({String searchQuery, SubmissionStatus? selectedStatus}), + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +/// Filtered Submissions Provider +/// +/// Combines submissions data with filter state to return filtered results. + +@ProviderFor(filteredSubmissions) +const filteredSubmissionsProvider = FilteredSubmissionsProvider._(); + +/// Filtered Submissions Provider +/// +/// Combines submissions data with filter state to return filtered results. + +final class FilteredSubmissionsProvider + extends + $FunctionalProvider< + AsyncValue>, + AsyncValue>, + AsyncValue> + > + with $Provider>> { + /// Filtered Submissions Provider + /// + /// Combines submissions data with filter state to return filtered results. + const FilteredSubmissionsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'filteredSubmissionsProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$filteredSubmissionsHash(); + + @$internal + @override + $ProviderElement>> $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + AsyncValue> create(Ref ref) { + return filteredSubmissions(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(AsyncValue> value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>>( + value, + ), + ); + } +} + +String _$filteredSubmissionsHash() => + r'd0a07ab78a0d98596f01d0ed0a25016d573db5aa';