diff --git a/docs/projects.sh b/docs/projects.sh new file mode 100644 index 0000000..84de842 --- /dev/null +++ b/docs/projects.sh @@ -0,0 +1,64 @@ +#get status list +curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_project_status_list' \ +--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \ +--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \ +--header 'Content-Type: application/json' \ +--data '{ + "limit_start": 0, + "limit_page_length": 0 +}' + +#response +{ + "message": [ + { + "status": "Pending approval", + "label": "Chờ phê duyệt", + "color": "Warning", + "index": 1 + }, + { + "status": "Approved", + "label": "Đã được phê duyệt", + "color": "Success", + "index": 2 + }, + { + "status": "Rejected", + "label": "Từ chối", + "color": "Danger", + "index": 3 + }, + { + "status": "Cancelled", + "label": "HỦY BỎ", + "color": "Danger", + "index": 4 + } + ] +} + + +#get project list +curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_list' \ +--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \ +--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \ +--header 'Content-Type: application/json' \ +--data '{ + "limit_start": 0, + "limit_page_length": 0 +}' +#response +{ + "message": [ + { + "name": "p9ti8veq2g", + "designed_area": "Sunrise Villa Phase 355", + "design_area": 350.5, + "request_date": "2025-11-26 09:30:00", + "status": "Đã được phê duyệt", + "reason_for_rejection": null, + "status_color": "Success" + } + ] +} \ No newline at end of file diff --git a/lib/core/constants/api_constants.dart b/lib/core/constants/api_constants.dart index 55123ea..42d1264 100644 --- a/lib/core/constants/api_constants.dart +++ b/lib/core/constants/api_constants.dart @@ -272,31 +272,45 @@ class ApiConstants { static const String getPaymentDetails = '/payments'; // ============================================================================ - // Project Endpoints + // Project Endpoints (Frappe ERPNext) // ============================================================================ - /// Create new project + /// Get project status list (requires sid and csrf_token) + /// POST /api/method/building_material.building_material.api.project.get_project_status_list + /// Body: { "limit_start": 0, "limit_page_length": 0 } + /// Returns: { "message": [{ "status": "...", "label": "...", "color": "...", "index": 0 }] } + static const String getProjectStatusList = + '/building_material.building_material.api.project.get_project_status_list'; + + /// Get list of project submissions (requires sid and csrf_token) + /// POST /api/method/building_material.building_material.api.project.get_list + /// Body: { "limit_start": 0, "limit_page_length": 0 } + /// Returns: { "message": [{ "name": "...", "designed_area": "...", "design_area": 0, ... }] } + static const String getProjectList = + '/building_material.building_material.api.project.get_list'; + + /// Create new project (legacy endpoint - may be deprecated) /// POST /projects static const String createProject = '/projects'; - /// Get user's projects + /// Get user's projects (legacy endpoint - may be deprecated) /// GET /projects?status={status}&page={page}&limit={limit} static const String getProjects = '/projects'; - /// Get project details by ID + /// Get project details by ID (legacy endpoint - may be deprecated) /// GET /projects/{projectId} static const String getProjectDetails = '/projects'; - /// Update project + /// Update project (legacy endpoint - may be deprecated) /// PUT /projects/{projectId} static const String updateProject = '/projects'; - /// Update project progress + /// Update project progress (legacy endpoint - may be deprecated) /// PATCH /projects/{projectId}/progress /// Body: { "progress": 75 } static const String updateProjectProgress = '/projects'; - /// Delete project + /// Delete project (legacy endpoint - may be deprecated) /// DELETE /projects/{projectId} static const String deleteProject = '/projects'; diff --git a/lib/core/constants/storage_constants.dart b/lib/core/constants/storage_constants.dart index 81e7566..672adbd 100644 --- a/lib/core/constants/storage_constants.dart +++ b/lib/core/constants/storage_constants.dart @@ -64,6 +64,9 @@ class HiveBoxNames { /// Order status list cache static const String orderStatusBox = 'order_status_box'; + /// Project status list cache + static const String projectStatusBox = 'project_status_box'; + /// Get all box names for initialization static List get allBoxes => [ userBox, @@ -77,6 +80,7 @@ class HiveBoxNames { cityBox, wardBox, orderStatusBox, + projectStatusBox, settingsBox, cacheBox, syncStateBox, @@ -139,6 +143,7 @@ class HiveTypeIds { static const int cityModel = 31; static const int wardModel = 32; static const int orderStatusModel = 62; + static const int projectStatusModel = 63; // Enums (33-61) static const int userRole = 33; @@ -239,6 +244,37 @@ class OrderStatusIndex { static const int cancelled = 6; } +/// Project Status Indices +/// +/// Index values for project statuses stored in Hive. +/// These correspond to the index field in ProjectStatusModel. +/// +/// API Response Structure: +/// - status: "Pending approval" (English status name) +/// - label: "Chờ phê duyệt" (Vietnamese display label) +/// - color: "Warning" (Status color indicator) +/// - index: 1 (Unique identifier) +class ProjectStatusIndex { + // Private constructor to prevent instantiation + ProjectStatusIndex._(); + + /// Pending approval - "Chờ phê duyệt" + /// Color: Warning + static const int pendingApproval = 1; + + /// Approved - "Đã được phê duyệt" + /// Color: Success + static const int approved = 2; + + /// Rejected - "Từ chối" + /// Color: Danger + static const int rejected = 3; + + /// Cancelled - "HỦY BỎ" + /// Color: Danger + static const int cancelled = 4; +} + /// Hive Keys (continued) extension HiveKeysContinued on HiveKeys { // Cache Box Keys diff --git a/lib/core/database/hive_service.dart b/lib/core/database/hive_service.dart index f51f761..22d73d4 100644 --- a/lib/core/database/hive_service.dart +++ b/lib/core/database/hive_service.dart @@ -102,9 +102,15 @@ class HiveService { debugPrint( 'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.orderStatus) ? "✓" : "✗"} OrderStatus adapter', ); + debugPrint( + 'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.orderStatusModel) ? "✓" : "✗"} OrderStatusModel adapter', + ); debugPrint( 'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectType) ? "✓" : "✗"} ProjectType adapter', ); + debugPrint( + 'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectStatusModel) ? "✓" : "✗"} ProjectStatusModel adapter', + ); debugPrint( 'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.entryType) ? "✓" : "✗"} EntryType adapter', ); @@ -171,6 +177,9 @@ class HiveService { // Order status box (non-sensitive) - caches order status list from API Hive.openBox(HiveBoxNames.orderStatusBox), + + // Project status box (non-sensitive) - caches project status list from API + Hive.openBox(HiveBoxNames.projectStatusBox), ]); // Open potentially encrypted boxes (sensitive data) diff --git a/lib/core/network/api_interceptor.g.dart b/lib/core/network/api_interceptor.g.dart index a21528d..27cfe02 100644 --- a/lib/core/network/api_interceptor.g.dart +++ b/lib/core/network/api_interceptor.g.dart @@ -189,7 +189,7 @@ final class LoggingInterceptorProvider } String _$loggingInterceptorHash() => - r'6afa480caa6fcd723dab769bb01601b8a37e20fd'; + r'4d3147e9084d261e14653386ecd74ee471993af4'; /// Provider for ErrorTransformerInterceptor diff --git a/lib/features/auth/presentation/providers/auth_provider.g.dart b/lib/features/auth/presentation/providers/auth_provider.g.dart index 2c5181b..4bdfc92 100644 --- a/lib/features/auth/presentation/providers/auth_provider.g.dart +++ b/lib/features/auth/presentation/providers/auth_provider.g.dart @@ -272,7 +272,7 @@ final class AuthProvider extends $AsyncNotifierProvider { Auth create() => Auth(); } -String _$authHash() => r'f1a16022d628a21f230c0bb567e80ff6e293d840'; +String _$authHash() => r'f0438cf6eb9eb17c0afc6b23055acd09926b21ae'; /// Authentication Provider /// diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index d8ce2b1..4390c8a 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -239,18 +239,4 @@ class _HomePageState extends ConsumerState { ), ); } - - /// Show coming soon message - void _showComingSoon( - BuildContext context, - String feature, - AppLocalizations l10n, - ) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('$feature - ${l10n.comingSoon}'), - duration: const Duration(seconds: 1), - ), - ); - } } diff --git a/lib/features/projects/data/datasources/project_status_local_datasource.dart b/lib/features/projects/data/datasources/project_status_local_datasource.dart new file mode 100644 index 0000000..8097c2a --- /dev/null +++ b/lib/features/projects/data/datasources/project_status_local_datasource.dart @@ -0,0 +1,47 @@ +/// Project Status Local Data Source +/// +/// Handles local caching of project status list using Hive. +library; + +import 'package:hive_ce/hive.dart'; +import 'package:worker/core/constants/storage_constants.dart'; +import 'package:worker/features/projects/data/models/project_status_model.dart'; + +/// Project Status Local Data Source +class ProjectStatusLocalDataSource { + /// Get Hive box for project statuses + Box get _box => Hive.box(HiveBoxNames.projectStatusBox); + + /// Save project status list to cache + Future cacheStatusList(List statuses) async { + // Clear existing cache + await _box.clear(); + + // Save each status with its index as key + for (final status in statuses) { + await _box.put(status.index, status); + } + } + + /// Get cached project status list + List getCachedStatusList() { + try { + final values = _box.values.whereType().toList() + // Sort by index + ..sort((a, b) => a.index.compareTo(b.index)); + return values; + } catch (e) { + return []; + } + } + + /// Check if cache exists and is not empty + bool hasCachedData() { + return _box.isNotEmpty; + } + + /// Clear all cached statuses + Future clearCache() async { + await _box.clear(); + } +} diff --git a/lib/features/projects/data/datasources/submissions_remote_datasource.dart b/lib/features/projects/data/datasources/submissions_remote_datasource.dart index ec4f838..ff809ee 100644 --- a/lib/features/projects/data/datasources/submissions_remote_datasource.dart +++ b/lib/features/projects/data/datasources/submissions_remote_datasource.dart @@ -3,166 +3,107 @@ /// Handles remote API calls for project submissions. library; -import 'package:worker/features/projects/domain/entities/project_submission.dart'; +import 'package:worker/core/constants/api_constants.dart'; +import 'package:worker/core/network/dio_client.dart'; +import 'package:worker/features/projects/data/models/project_status_model.dart'; +import 'package:worker/features/projects/data/models/project_submission_model.dart'; /// Submissions Remote Data Source /// -/// Abstract interface for remote submissions operations. +/// Interface for remote project submission operations. abstract class SubmissionsRemoteDataSource { + /// Fetch project status list from API + Future> getProjectStatusList(); + /// 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); + Future> getSubmissions({ + int limitStart = 0, + int limitPageLength = 0, + }); } -/// Mock Implementation of Submissions Remote Data Source +/// Submissions Remote Data Source Implementation /// -/// Provides mock data for development and testing. +/// Uses Frappe API endpoints for project submissions. class SubmissionsRemoteDataSourceImpl implements SubmissionsRemoteDataSource { - @override - Future> getSubmissions() async { - // Simulate network delay - await Future.delayed(const Duration(milliseconds: 500)); + const SubmissionsRemoteDataSourceImpl(this._dioClient); - 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, - ), - ]; + final DioClient _dioClient; + + /// Get project status list + /// + /// Calls: POST /api/method/building_material.building_material.api.project.get_project_status_list + /// Body: { "limit_start": 0, "limit_page_length": 0 } + /// Returns: List of project statuses with labels and colors + @override + Future> getProjectStatusList() async { + try { + final response = await _dioClient.post>( + '${ApiConstants.frappeApiMethod}${ApiConstants.getProjectStatusList}', + data: { + 'limit_start': 0, + 'limit_page_length': 0, + }, + ); + + final data = response.data; + if (data == null) { + throw Exception('No data received from getProjectStatusList API'); + } + + // API returns: { "message": [...] } + final message = data['message']; + if (message == null) { + throw Exception('No message field in getProjectStatusList response'); + } + + final List statusList = message as List; + return statusList + .map((json) => + ProjectStatusModel.fromJson(json as Map)) + .toList(); + } catch (e) { + throw Exception('Failed to get project status list: $e'); + } } + /// Get list of project submissions + /// + /// Calls: POST /api/method/building_material.building_material.api.project.get_list + /// Body: { "limit_start": 0, "limit_page_length": 0 } + /// Returns: List of project submissions @override - Future getSubmissionById(String submissionId) async { - // Simulate network delay - await Future.delayed(const Duration(milliseconds: 300)); + Future> getSubmissions({ + int limitStart = 0, + int limitPageLength = 0, + }) async { + try { + final response = await _dioClient.post>( + '${ApiConstants.frappeApiMethod}${ApiConstants.getProjectList}', + data: { + 'limit_start': limitStart, + 'limit_page_length': limitPageLength, + }, + ); - final submissions = await getSubmissions(); - return submissions.firstWhere( - (s) => s.submissionId == submissionId, - orElse: () => throw Exception('Submission not found'), - ); - } + final data = response.data; + if (data == null) { + throw Exception('No data received from getProjectList API'); + } - @override - Future createSubmission( - ProjectSubmission submission, - ) async { - // Simulate network delay - await Future.delayed(const Duration(milliseconds: 800)); + // API returns: { "message": [...] } + final message = data['message']; + if (message == null) { + throw Exception('No message field in getProjectList response'); + } - // 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 + final List submissionsList = message as List; + return submissionsList + .map((json) => + ProjectSubmissionModel.fromJson(json as Map)) + .toList(); + } catch (e) { + throw Exception('Failed to get project submissions: $e'); + } } } diff --git a/lib/features/projects/data/models/project_status_model.dart b/lib/features/projects/data/models/project_status_model.dart new file mode 100644 index 0000000..f98cc81 --- /dev/null +++ b/lib/features/projects/data/models/project_status_model.dart @@ -0,0 +1,73 @@ +/// Project Status Model +/// +/// Data model for project status from API responses with Hive caching. +library; + +import 'package:hive_ce/hive.dart'; +import 'package:worker/core/constants/storage_constants.dart'; +import 'package:worker/features/projects/domain/entities/project_status.dart'; + +part 'project_status_model.g.dart'; + +/// Project Status Model - Type ID: 63 +@HiveType(typeId: HiveTypeIds.projectStatusModel) +class ProjectStatusModel extends HiveObject { + @HiveField(0) + final String status; + + @HiveField(1) + final String label; + + @HiveField(2) + final String color; + + @HiveField(3) + final int index; + + ProjectStatusModel({ + required this.status, + required this.label, + required this.color, + required this.index, + }); + + /// Create from JSON + factory ProjectStatusModel.fromJson(Map json) { + return ProjectStatusModel( + status: json['status'] as String, + label: json['label'] as String, + color: json['color'] as String, + index: json['index'] as int, + ); + } + + /// Convert to JSON + Map toJson() { + return { + 'status': status, + 'label': label, + 'color': color, + 'index': index, + }; + } + + /// Convert to entity + ProjectStatus toEntity() { + return ProjectStatus( + status: status, + label: label, + color: color, + index: index, + ); + } + + /// Create from entity + factory ProjectStatusModel.fromEntity(ProjectStatus entity) { + return ProjectStatusModel( + status: entity.status, + label: entity.label, + color: entity.color, + index: entity.index, + ); + } +} diff --git a/lib/features/projects/data/models/project_status_model.g.dart b/lib/features/projects/data/models/project_status_model.g.dart new file mode 100644 index 0000000..e106244 --- /dev/null +++ b/lib/features/projects/data/models/project_status_model.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'project_status_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ProjectStatusModelAdapter extends TypeAdapter { + @override + final typeId = 63; + + @override + ProjectStatusModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ProjectStatusModel( + status: fields[0] as String, + label: fields[1] as String, + color: fields[2] as String, + index: (fields[3] as num).toInt(), + ); + } + + @override + void write(BinaryWriter writer, ProjectStatusModel obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.status) + ..writeByte(1) + ..write(obj.label) + ..writeByte(2) + ..write(obj.color) + ..writeByte(3) + ..write(obj.index); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ProjectStatusModelAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/features/projects/data/models/project_submission_model.dart b/lib/features/projects/data/models/project_submission_model.dart index bc0e147..0d2cf1a 100644 --- a/lib/features/projects/data/models/project_submission_model.dart +++ b/lib/features/projects/data/models/project_submission_model.dart @@ -1,129 +1,105 @@ -import 'dart:convert'; +/// Project Submission Model +/// +/// Data model for project submission from API responses with Hive caching. +/// Based on API response from building_material.building_material.api.project.get_list +library; + import 'package:hive_ce/hive.dart'; import 'package:worker/core/constants/storage_constants.dart'; -import 'package:worker/core/database/models/enums.dart'; +import 'package:worker/features/projects/domain/entities/project_submission.dart'; part 'project_submission_model.g.dart'; +/// Project Submission Model - Type ID: 14 @HiveType(typeId: HiveTypeIds.projectSubmissionModel) class ProjectSubmissionModel extends HiveObject { - ProjectSubmissionModel({ - required this.submissionId, - required this.userId, - required this.projectName, - required this.projectAddress, - required this.projectValue, - required this.projectType, - this.beforePhotos, - this.afterPhotos, - this.invoices, - required this.status, - this.reviewNotes, - this.rejectionReason, - this.pointsEarned, - required this.submittedAt, - this.reviewedAt, - this.reviewedBy, - }); - + /// Unique submission identifier (API: name) @HiveField(0) final String submissionId; + + /// Project name/title (API: designed_area) @HiveField(1) - final String userId; + final String designedArea; + + /// Design area value in square meters (API: design_area) @HiveField(2) - final String projectName; + final double designArea; + + /// Submission/request date (API: request_date) @HiveField(3) - final String projectAddress; + final DateTime requestDate; + + /// Status label - Vietnamese (API: status) @HiveField(4) - final double projectValue; + final String status; + + /// Rejection reason if rejected (API: reason_for_rejection) @HiveField(5) - final ProjectType projectType; + final String? reasonForRejection; + + /// Status color indicator (API: status_color) @HiveField(6) - final String? beforePhotos; - @HiveField(7) - final String? afterPhotos; - @HiveField(8) - final String? invoices; - @HiveField(9) - final SubmissionStatus status; - @HiveField(10) - final String? reviewNotes; - @HiveField(11) - final String? rejectionReason; - @HiveField(12) - final int? pointsEarned; - @HiveField(13) - final DateTime submittedAt; - @HiveField(14) - final DateTime? reviewedAt; - @HiveField(15) - final String? reviewedBy; + final String statusColor; - factory ProjectSubmissionModel.fromJson( - Map json, - ) => ProjectSubmissionModel( - submissionId: json['submission_id'] as String, - userId: json['user_id'] as String, - projectName: json['project_name'] as String, - projectAddress: json['project_address'] as String, - projectValue: (json['project_value'] as num).toDouble(), - projectType: ProjectType.values.firstWhere( - (e) => e.name == json['project_type'], - ), - beforePhotos: json['before_photos'] != null - ? jsonEncode(json['before_photos']) - : null, - afterPhotos: json['after_photos'] != null - ? jsonEncode(json['after_photos']) - : null, - invoices: json['invoices'] != null ? jsonEncode(json['invoices']) : null, - status: SubmissionStatus.values.firstWhere((e) => e.name == json['status']), - reviewNotes: json['review_notes'] as String?, - rejectionReason: json['rejection_reason'] as String?, - pointsEarned: json['points_earned'] as int?, - submittedAt: DateTime.parse(json['submitted_at']?.toString() ?? ''), - reviewedAt: json['reviewed_at'] != null - ? DateTime.parse(json['reviewed_at']?.toString() ?? '') - : null, - reviewedBy: json['reviewed_by'] as String?, - ); + ProjectSubmissionModel({ + required this.submissionId, + required this.designedArea, + required this.designArea, + required this.requestDate, + required this.status, + this.reasonForRejection, + required this.statusColor, + }); - Map toJson() => { - 'submission_id': submissionId, - 'user_id': userId, - 'project_name': projectName, - 'project_address': projectAddress, - 'project_value': projectValue, - 'project_type': projectType.name, - 'before_photos': beforePhotos != null ? jsonDecode(beforePhotos!) : null, - 'after_photos': afterPhotos != null ? jsonDecode(afterPhotos!) : null, - 'invoices': invoices != null ? jsonDecode(invoices!) : null, - 'status': status.name, - 'review_notes': reviewNotes, - 'rejection_reason': rejectionReason, - 'points_earned': pointsEarned, - 'submitted_at': submittedAt.toIso8601String(), - 'reviewed_at': reviewedAt?.toIso8601String(), - 'reviewed_by': reviewedBy, - }; - - List? get beforePhotosList { - if (beforePhotos == null) return null; - try { - final decoded = jsonDecode(beforePhotos!) as List; - return decoded.map((e) => e.toString()).toList(); - } catch (e) { - return null; - } + /// Create from JSON (API response) + factory ProjectSubmissionModel.fromJson(Map json) { + return ProjectSubmissionModel( + submissionId: json['name'] as String, + designedArea: json['designed_area'] as String, + designArea: (json['design_area'] as num).toDouble(), + requestDate: DateTime.parse(json['request_date'] as String), + status: json['status'] as String, + reasonForRejection: json['reason_for_rejection'] as String?, + statusColor: json['status_color'] as String, + ); } - List? get afterPhotosList { - if (afterPhotos == null) return null; - try { - final decoded = jsonDecode(afterPhotos!) as List; - return decoded.map((e) => e.toString()).toList(); - } catch (e) { - return null; - } + /// Convert to JSON + Map toJson() { + return { + 'name': submissionId, + 'designed_area': designedArea, + 'design_area': designArea, + 'request_date': requestDate.toIso8601String(), + 'status': status, + 'reason_for_rejection': reasonForRejection, + 'status_color': statusColor, + }; + } + + /// Convert to entity + ProjectSubmission toEntity() { + return ProjectSubmission( + submissionId: submissionId, + designedArea: designedArea, + designArea: designArea, + requestDate: requestDate, + status: status, + reasonForRejection: reasonForRejection, + statusColor: statusColor, + ); + } + + /// Create from entity + factory ProjectSubmissionModel.fromEntity(ProjectSubmission entity) { + return ProjectSubmissionModel( + submissionId: entity.submissionId, + designedArea: entity.designedArea, + designArea: entity.designArea, + requestDate: entity.requestDate, + status: entity.status, + reasonForRejection: entity.reasonForRejection, + statusColor: entity.statusColor, + ); } } diff --git a/lib/features/projects/data/models/project_submission_model.g.dart b/lib/features/projects/data/models/project_submission_model.g.dart index 3456bf0..168f84a 100644 --- a/lib/features/projects/data/models/project_submission_model.g.dart +++ b/lib/features/projects/data/models/project_submission_model.g.dart @@ -19,60 +19,33 @@ class ProjectSubmissionModelAdapter }; return ProjectSubmissionModel( submissionId: fields[0] as String, - userId: fields[1] as String, - projectName: fields[2] as String, - projectAddress: fields[3] as String, - projectValue: (fields[4] as num).toDouble(), - projectType: fields[5] as ProjectType, - beforePhotos: fields[6] as String?, - afterPhotos: fields[7] as String?, - invoices: fields[8] as String?, - status: fields[9] as SubmissionStatus, - reviewNotes: fields[10] as String?, - rejectionReason: fields[11] as String?, - pointsEarned: (fields[12] as num?)?.toInt(), - submittedAt: fields[13] as DateTime, - reviewedAt: fields[14] as DateTime?, - reviewedBy: fields[15] as String?, + designedArea: fields[1] as String, + designArea: (fields[2] as num).toDouble(), + requestDate: fields[3] as DateTime, + status: fields[4] as String, + reasonForRejection: fields[5] as String?, + statusColor: fields[6] as String, ); } @override void write(BinaryWriter writer, ProjectSubmissionModel obj) { writer - ..writeByte(16) + ..writeByte(7) ..writeByte(0) ..write(obj.submissionId) ..writeByte(1) - ..write(obj.userId) + ..write(obj.designedArea) ..writeByte(2) - ..write(obj.projectName) + ..write(obj.designArea) ..writeByte(3) - ..write(obj.projectAddress) + ..write(obj.requestDate) ..writeByte(4) - ..write(obj.projectValue) - ..writeByte(5) - ..write(obj.projectType) - ..writeByte(6) - ..write(obj.beforePhotos) - ..writeByte(7) - ..write(obj.afterPhotos) - ..writeByte(8) - ..write(obj.invoices) - ..writeByte(9) ..write(obj.status) - ..writeByte(10) - ..write(obj.reviewNotes) - ..writeByte(11) - ..write(obj.rejectionReason) - ..writeByte(12) - ..write(obj.pointsEarned) - ..writeByte(13) - ..write(obj.submittedAt) - ..writeByte(14) - ..write(obj.reviewedAt) - ..writeByte(15) - ..write(obj.reviewedBy); + ..writeByte(5) + ..write(obj.reasonForRejection) + ..writeByte(6) + ..write(obj.statusColor); } @override diff --git a/lib/features/projects/data/repositories/submissions_repository_impl.dart b/lib/features/projects/data/repositories/submissions_repository_impl.dart index b62dfbe..524ad01 100644 --- a/lib/features/projects/data/repositories/submissions_repository_impl.dart +++ b/lib/features/projects/data/repositories/submissions_repository_impl.dart @@ -1,66 +1,85 @@ /// Submissions Repository Implementation /// -/// Implements the submissions repository interface. +/// Implements the submissions repository interface with caching support. library; +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/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'; /// Submissions Repository Implementation /// -/// Handles data operations for project submissions. +/// Handles data operations for project submissions with cache-first pattern. class SubmissionsRepositoryImpl implements SubmissionsRepository { + const SubmissionsRepositoryImpl( + this._remoteDataSource, + this._statusLocalDataSource, + ); - const SubmissionsRepositoryImpl(this._remoteDataSource); final SubmissionsRemoteDataSource _remoteDataSource; + final ProjectStatusLocalDataSource _statusLocalDataSource; + /// Get project status list with cache-first pattern + /// + /// 1. Return cached data if available + /// 2. Fetch from API in background and update cache + /// 3. If no cache, wait for API response @override - Future> getSubmissions() async { + Future> getProjectStatusList({ + bool forceRefresh = false, + }) async { + // Check cache first (unless force refresh) + if (!forceRefresh && _statusLocalDataSource.hasCachedData()) { + final cachedStatuses = _statusLocalDataSource.getCachedStatusList(); + if (cachedStatuses.isNotEmpty) { + // Return cached data immediately + // Also refresh cache in background (fire and forget) + _refreshStatusCache(); + return cachedStatuses.map((model) => model.toEntity()).toList(); + } + } + + // No cache or force refresh - fetch from API try { - return await _remoteDataSource.getSubmissions(); + final statusModels = await _remoteDataSource.getProjectStatusList(); + + // Cache the result + await _statusLocalDataSource.cacheStatusList(statusModels); + + return statusModels.map((model) => model.toEntity()).toList(); } catch (e) { - // In real implementation, handle errors properly - // For now, rethrow + // If API fails, try to return cached data as fallback + final cachedStatuses = _statusLocalDataSource.getCachedStatusList(); + if (cachedStatuses.isNotEmpty) { + return cachedStatuses.map((model) => model.toEntity()).toList(); + } rethrow; } } - @override - Future getSubmissionById(String submissionId) async { + /// Refresh status cache in background + Future _refreshStatusCache() async { try { - return await _remoteDataSource.getSubmissionById(submissionId); + final statusModels = await _remoteDataSource.getProjectStatusList(); + await _statusLocalDataSource.cacheStatusList(statusModels); } catch (e) { - rethrow; + // Silently fail - we already returned cached data } } @override - Future createSubmission( - ProjectSubmission submission, - ) async { + Future> getSubmissions({ + int limitStart = 0, + int limitPageLength = 0, + }) 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); + final submissionModels = await _remoteDataSource.getSubmissions( + limitStart: limitStart, + limitPageLength: limitPageLength, + ); + return submissionModels.map((model) => model.toEntity()).toList(); } catch (e) { rethrow; } diff --git a/lib/features/projects/domain/entities/design_request.dart b/lib/features/projects/domain/entities/design_request.dart index de20a15..cafae8b 100644 --- a/lib/features/projects/domain/entities/design_request.dart +++ b/lib/features/projects/domain/entities/design_request.dart @@ -3,7 +3,7 @@ /// Represents a request for design consultation service. library; -import 'project_submission.dart'; +import 'package:worker/features/projects/domain/entities/project_type.dart'; /// Design status enum enum DesignStatus { diff --git a/lib/features/projects/domain/entities/project_status.dart b/lib/features/projects/domain/entities/project_status.dart new file mode 100644 index 0000000..70db5f7 --- /dev/null +++ b/lib/features/projects/domain/entities/project_status.dart @@ -0,0 +1,33 @@ +/// Project Status Entity +/// +/// Represents a project status option from the API. +library; + +import 'package:equatable/equatable.dart'; + +/// Project Status Entity +/// +/// Similar to OrderStatus - represents status options for project submissions. +class ProjectStatus extends Equatable { + /// Status value (e.g., "Pending approval", "Approved", "Rejected", "Cancelled") + final String status; + + /// Vietnamese label (e.g., "Chờ phê duyệt", "Đã được phê duyệt", "Từ chối", "HỦY BỎ") + final String label; + + /// Color indicator (e.g., "Warning", "Success", "Danger") + final String color; + + /// Display order index + final int index; + + const ProjectStatus({ + required this.status, + required this.label, + required this.color, + required this.index, + }); + + @override + List get props => [status, label, color, index]; +} diff --git a/lib/features/projects/domain/entities/project_submission.dart b/lib/features/projects/domain/entities/project_submission.dart index 405957b..ae04bba 100644 --- a/lib/features/projects/domain/entities/project_submission.dart +++ b/lib/features/projects/domain/entities/project_submission.dart @@ -1,242 +1,100 @@ /// Domain Entity: Project Submission /// /// Represents a completed project submitted for loyalty points. +/// Based on API response from building_material.building_material.api.project.get_list library; -/// Project type enum -enum ProjectType { - /// Residential project - residential, - - /// Commercial project - commercial, - - /// Industrial project - industrial, - - /// Public infrastructure - infrastructure, - - /// Other type - other; - - /// Get display name for project type - String get displayName { - switch (this) { - case ProjectType.residential: - return 'Residential'; - case ProjectType.commercial: - return 'Commercial'; - case ProjectType.industrial: - return 'Industrial'; - case ProjectType.infrastructure: - return 'Infrastructure'; - case ProjectType.other: - return 'Other'; - } - } -} - -/// Submission status enum -enum SubmissionStatus { - /// Submitted, pending review - pending, - - /// Under review - reviewing, - - /// Approved, points awarded - approved, - - /// Rejected - rejected; - - /// Get display name for status - String get displayName { - switch (this) { - case SubmissionStatus.pending: - return 'Pending'; - case SubmissionStatus.reviewing: - return 'Reviewing'; - case SubmissionStatus.approved: - return 'Approved'; - case SubmissionStatus.rejected: - return 'Rejected'; - } - } -} +import 'package:equatable/equatable.dart'; /// Project Submission Entity /// -/// Contains information about a completed project: -/// - Project details -/// - Before/after photos -/// - Invoice documentation -/// - Review status -/// - Points earned -class ProjectSubmission { - /// Unique submission identifier +/// Contains information about a completed project submission. +/// Mapped from API response: +/// - name -> submissionId +/// - designed_area -> designedArea (project name/title) +/// - design_area -> designArea (area value in m²) +/// - request_date -> requestDate +/// - status -> status (Vietnamese label) +/// - reason_for_rejection -> reasonForRejection +/// - status_color -> statusColor +class ProjectSubmission extends Equatable { + /// Unique submission identifier (API: name) final String submissionId; - /// User ID who submitted - final String userId; + /// Project name/title (API: designed_area) + final String designedArea; - /// Project name - final String projectName; + /// Design area value in square meters (API: design_area) + final double designArea; - /// Project address/location - final String? projectAddress; + /// Submission/request date (API: request_date) + final DateTime requestDate; - /// Project value/cost - final double projectValue; + /// Status label - Vietnamese (API: status) + /// e.g., "Chờ phê duyệt", "Đã được phê duyệt", "Từ chối", "HỦY BỎ" + final String status; - /// Project type - final ProjectType projectType; + /// Rejection reason if rejected (API: reason_for_rejection) + final String? reasonForRejection; - /// Before photos URLs - final List beforePhotos; - - /// After photos URLs - final List afterPhotos; - - /// Invoice/receipt URLs - final List invoices; - - /// Submission status - final SubmissionStatus status; - - /// Review notes from admin - final String? reviewNotes; - - /// Rejection reason (if rejected) - final String? rejectionReason; - - /// Points earned (if approved) - final int? pointsEarned; - - /// Submission timestamp - final DateTime submittedAt; - - /// Review timestamp - final DateTime? reviewedAt; - - /// ID of admin who reviewed - final String? reviewedBy; + /// Status color indicator (API: status_color) + /// Values: "Warning", "Success", "Danger" + final String statusColor; const ProjectSubmission({ required this.submissionId, - required this.userId, - required this.projectName, - this.projectAddress, - required this.projectValue, - required this.projectType, - required this.beforePhotos, - required this.afterPhotos, - required this.invoices, + required this.designedArea, + required this.designArea, + required this.requestDate, required this.status, - this.reviewNotes, - this.rejectionReason, - this.pointsEarned, - required this.submittedAt, - this.reviewedAt, - this.reviewedBy, + this.reasonForRejection, + required this.statusColor, }); - /// Check if submission is pending - bool get isPending => status == SubmissionStatus.pending; - - /// Check if submission is under review - bool get isReviewing => status == SubmissionStatus.reviewing; + /// Check if submission is pending approval + bool get isPending => statusColor == 'Warning'; /// Check if submission is approved - bool get isApproved => status == SubmissionStatus.approved; + bool get isApproved => statusColor == 'Success'; - /// Check if submission is rejected - bool get isRejected => status == SubmissionStatus.rejected; - - /// Check if submission has been reviewed - bool get isReviewed => - status == SubmissionStatus.approved || - status == SubmissionStatus.rejected; - - /// Check if submission has before photos - bool get hasBeforePhotos => beforePhotos.isNotEmpty; - - /// Check if submission has after photos - bool get hasAfterPhotos => afterPhotos.isNotEmpty; - - /// Check if submission has invoices - bool get hasInvoices => invoices.isNotEmpty; - - /// Get total number of photos - int get totalPhotos => beforePhotos.length + afterPhotos.length; - - /// Get review duration - Duration? get reviewDuration { - if (reviewedAt == null) return null; - return reviewedAt!.difference(submittedAt); - } + /// Check if submission is rejected or cancelled + bool get isRejected => statusColor == 'Danger'; /// Copy with method for immutability ProjectSubmission copyWith({ String? submissionId, - String? userId, - String? projectName, - String? projectAddress, - double? projectValue, - ProjectType? projectType, - List? beforePhotos, - List? afterPhotos, - List? invoices, - SubmissionStatus? status, - String? reviewNotes, - String? rejectionReason, - int? pointsEarned, - DateTime? submittedAt, - DateTime? reviewedAt, - String? reviewedBy, + String? designedArea, + double? designArea, + DateTime? requestDate, + String? status, + String? reasonForRejection, + String? statusColor, }) { return ProjectSubmission( submissionId: submissionId ?? this.submissionId, - userId: userId ?? this.userId, - projectName: projectName ?? this.projectName, - projectAddress: projectAddress ?? this.projectAddress, - projectValue: projectValue ?? this.projectValue, - projectType: projectType ?? this.projectType, - beforePhotos: beforePhotos ?? this.beforePhotos, - afterPhotos: afterPhotos ?? this.afterPhotos, - invoices: invoices ?? this.invoices, + designedArea: designedArea ?? this.designedArea, + designArea: designArea ?? this.designArea, + requestDate: requestDate ?? this.requestDate, status: status ?? this.status, - reviewNotes: reviewNotes ?? this.reviewNotes, - rejectionReason: rejectionReason ?? this.rejectionReason, - pointsEarned: pointsEarned ?? this.pointsEarned, - submittedAt: submittedAt ?? this.submittedAt, - reviewedAt: reviewedAt ?? this.reviewedAt, - reviewedBy: reviewedBy ?? this.reviewedBy, + reasonForRejection: reasonForRejection ?? this.reasonForRejection, + statusColor: statusColor ?? this.statusColor, ); } @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is ProjectSubmission && - other.submissionId == submissionId && - other.userId == userId && - other.projectName == projectName && - other.projectValue == projectValue && - other.status == status; - } - - @override - int get hashCode { - return Object.hash(submissionId, userId, projectName, projectValue, status); - } + List get props => [ + submissionId, + designedArea, + designArea, + requestDate, + status, + reasonForRejection, + statusColor, + ]; @override String toString() { - return 'ProjectSubmission(submissionId: $submissionId, projectName: $projectName, ' - 'projectValue: $projectValue, projectType: $projectType, status: $status, ' - 'pointsEarned: $pointsEarned)'; + return 'ProjectSubmission(submissionId: $submissionId, designedArea: $designedArea, ' + 'designArea: $designArea, status: $status, statusColor: $statusColor)'; } } diff --git a/lib/features/projects/domain/entities/project_type.dart b/lib/features/projects/domain/entities/project_type.dart new file mode 100644 index 0000000..339bd35 --- /dev/null +++ b/lib/features/projects/domain/entities/project_type.dart @@ -0,0 +1,38 @@ +/// Project Type Enum +/// +/// Represents the type of construction project. +library; + +/// Project type enum +enum ProjectType { + /// Residential project + residential, + + /// Commercial project + commercial, + + /// Industrial project + industrial, + + /// Public infrastructure + infrastructure, + + /// Other type + other; + + /// Get display name for project type + String get displayName { + switch (this) { + case ProjectType.residential: + return 'Residential'; + case ProjectType.commercial: + return 'Commercial'; + case ProjectType.industrial: + return 'Industrial'; + case ProjectType.infrastructure: + return 'Infrastructure'; + case ProjectType.other: + return 'Other'; + } + } +} diff --git a/lib/features/projects/domain/repositories/submissions_repository.dart b/lib/features/projects/domain/repositories/submissions_repository.dart index 3430d8c..1b528b2 100644 --- a/lib/features/projects/domain/repositories/submissions_repository.dart +++ b/lib/features/projects/domain/repositories/submissions_repository.dart @@ -3,24 +3,26 @@ /// Repository interface for project submissions operations. library; +import 'package:worker/features/projects/domain/entities/project_status.dart'; import 'package:worker/features/projects/domain/entities/project_submission.dart'; /// Submissions Repository /// /// Defines contract for project submissions data operations. abstract class SubmissionsRepository { + /// Get list of available project statuses + /// + /// Uses cache-first pattern: + /// - Returns cached data if available + /// - Fetches from API and updates cache + /// - [forceRefresh] bypasses cache and fetches fresh data + Future> getProjectStatusList({ + bool forceRefresh = false, + }); + /// 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); + Future> getSubmissions({ + int limitStart = 0, + int limitPageLength = 0, + }); } diff --git a/lib/features/projects/domain/usecases/get_submissions.dart b/lib/features/projects/domain/usecases/get_submissions.dart deleted file mode 100644 index 10fb1a1..0000000 --- a/lib/features/projects/domain/usecases/get_submissions.dart +++ /dev/null @@ -1,23 +0,0 @@ -/// 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 index acd4b36..75f3d38 100644 --- a/lib/features/projects/presentation/pages/submissions_page.dart +++ b/lib/features/projects/presentation/pages/submissions_page.dart @@ -21,6 +21,7 @@ class SubmissionsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final submissionsAsync = ref.watch(filteredSubmissionsProvider); + final statusListAsync = ref.watch(projectStatusListProvider); final filter = ref.watch(submissionsFilterProvider); final selectedStatus = filter.selectedStatus; @@ -53,7 +54,7 @@ class SubmissionsPage extends ConsumerWidget { padding: const EdgeInsets.all(16), child: TextField( decoration: InputDecoration( - hintText: 'Mã dự án hoặc tên dự án', + hintText: 'Mã dự án hoặc tên công trình', prefixIcon: const Icon(Icons.search, color: AppColors.grey500), filled: true, fillColor: AppColors.white, @@ -86,16 +87,23 @@ class SubmissionsPage extends ConsumerWidget { 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), + // Use projectStatusListProvider to get status options + statusListAsync.when( + data: (statuses) => Row( + children: statuses.map((status) => Padding( + padding: const EdgeInsets.only(right: 8), + child: _buildFilterChip( + context, + ref, + label: status.label, + isSelected: selectedStatus == status.label, + onTap: () => ref.read(submissionsFilterProvider.notifier).selectStatus(status.label), + ), + )).toList(), ), - )), + loading: () => const SizedBox.shrink(), + error: (_, __) => const SizedBox.shrink(), + ), ], ), ), @@ -268,19 +276,27 @@ class SubmissionsPage extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '#${submission.submissionId}', + submission.designedArea, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.grey900, ), ), - _buildStatusBadge(submission.status), + _buildStatusBadge(submission.status, submission.statusColor), ], ), const SizedBox(height: 8), + // Text( + // 'Tên công trình: ${submission.designedArea}', + // style: const TextStyle( + // fontSize: 14, + // color: AppColors.grey900, + // ), + // ), + // const SizedBox(height: 4), Text( - 'Tên công trình: ${submission.projectName}', + 'Ngày nộp: ${DateFormat('dd/MM/yyyy HH:mm').format(submission.requestDate)}', style: const TextStyle( fontSize: 14, color: AppColors.grey900, @@ -288,21 +304,13 @@ class SubmissionsPage extends ConsumerWidget { ), const SizedBox(height: 4), Text( - 'Ngày nộp: ${DateFormat('dd/MM/yyyy').format(submission.submittedAt)}', + 'Diện tích: ${submission.designArea} m²', style: const TextStyle( fontSize: 13, - color: AppColors.grey500, + color: AppColors.grey900, ), ), - 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) ...[ + if (submission.reasonForRejection != null) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), @@ -320,7 +328,7 @@ class SubmissionsPage extends ConsumerWidget { const SizedBox(width: 8), Expanded( child: Text( - submission.rejectionReason!, + submission.reasonForRejection!, style: const TextStyle( fontSize: 12, color: AppColors.danger, @@ -338,8 +346,8 @@ class SubmissionsPage extends ConsumerWidget { ); } - Widget _buildStatusBadge(SubmissionStatus status) { - final color = _getStatusColor(status); + Widget _buildStatusBadge(String status, String statusColor) { + final color = _getColorFromStatusColor(statusColor); return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( @@ -347,7 +355,7 @@ class SubmissionsPage extends ConsumerWidget { borderRadius: BorderRadius.circular(12), ), child: Text( - status.displayName, + status, style: TextStyle( color: color, fontSize: 12, @@ -357,16 +365,18 @@ class SubmissionsPage extends ConsumerWidget { ); } - Color _getStatusColor(SubmissionStatus status) { - switch (status) { - case SubmissionStatus.pending: + Color _getColorFromStatusColor(String statusColor) { + switch (statusColor) { + case 'Warning': return AppColors.warning; - case SubmissionStatus.reviewing: - return AppColors.info; - case SubmissionStatus.approved: + case 'Success': return AppColors.success; - case SubmissionStatus.rejected: + case 'Danger': return AppColors.danger; + case 'Info': + return AppColors.info; + default: + return AppColors.grey500; } } } diff --git a/lib/features/projects/presentation/providers/submissions_provider.dart b/lib/features/projects/presentation/providers/submissions_provider.dart index 6b823c8..b4bb680 100644 --- a/lib/features/projects/presentation/providers/submissions_provider.dart +++ b/lib/features/projects/presentation/providers/submissions_provider.dart @@ -4,51 +4,84 @@ library; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:worker/core/network/dio_client.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/repositories/submissions_repository_impl.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'; -import 'package:worker/features/projects/domain/usecases/get_submissions.dart'; part 'submissions_provider.g.dart'; +/// Project Status Local Data Source Provider +@riverpod +ProjectStatusLocalDataSource projectStatusLocalDataSource(Ref ref) { + return ProjectStatusLocalDataSource(); +} + /// Submissions Remote Data Source Provider @riverpod -SubmissionsRemoteDataSource submissionsRemoteDataSource(Ref ref) { - return SubmissionsRemoteDataSourceImpl(); +Future submissionsRemoteDataSource(Ref ref) async { + final dioClient = await ref.watch(dioClientProvider.future); + return SubmissionsRemoteDataSourceImpl(dioClient); } /// Submissions Repository Provider @riverpod -SubmissionsRepository submissionsRepository(Ref ref) { - final remoteDataSource = ref.watch(submissionsRemoteDataSourceProvider); - return SubmissionsRepositoryImpl(remoteDataSource); +Future submissionsRepository(Ref ref) async { + final remoteDataSource = await ref.watch(submissionsRemoteDataSourceProvider.future); + final statusLocalDataSource = ref.watch(projectStatusLocalDataSourceProvider); + return SubmissionsRepositoryImpl(remoteDataSource, statusLocalDataSource); } -/// Get Submissions Use Case Provider +/// 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 -GetSubmissions getSubmissions(Ref ref) { - final repository = ref.watch(submissionsRepositoryProvider); - return GetSubmissions(repository); +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); + }); + } } /// All Submissions Provider /// /// Fetches and manages submissions data from remote. +/// Waits for project status list to be loaded first. @riverpod class AllSubmissions extends _$AllSubmissions { @override Future> build() async { - final useCase = ref.watch(getSubmissionsProvider); - return await useCase(); + // Ensure status list is loaded first (for filter options) + await ref.watch(projectStatusListProvider.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 { - final useCase = ref.read(getSubmissionsProvider); - return await useCase(); + // Also refresh status list + await ref.read(projectStatusListProvider.notifier).refresh(); + + final repository = await ref.read(submissionsRepositoryProvider.future); + return repository.getSubmissions(); }); } } @@ -56,10 +89,11 @@ class AllSubmissions extends _$AllSubmissions { /// 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, SubmissionStatus? selectedStatus}) build() { + ({String searchQuery, String? selectedStatus}) build() { return (searchQuery: '', selectedStatus: null); } @@ -68,8 +102,8 @@ class SubmissionsFilter extends _$SubmissionsFilter { state = (searchQuery: query, selectedStatus: state.selectedStatus); } - /// Select a status filter - void selectStatus(SubmissionStatus? status) { + /// Select a status filter (uses Vietnamese label from API) + void selectStatus(String? status) { state = (searchQuery: state.searchQuery, selectedStatus: status); } @@ -100,7 +134,7 @@ AsyncValue> filteredSubmissions(Ref ref) { return dataAsync.whenData((submissions) { var filtered = submissions; - // Filter by status + // Filter by status (matches Vietnamese label from API) if (filter.selectedStatus != null) { filtered = filtered.where((s) => s.status == filter.selectedStatus).toList(); } @@ -110,12 +144,12 @@ AsyncValue> filteredSubmissions(Ref ref) { final query = filter.searchQuery.toLowerCase(); filtered = filtered.where((s) { return s.submissionId.toLowerCase().contains(query) || - s.projectName.toLowerCase().contains(query); + s.designedArea.toLowerCase().contains(query); }).toList(); } - // Sort by submitted date (newest first) - filtered.sort((a, b) => b.submittedAt.compareTo(a.submittedAt)); + // Sort by request date (newest first) + filtered.sort((a, b) => b.requestDate.compareTo(a.requestDate)); return filtered; }); diff --git a/lib/features/projects/presentation/providers/submissions_provider.g.dart b/lib/features/projects/presentation/providers/submissions_provider.g.dart index 14966a4..265baa4 100644 --- a/lib/features/projects/presentation/providers/submissions_provider.g.dart +++ b/lib/features/projects/presentation/providers/submissions_provider.g.dart @@ -8,6 +8,60 @@ part of 'submissions_provider.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning +/// Project Status Local Data Source Provider + +@ProviderFor(projectStatusLocalDataSource) +const projectStatusLocalDataSourceProvider = + ProjectStatusLocalDataSourceProvider._(); + +/// Project Status Local Data Source Provider + +final class ProjectStatusLocalDataSourceProvider + extends + $FunctionalProvider< + ProjectStatusLocalDataSource, + ProjectStatusLocalDataSource, + ProjectStatusLocalDataSource + > + with $Provider { + /// Project Status Local Data Source Provider + const ProjectStatusLocalDataSourceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'projectStatusLocalDataSourceProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$projectStatusLocalDataSourceHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + ProjectStatusLocalDataSource create(Ref ref) { + return projectStatusLocalDataSource(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ProjectStatusLocalDataSource value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$projectStatusLocalDataSourceHash() => + r'c57291e51bd390f9524369860c241d7a0a90fdbf'; + /// Submissions Remote Data Source Provider @ProviderFor(submissionsRemoteDataSource) @@ -19,11 +73,13 @@ const submissionsRemoteDataSourceProvider = final class SubmissionsRemoteDataSourceProvider extends $FunctionalProvider< + AsyncValue, SubmissionsRemoteDataSource, - SubmissionsRemoteDataSource, - SubmissionsRemoteDataSource + FutureOr > - with $Provider { + with + $FutureModifier, + $FutureProvider { /// Submissions Remote Data Source Provider const SubmissionsRemoteDataSourceProvider._() : super( @@ -41,26 +97,18 @@ final class SubmissionsRemoteDataSourceProvider @$internal @override - $ProviderElement $createElement( + $FutureProviderElement $createElement( $ProviderPointer pointer, - ) => $ProviderElement(pointer); + ) => $FutureProviderElement(pointer); @override - SubmissionsRemoteDataSource create(Ref ref) { + FutureOr 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'; + r'ffaa92dd55ef50c8f1166773a83cd5c8cc16ded4'; /// Submissions Repository Provider @@ -72,11 +120,13 @@ const submissionsRepositoryProvider = SubmissionsRepositoryProvider._(); final class SubmissionsRepositoryProvider extends $FunctionalProvider< + AsyncValue, SubmissionsRepository, - SubmissionsRepository, - SubmissionsRepository + FutureOr > - with $Provider { + with + $FutureModifier, + $FutureProvider { /// Submissions Repository Provider const SubmissionsRepositoryProvider._() : super( @@ -94,76 +144,87 @@ final class SubmissionsRepositoryProvider @$internal @override - $ProviderElement $createElement( + $FutureProviderElement $createElement( $ProviderPointer pointer, - ) => $ProviderElement(pointer); + ) => $FutureProviderElement(pointer); @override - SubmissionsRepository create(Ref ref) { + FutureOr 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'; + r'd8261cc538c1fdaa47064e4945302b80f49098bb'; -/// Get Submissions Use Case Provider +/// 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. -@ProviderFor(getSubmissions) -const getSubmissionsProvider = GetSubmissionsProvider._(); +@ProviderFor(ProjectStatusList) +const projectStatusListProvider = ProjectStatusListProvider._(); -/// Get Submissions Use Case Provider - -final class GetSubmissionsProvider - extends $FunctionalProvider - with $Provider { - /// Get Submissions Use Case Provider - const GetSubmissionsProvider._() +/// 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. +final class ProjectStatusListProvider + extends $AsyncNotifierProvider> { + /// 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. + const ProjectStatusListProvider._() : super( from: null, argument: null, retry: null, - name: r'getSubmissionsProvider', + name: r'projectStatusListProvider', isAutoDispose: true, dependencies: null, $allTransitiveDependencies: null, ); @override - String debugGetCreateSourceHash() => _$getSubmissionsHash(); + String debugGetCreateSourceHash() => _$projectStatusListHash(); @$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), - ); - } + ProjectStatusList create() => ProjectStatusList(); } -String _$getSubmissionsHash() => r'91b497f826ae6dc72618ba879289fc449a7ef5cb'; +String _$projectStatusListHash() => r'69a43b619738dec3a6643a9a780599417403b838'; + +/// 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. + +abstract class _$ProjectStatusList extends $AsyncNotifier> { + FutureOr> build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = + this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} /// All Submissions Provider /// /// Fetches and manages submissions data from remote. +/// Waits for project status list to be loaded first. @ProviderFor(AllSubmissions) const allSubmissionsProvider = AllSubmissionsProvider._(); @@ -171,11 +232,13 @@ const allSubmissionsProvider = AllSubmissionsProvider._(); /// All Submissions Provider /// /// Fetches and manages submissions data from remote. +/// Waits for project status list to be loaded first. final class AllSubmissionsProvider extends $AsyncNotifierProvider> { /// All Submissions Provider /// /// Fetches and manages submissions data from remote. + /// Waits for project status list to be loaded first. const AllSubmissionsProvider._() : super( from: null, @@ -195,11 +258,12 @@ final class AllSubmissionsProvider AllSubmissions create() => AllSubmissions(); } -String _$allSubmissionsHash() => r'40ea0460a8962a4105dabb482bc80573452d4c80'; +String _$allSubmissionsHash() => r'a4a7fb0d2953efb21e2e6343429f7550c763ea85'; /// All Submissions Provider /// /// Fetches and manages submissions data from remote. +/// Waits for project status list to be loaded first. abstract class _$AllSubmissions extends $AsyncNotifier> { @@ -232,6 +296,7 @@ abstract class _$AllSubmissions /// Submissions Filter State /// /// Manages search and status filter state. +/// Status filter uses the status label string from API (e.g., "Chờ phê duyệt"). @ProviderFor(SubmissionsFilter) const submissionsFilterProvider = SubmissionsFilterProvider._(); @@ -239,15 +304,17 @@ const submissionsFilterProvider = SubmissionsFilterProvider._(); /// Submissions Filter State /// /// Manages search and status filter state. +/// Status filter uses the status label string from API (e.g., "Chờ phê duyệt"). final class SubmissionsFilterProvider extends $NotifierProvider< SubmissionsFilter, - ({String searchQuery, SubmissionStatus? selectedStatus}) + ({String searchQuery, String? selectedStatus}) > { /// Submissions Filter State /// /// Manages search and status filter state. + /// Status filter uses the status label string from API (e.g., "Chờ phê duyệt"). const SubmissionsFilterProvider._() : super( from: null, @@ -268,28 +335,28 @@ final class SubmissionsFilterProvider /// {@macro riverpod.override_with_value} Override overrideWithValue( - ({String searchQuery, SubmissionStatus? selectedStatus}) value, + ({String searchQuery, String? selectedStatus}) value, ) { return $ProviderOverride( origin: this, providerOverride: - $SyncValueProvider< - ({String searchQuery, SubmissionStatus? selectedStatus}) - >(value), + $SyncValueProvider<({String searchQuery, String? selectedStatus})>( + value, + ), ); } } -String _$submissionsFilterHash() => r'049dd9fa4f6f1bff0d49c6cba0975f9714621883'; +String _$submissionsFilterHash() => r'b3c59003922b1786b71f68726f97b210eed94c89'; /// Submissions Filter State /// /// Manages search and status filter state. +/// Status filter uses the status label string from API (e.g., "Chờ phê duyệt"). abstract class _$SubmissionsFilter - extends - $Notifier<({String searchQuery, SubmissionStatus? selectedStatus})> { - ({String searchQuery, SubmissionStatus? selectedStatus}) build(); + extends $Notifier<({String searchQuery, String? selectedStatus})> { + ({String searchQuery, String? selectedStatus}) build(); @$mustCallSuper @override void runBuild() { @@ -297,17 +364,17 @@ abstract class _$SubmissionsFilter final ref = this.ref as $Ref< - ({String searchQuery, SubmissionStatus? selectedStatus}), - ({String searchQuery, SubmissionStatus? selectedStatus}) + ({String searchQuery, String? selectedStatus}), + ({String searchQuery, String? selectedStatus}) >; final element = ref.element as $ClassProviderElement< AnyNotifier< - ({String searchQuery, SubmissionStatus? selectedStatus}), - ({String searchQuery, SubmissionStatus? selectedStatus}) + ({String searchQuery, String? selectedStatus}), + ({String searchQuery, String? selectedStatus}) >, - ({String searchQuery, SubmissionStatus? selectedStatus}), + ({String searchQuery, String? selectedStatus}), Object?, Object? >; @@ -374,4 +441,4 @@ final class FilteredSubmissionsProvider } String _$filteredSubmissionsHash() => - r'd0a07ab78a0d98596f01d0ed0a25016d573db5aa'; + r'5be22b3242426c6b0c2f9778eaee5c7cf23e4814'; diff --git a/lib/hive_registrar.g.dart b/lib/hive_registrar.g.dart index 351c7d0..6d67584 100644 --- a/lib/hive_registrar.g.dart +++ b/lib/hive_registrar.g.dart @@ -32,6 +32,7 @@ import 'package:worker/features/products/data/models/category_model.dart'; import 'package:worker/features/products/data/models/product_model.dart'; import 'package:worker/features/products/data/models/stock_level_model.dart'; import 'package:worker/features/projects/data/models/design_request_model.dart'; +import 'package:worker/features/projects/data/models/project_status_model.dart'; import 'package:worker/features/projects/data/models/project_submission_model.dart'; import 'package:worker/features/quotes/data/models/quote_item_model.dart'; import 'package:worker/features/quotes/data/models/quote_model.dart'; @@ -76,6 +77,7 @@ extension HiveRegistrar on HiveInterface { registerAdapter(PointsRecordModelAdapter()); registerAdapter(PointsStatusAdapter()); registerAdapter(ProductModelAdapter()); + registerAdapter(ProjectStatusModelAdapter()); registerAdapter(ProjectSubmissionModelAdapter()); registerAdapter(ProjectTypeAdapter()); registerAdapter(PromotionModelAdapter()); @@ -135,6 +137,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface { registerAdapter(PointsRecordModelAdapter()); registerAdapter(PointsStatusAdapter()); registerAdapter(ProductModelAdapter()); + registerAdapter(ProjectStatusModelAdapter()); registerAdapter(ProjectSubmissionModelAdapter()); registerAdapter(ProjectTypeAdapter()); registerAdapter(PromotionModelAdapter()); diff --git a/pubspec.yaml b/pubspec.yaml index 0c51038..e0b06aa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.1+16 +version: 1.0.1+18 environment: sdk: ^3.10.0